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.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. 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
+ """