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,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chain workflow implementation using the clean BaseAgent adapter pattern.
|
|
3
|
+
|
|
4
|
+
This provides an implementation that delegates operations to a sequence of
|
|
5
|
+
other agents, chaining their outputs together.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, List, Optional, Tuple, Type
|
|
9
|
+
|
|
10
|
+
from mcp import Tool
|
|
11
|
+
from mcp.types import TextContent
|
|
12
|
+
from opentelemetry import trace
|
|
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.logging.logger import get_logger
|
|
17
|
+
from fast_agent.core.prompt import Prompt
|
|
18
|
+
from fast_agent.interfaces import ModelT
|
|
19
|
+
from fast_agent.types import PromptMessageExtended, RequestParams
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChainAgent(LlmAgent):
|
|
25
|
+
"""
|
|
26
|
+
A chain agent that processes requests through a series of specialized agents in sequence.
|
|
27
|
+
Passes the output of each agent to the next agent in the chain.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# TODO -- consider adding "repeat" mode
|
|
31
|
+
@property
|
|
32
|
+
def agent_type(self) -> AgentType:
|
|
33
|
+
"""Return the type of this agent."""
|
|
34
|
+
return AgentType.CHAIN
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
config: AgentConfig,
|
|
39
|
+
agents: List[LlmAgent],
|
|
40
|
+
cumulative: bool = False,
|
|
41
|
+
context: Optional[Any] = None,
|
|
42
|
+
**kwargs,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Initialize a ChainAgent.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
config: Agent configuration or name
|
|
49
|
+
agents: List of agents to chain together in sequence
|
|
50
|
+
cumulative: Whether each agent sees all previous responses
|
|
51
|
+
context: Optional context object
|
|
52
|
+
**kwargs: Additional keyword arguments to pass to BaseAgent
|
|
53
|
+
"""
|
|
54
|
+
super().__init__(config, context=context, **kwargs)
|
|
55
|
+
self.agents = agents
|
|
56
|
+
self.cumulative = cumulative
|
|
57
|
+
|
|
58
|
+
async def generate_impl(
|
|
59
|
+
self,
|
|
60
|
+
messages: List[PromptMessageExtended],
|
|
61
|
+
request_params: Optional[RequestParams] = None,
|
|
62
|
+
tools: List[Tool] | None = None,
|
|
63
|
+
) -> PromptMessageExtended:
|
|
64
|
+
"""
|
|
65
|
+
Chain the request through multiple agents in sequence.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
normalized_messages: Already normalized list of PromptMessageExtended
|
|
69
|
+
request_params: Optional request parameters
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The response from the final agent in the chain
|
|
73
|
+
"""
|
|
74
|
+
tracer = trace.get_tracer(__name__)
|
|
75
|
+
# Forward request params but strip any system prompt so subagents keep their own instructions.
|
|
76
|
+
|
|
77
|
+
with tracer.start_as_current_span(f"Chain: '{self._name}' generate"):
|
|
78
|
+
# Get the original user message (last message in the list)
|
|
79
|
+
user_message = messages[-1]
|
|
80
|
+
|
|
81
|
+
if not self.cumulative:
|
|
82
|
+
# First agent in chain
|
|
83
|
+
async with self.workflow_telemetry.start_step(
|
|
84
|
+
"chain.step",
|
|
85
|
+
server_name=self.name,
|
|
86
|
+
arguments={"agent": self.agents[0].name, "step": 1, "total": len(self.agents)},
|
|
87
|
+
) as step:
|
|
88
|
+
response: PromptMessageExtended = await self.agents[0].generate(
|
|
89
|
+
messages, request_params
|
|
90
|
+
)
|
|
91
|
+
await step.finish(
|
|
92
|
+
True, text=f"{self.agents[0].name} completed step 1/{len(self.agents)}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Process the rest of the agents in the chain
|
|
96
|
+
for i, agent in enumerate(self.agents[1:], start=2):
|
|
97
|
+
async with self.workflow_telemetry.start_step(
|
|
98
|
+
"chain.step",
|
|
99
|
+
server_name=self.name,
|
|
100
|
+
arguments={"agent": agent.name, "step": i, "total": len(self.agents)},
|
|
101
|
+
) as step:
|
|
102
|
+
next_message = Prompt.user(*response.content)
|
|
103
|
+
response = await agent.generate([next_message], request_params)
|
|
104
|
+
await step.finish(
|
|
105
|
+
True, text=f"{agent.name} completed step {i}/{len(self.agents)}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return response
|
|
109
|
+
|
|
110
|
+
# Track all responses in the chain
|
|
111
|
+
all_responses: List[PromptMessageExtended] = []
|
|
112
|
+
|
|
113
|
+
# Initialize list for storing formatted results
|
|
114
|
+
final_results: List[str] = []
|
|
115
|
+
|
|
116
|
+
# Add the original request with XML tag
|
|
117
|
+
request_text = f"<fastagent:request>{user_message.all_text() or '<no response>'}</fastagent:request>"
|
|
118
|
+
final_results.append(request_text)
|
|
119
|
+
|
|
120
|
+
# Process through each agent in sequence
|
|
121
|
+
for i, agent in enumerate(self.agents):
|
|
122
|
+
async with self.workflow_telemetry.start_step(
|
|
123
|
+
"chain.step",
|
|
124
|
+
server_name=self.name,
|
|
125
|
+
arguments={
|
|
126
|
+
"agent": agent.name,
|
|
127
|
+
"step": i + 1,
|
|
128
|
+
"total": len(self.agents),
|
|
129
|
+
"cumulative": True,
|
|
130
|
+
},
|
|
131
|
+
) as step:
|
|
132
|
+
# In cumulative mode, include the original message and all previous responses
|
|
133
|
+
chain_messages = messages.copy()
|
|
134
|
+
|
|
135
|
+
# Convert previous assistant responses to user messages for the next agent
|
|
136
|
+
for prev_response in all_responses:
|
|
137
|
+
chain_messages.append(Prompt.user(prev_response.all_text()))
|
|
138
|
+
|
|
139
|
+
current_response = await agent.generate(
|
|
140
|
+
chain_messages,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Store the response
|
|
144
|
+
all_responses.append(current_response)
|
|
145
|
+
|
|
146
|
+
response_text = current_response.all_text()
|
|
147
|
+
attributed_response = f"<fastagent:response agent='{agent.name}'>{response_text}</fastagent:response>"
|
|
148
|
+
final_results.append(attributed_response)
|
|
149
|
+
await step.finish(
|
|
150
|
+
True, text=f"{agent.name} completed step {i + 1}/{len(self.agents)}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# For cumulative mode, return the properly formatted output with XML tags
|
|
154
|
+
response_text = "\n\n".join(final_results)
|
|
155
|
+
return PromptMessageExtended(
|
|
156
|
+
role="assistant",
|
|
157
|
+
content=[TextContent(type="text", text=response_text)],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
async def structured_impl(
|
|
161
|
+
self,
|
|
162
|
+
messages: List[PromptMessageExtended],
|
|
163
|
+
model: Type[ModelT],
|
|
164
|
+
request_params: Optional[RequestParams] = None,
|
|
165
|
+
) -> Tuple[ModelT | None, PromptMessageExtended]:
|
|
166
|
+
"""
|
|
167
|
+
Chain the request through multiple agents and parse the final response.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
prompt: List of messages to send through the chain
|
|
171
|
+
model: Pydantic model to parse the final response into
|
|
172
|
+
request_params: Optional request parameters
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
The parsed response from the final agent, or None if parsing fails
|
|
176
|
+
"""
|
|
177
|
+
# Generate response through the chain
|
|
178
|
+
response = await self.generate(messages, request_params)
|
|
179
|
+
last_agent = self.agents[-1]
|
|
180
|
+
try:
|
|
181
|
+
forward_params = None
|
|
182
|
+
if request_params:
|
|
183
|
+
forward_params = request_params.model_copy(deep=True)
|
|
184
|
+
forward_params.systemPrompt = None
|
|
185
|
+
return await last_agent.structured([response], model, forward_params)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.warning(f"Failed to parse response from chain: {str(e)}")
|
|
188
|
+
return None, Prompt.assistant("Failed to parse response from chain: {str(e)}")
|
|
189
|
+
|
|
190
|
+
async def initialize(self) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Initialize the chain agent and all agents in the chain.
|
|
193
|
+
"""
|
|
194
|
+
await super().initialize()
|
|
195
|
+
|
|
196
|
+
# Initialize all agents in the chain if not already initialized
|
|
197
|
+
for agent in self.agents:
|
|
198
|
+
if not getattr(agent, "initialized", False):
|
|
199
|
+
await agent.initialize()
|
|
200
|
+
|
|
201
|
+
async def shutdown(self) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Shutdown the chain agent and all agents in the chain.
|
|
204
|
+
"""
|
|
205
|
+
await super().shutdown()
|
|
206
|
+
|
|
207
|
+
# Shutdown all agents in the chain
|
|
208
|
+
for agent in self.agents:
|
|
209
|
+
try:
|
|
210
|
+
await agent.shutdown()
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.warning(f"Error shutting down agent in chain: {str(e)}")
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Evaluator-Optimizer workflow implementation using the BaseAgent adapter pattern.
|
|
3
|
+
|
|
4
|
+
This workflow provides a mechanism for iterative refinement of responses through
|
|
5
|
+
evaluation and feedback cycles. It uses one agent to generate responses and another
|
|
6
|
+
to evaluate and provide feedback, continuing until a quality threshold is reached
|
|
7
|
+
or a maximum number of refinements is attempted.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, List, Optional, Tuple, Type
|
|
12
|
+
|
|
13
|
+
from mcp import Tool
|
|
14
|
+
from opentelemetry import trace
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from fast_agent.agents.agent_types import AgentConfig, AgentType
|
|
18
|
+
from fast_agent.agents.llm_agent import LlmAgent
|
|
19
|
+
from fast_agent.core.exceptions import AgentConfigError
|
|
20
|
+
from fast_agent.core.logging.logger import get_logger
|
|
21
|
+
from fast_agent.core.prompt import Prompt
|
|
22
|
+
from fast_agent.interfaces import AgentProtocol, ModelT
|
|
23
|
+
from fast_agent.types import PromptMessageExtended, RequestParams
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class QualityRating(str, Enum):
|
|
29
|
+
"""Enum for evaluation quality ratings."""
|
|
30
|
+
|
|
31
|
+
POOR = "POOR" # Major improvements needed
|
|
32
|
+
FAIR = "FAIR" # Several improvements needed
|
|
33
|
+
GOOD = "GOOD" # Minor improvements possible
|
|
34
|
+
EXCELLENT = "EXCELLENT" # No improvements needed
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Separate mapping for quality ratings to numerical values
|
|
38
|
+
QUALITY_RATING_VALUES = {
|
|
39
|
+
QualityRating.POOR: 0,
|
|
40
|
+
QualityRating.FAIR: 1,
|
|
41
|
+
QualityRating.GOOD: 2,
|
|
42
|
+
QualityRating.EXCELLENT: 3,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EvaluationResult(BaseModel):
|
|
47
|
+
"""Model representing the evaluation result from the evaluator agent."""
|
|
48
|
+
|
|
49
|
+
rating: QualityRating = Field(description="Quality rating of the response")
|
|
50
|
+
feedback: str = Field(description="Specific feedback and suggestions for improvement")
|
|
51
|
+
needs_improvement: bool = Field(description="Whether the output needs further improvement")
|
|
52
|
+
focus_areas: List[str] = Field(
|
|
53
|
+
default_factory=list, description="Specific areas to focus on in next iteration"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EvaluatorOptimizerAgent(LlmAgent):
|
|
58
|
+
"""
|
|
59
|
+
An agent that implements the evaluator-optimizer workflow pattern.
|
|
60
|
+
|
|
61
|
+
Uses one agent to generate responses and another to evaluate and provide feedback
|
|
62
|
+
for refinement, continuing until a quality threshold is reached or a maximum
|
|
63
|
+
number of refinement cycles is completed.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def agent_type(self) -> AgentType:
|
|
68
|
+
"""Return the type of this agent."""
|
|
69
|
+
return AgentType.EVALUATOR_OPTIMIZER
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
config: AgentConfig,
|
|
74
|
+
generator_agent: AgentProtocol,
|
|
75
|
+
evaluator_agent: AgentProtocol,
|
|
76
|
+
min_rating: QualityRating = QualityRating.GOOD,
|
|
77
|
+
max_refinements: int = 3,
|
|
78
|
+
refinement_instruction: str | None = None,
|
|
79
|
+
context: Optional[Any] = None,
|
|
80
|
+
**kwargs,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Initialize the evaluator-optimizer agent.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
config: Agent configuration or name
|
|
87
|
+
generator_agent: LlmAgent that generates the initial and refined responses
|
|
88
|
+
evaluator_agent: LlmAgent that evaluates responses and provides feedback
|
|
89
|
+
min_rating: Minimum acceptable quality rating to stop refinement
|
|
90
|
+
max_refinements: Maximum number of refinement cycles to attempt
|
|
91
|
+
context: Optional context object
|
|
92
|
+
**kwargs: Additional keyword arguments to pass to BaseAgent
|
|
93
|
+
"""
|
|
94
|
+
super().__init__(config, context=context, **kwargs)
|
|
95
|
+
|
|
96
|
+
if not generator_agent:
|
|
97
|
+
raise AgentConfigError("Generator agent must be provided")
|
|
98
|
+
|
|
99
|
+
if not evaluator_agent:
|
|
100
|
+
raise AgentConfigError("Evaluator agent must be provided")
|
|
101
|
+
|
|
102
|
+
self.generator_agent = generator_agent
|
|
103
|
+
self.evaluator_agent = evaluator_agent
|
|
104
|
+
self.min_rating = min_rating
|
|
105
|
+
self.max_refinements = max_refinements
|
|
106
|
+
self.refinement_history = []
|
|
107
|
+
self.refinement_instruction = refinement_instruction
|
|
108
|
+
|
|
109
|
+
async def generate_impl(
|
|
110
|
+
self,
|
|
111
|
+
messages: List[PromptMessageExtended],
|
|
112
|
+
request_params: RequestParams | None = None,
|
|
113
|
+
tools: List[Tool] | None = None,
|
|
114
|
+
) -> PromptMessageExtended:
|
|
115
|
+
"""
|
|
116
|
+
Generate a response through evaluation-guided refinement.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
normalized_messages: Already normalized list of PromptMessageExtended
|
|
120
|
+
request_params: Optional request parameters
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The optimized response after evaluation and refinement
|
|
124
|
+
"""
|
|
125
|
+
tracer = trace.get_tracer(__name__)
|
|
126
|
+
with tracer.start_as_current_span(f"EvaluatorOptimizer: '{self._name}' generate"):
|
|
127
|
+
# Initialize tracking variables
|
|
128
|
+
refinement_count = 0
|
|
129
|
+
best_response = None
|
|
130
|
+
best_rating = QualityRating.POOR
|
|
131
|
+
self.refinement_history = []
|
|
132
|
+
|
|
133
|
+
# Extract the user request
|
|
134
|
+
request = messages[-1].all_text() if messages else ""
|
|
135
|
+
|
|
136
|
+
# Initial generation
|
|
137
|
+
async with self.workflow_telemetry.start_step(
|
|
138
|
+
"evaluator_optimizer.generate",
|
|
139
|
+
server_name=self.name,
|
|
140
|
+
arguments={"agent": self.generator_agent.name, "iteration": 0},
|
|
141
|
+
) as step:
|
|
142
|
+
response = await self.generator_agent.generate(messages, request_params)
|
|
143
|
+
best_response = response
|
|
144
|
+
await step.finish(True, text=f"{self.generator_agent.name} generated initial response")
|
|
145
|
+
|
|
146
|
+
# Refinement loop
|
|
147
|
+
while refinement_count < self.max_refinements:
|
|
148
|
+
logger.debug(f"Evaluating response (iteration {refinement_count + 1})")
|
|
149
|
+
|
|
150
|
+
# Evaluate current response
|
|
151
|
+
async with self.workflow_telemetry.start_step(
|
|
152
|
+
"evaluator_optimizer.evaluate",
|
|
153
|
+
server_name=self.name,
|
|
154
|
+
arguments={
|
|
155
|
+
"agent": self.evaluator_agent.name,
|
|
156
|
+
"iteration": refinement_count + 1,
|
|
157
|
+
"max_refinements": self.max_refinements,
|
|
158
|
+
},
|
|
159
|
+
) as step:
|
|
160
|
+
eval_prompt = self._build_eval_prompt(
|
|
161
|
+
request=request, response=response.last_text() or "", iteration=refinement_count
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Create evaluation message and get structured evaluation result
|
|
165
|
+
eval_message = Prompt.user(eval_prompt)
|
|
166
|
+
evaluation_result, _ = await self.evaluator_agent.structured(
|
|
167
|
+
[eval_message], EvaluationResult, request_params
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# If structured parsing failed, use default evaluation
|
|
171
|
+
if evaluation_result is None:
|
|
172
|
+
logger.warning("Structured parsing failed, using default evaluation")
|
|
173
|
+
evaluation_result = EvaluationResult(
|
|
174
|
+
rating=QualityRating.POOR,
|
|
175
|
+
feedback="Failed to parse evaluation",
|
|
176
|
+
needs_improvement=True,
|
|
177
|
+
focus_areas=["Improve overall quality"],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
await step.finish(
|
|
181
|
+
True,
|
|
182
|
+
text=f"Evaluation {refinement_count + 1}/{self.max_refinements}: {evaluation_result.rating.value}",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Track iteration
|
|
186
|
+
self.refinement_history.append(
|
|
187
|
+
{
|
|
188
|
+
"attempt": refinement_count + 1,
|
|
189
|
+
"response": response.all_text(),
|
|
190
|
+
"evaluation": evaluation_result.model_dump(),
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
logger.debug(f"Evaluation result: {evaluation_result.rating}")
|
|
195
|
+
|
|
196
|
+
# Track best response based on rating
|
|
197
|
+
if QUALITY_RATING_VALUES[evaluation_result.rating] > QUALITY_RATING_VALUES[best_rating]:
|
|
198
|
+
best_rating = evaluation_result.rating
|
|
199
|
+
best_response = response
|
|
200
|
+
logger.debug(f"New best response (rating: {best_rating})")
|
|
201
|
+
|
|
202
|
+
# Check if we've reached acceptable quality
|
|
203
|
+
if not evaluation_result.needs_improvement:
|
|
204
|
+
logger.debug("Improvement not needed, stopping refinement")
|
|
205
|
+
# When evaluator says no improvement needed, use the current response
|
|
206
|
+
best_response = response
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
if (
|
|
210
|
+
QUALITY_RATING_VALUES[evaluation_result.rating]
|
|
211
|
+
>= QUALITY_RATING_VALUES[self.min_rating]
|
|
212
|
+
):
|
|
213
|
+
logger.debug(f"Acceptable quality reached ({evaluation_result.rating})")
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
# Generate refined response
|
|
217
|
+
async with self.workflow_telemetry.start_step(
|
|
218
|
+
"evaluator_optimizer.refine",
|
|
219
|
+
server_name=self.name,
|
|
220
|
+
arguments={
|
|
221
|
+
"agent": self.generator_agent.name,
|
|
222
|
+
"iteration": refinement_count + 1,
|
|
223
|
+
"previous_rating": evaluation_result.rating.value,
|
|
224
|
+
},
|
|
225
|
+
) as step:
|
|
226
|
+
refinement_prompt = self._build_refinement_prompt(
|
|
227
|
+
feedback=evaluation_result,
|
|
228
|
+
iteration=refinement_count,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Create refinement message and get refined response
|
|
232
|
+
refinement_message = Prompt.user(refinement_prompt)
|
|
233
|
+
response = await self.generator_agent.generate([refinement_message], request_params)
|
|
234
|
+
await step.finish(
|
|
235
|
+
True,
|
|
236
|
+
text=f"{self.generator_agent.name} refined response (iteration {refinement_count + 1})",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
refinement_count += 1
|
|
240
|
+
|
|
241
|
+
return best_response
|
|
242
|
+
|
|
243
|
+
async def structured_impl(
|
|
244
|
+
self,
|
|
245
|
+
messages: List[PromptMessageExtended],
|
|
246
|
+
model: Type[ModelT],
|
|
247
|
+
request_params: RequestParams | None = None,
|
|
248
|
+
) -> Tuple[ModelT | None, PromptMessageExtended]:
|
|
249
|
+
"""
|
|
250
|
+
Generate an optimized response and parse it into a structured format.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
messages: List of messages to process
|
|
254
|
+
model: Pydantic model to parse the response into
|
|
255
|
+
request_params: Optional request parameters
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The parsed response, or None if parsing fails
|
|
259
|
+
"""
|
|
260
|
+
# Generate optimized response
|
|
261
|
+
response = await self.generate_impl(messages, request_params)
|
|
262
|
+
|
|
263
|
+
# Delegate structured parsing to the generator agent
|
|
264
|
+
structured_prompt = Prompt.user(response.all_text())
|
|
265
|
+
return await self.generator_agent.structured([structured_prompt], model, request_params)
|
|
266
|
+
|
|
267
|
+
async def initialize(self) -> None:
|
|
268
|
+
"""Initialize the agent and its generator and evaluator agents."""
|
|
269
|
+
await super().initialize()
|
|
270
|
+
|
|
271
|
+
# Initialize generator and evaluator agents if not already initialized
|
|
272
|
+
if not self.generator_agent.initialized:
|
|
273
|
+
await self.generator_agent.initialize()
|
|
274
|
+
|
|
275
|
+
if not self.evaluator_agent.initialized:
|
|
276
|
+
await self.evaluator_agent.initialize()
|
|
277
|
+
|
|
278
|
+
self.initialized = True
|
|
279
|
+
|
|
280
|
+
async def shutdown(self) -> None:
|
|
281
|
+
"""Shutdown the agent and its generator and evaluator agents."""
|
|
282
|
+
await super().shutdown()
|
|
283
|
+
|
|
284
|
+
# Shutdown generator and evaluator agents
|
|
285
|
+
try:
|
|
286
|
+
await self.generator_agent.shutdown()
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.warning(f"Error shutting down generator agent: {str(e)}")
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
await self.evaluator_agent.shutdown()
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.warning(f"Error shutting down evaluator agent: {str(e)}")
|
|
294
|
+
|
|
295
|
+
def _build_eval_prompt(self, request: str, response: str, iteration: int) -> str:
|
|
296
|
+
"""
|
|
297
|
+
Build the evaluation prompt for the evaluator agent.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
request: The original user request
|
|
301
|
+
response: The current response to evaluate
|
|
302
|
+
iteration: The current iteration number
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Formatted evaluation prompt
|
|
306
|
+
"""
|
|
307
|
+
return f"""
|
|
308
|
+
{self.refinement_instruction or 'You are an expert evaluator for content quality.'}
|
|
309
|
+
Your task is to evaluate a response against the user's original request.
|
|
310
|
+
Evaluate the response for iteration {iteration + 1} and provide feedback on its quality and areas for improvement.
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
<fastagent:data>
|
|
314
|
+
<fastagent:request>
|
|
315
|
+
{request}
|
|
316
|
+
</fastagent:request>
|
|
317
|
+
|
|
318
|
+
<fastagent:response>
|
|
319
|
+
{response}
|
|
320
|
+
</fastagent:response>
|
|
321
|
+
</fastagent:data>
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
def _build_refinement_prompt(
|
|
328
|
+
self,
|
|
329
|
+
feedback: EvaluationResult,
|
|
330
|
+
iteration: int,
|
|
331
|
+
) -> str:
|
|
332
|
+
"""
|
|
333
|
+
Build the refinement prompt for the generator agent.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
request: The original user request
|
|
337
|
+
response: The current response to refine
|
|
338
|
+
feedback: The evaluation feedback
|
|
339
|
+
iteration: The current iteration number
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Formatted refinement prompt
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
# Format focus areas as bulleted list with each item on a separate line
|
|
346
|
+
if feedback.focus_areas:
|
|
347
|
+
focus_areas = "\n".join(f" * {area}" for area in feedback.focus_areas)
|
|
348
|
+
else:
|
|
349
|
+
focus_areas = "None specified"
|
|
350
|
+
|
|
351
|
+
return f"""
|
|
352
|
+
You are tasked with improving your previous response.
|
|
353
|
+
{self.refinement_instruction or 'You are an expert evaluator for content quality.'}
|
|
354
|
+
This is iteration {iteration + 1} of the refinement process.
|
|
355
|
+
|
|
356
|
+
Your goal is to address all feedback points while maintaining accuracy and relevance to the original request.
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
<fastagent:feedback>
|
|
361
|
+
<rating>{feedback.rating.name}</rating>
|
|
362
|
+
<details>{feedback.feedback}</details>
|
|
363
|
+
<focus-areas>
|
|
364
|
+
{focus_areas}
|
|
365
|
+
</focus-areas>
|
|
366
|
+
</fastagent:feedback>
|
|
367
|
+
|
|
368
|
+
<fastagent:instruction>
|
|
369
|
+
Create an improved version of the response that:
|
|
370
|
+
1. Directly addresses each point in the feedback
|
|
371
|
+
2. Focuses on the specific areas mentioned for improvement
|
|
372
|
+
3. Maintains all the strengths of the original response
|
|
373
|
+
4. Remains accurate and relevant to the original request
|
|
374
|
+
|
|
375
|
+
Provide your complete improved response without explanations or commentary.
|
|
376
|
+
</fastagent:instruction>
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
"""
|