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,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MAKER: Massively decomposed Agentic processes with K-voting Error Reduction.
|
|
3
|
+
|
|
4
|
+
Implementation based on the paper:
|
|
5
|
+
"Solving a Million-Step LLM Task with Zero Errors" (arXiv:2511.09030)
|
|
6
|
+
https://arxiv.org/abs/2511.09030
|
|
7
|
+
|
|
8
|
+
This workflow implements first-to-ahead-by-k voting for statistical error
|
|
9
|
+
correction, enabling high reliability with cost-effective models. The key
|
|
10
|
+
insight is that by sampling multiple responses and requiring a k-vote margin
|
|
11
|
+
for consensus, the probability of error decreases exponentially while cost
|
|
12
|
+
grows only logarithmically with the number of steps.
|
|
13
|
+
|
|
14
|
+
Key concepts from the paper:
|
|
15
|
+
- Maximal Agentic Decomposition (MAD): Break tasks into single-step subtasks
|
|
16
|
+
- First-to-ahead-by-k voting: Winner needs k more votes than runner-up
|
|
17
|
+
- Red-flagging: Discard suspicious outputs (too long, malformed) before voting
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from enum import StrEnum
|
|
22
|
+
from typing import Any, Callable, List, Optional, Tuple, Type
|
|
23
|
+
|
|
24
|
+
from mcp import Tool
|
|
25
|
+
from opentelemetry import trace
|
|
26
|
+
from pydantic import BaseModel, Field
|
|
27
|
+
|
|
28
|
+
from fast_agent.agents.agent_types import AgentConfig, AgentType
|
|
29
|
+
from fast_agent.agents.llm_agent import LlmAgent
|
|
30
|
+
from fast_agent.core.exceptions import AgentConfigError
|
|
31
|
+
from fast_agent.core.logging.logger import get_logger
|
|
32
|
+
from fast_agent.core.prompt import Prompt
|
|
33
|
+
from fast_agent.interfaces import AgentProtocol, ModelT
|
|
34
|
+
from fast_agent.types import PromptMessageExtended, RequestParams
|
|
35
|
+
|
|
36
|
+
logger = get_logger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MatchStrategy(StrEnum):
|
|
40
|
+
"""
|
|
41
|
+
Strategies for comparing responses during voting.
|
|
42
|
+
|
|
43
|
+
The choice of strategy affects how responses are grouped for voting:
|
|
44
|
+
- EXACT: Responses must match character-for-character
|
|
45
|
+
- NORMALIZED: Whitespace and case differences are ignored
|
|
46
|
+
- STRUCTURED: JSON responses are parsed and compared structurally
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
EXACT = "exact"
|
|
50
|
+
NORMALIZED = "normalized"
|
|
51
|
+
STRUCTURED = "structured"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MakerResult(BaseModel):
|
|
55
|
+
"""
|
|
56
|
+
Result of a MAKER voting process.
|
|
57
|
+
|
|
58
|
+
Provides transparency into the voting outcome for debugging and analysis.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
winner: str = Field(description="The winning response text")
|
|
62
|
+
votes: dict[str, int] = Field(
|
|
63
|
+
default_factory=dict, description="Vote counts per unique response"
|
|
64
|
+
)
|
|
65
|
+
total_samples: int = Field(default=0, description="Total samples drawn")
|
|
66
|
+
discarded_samples: int = Field(
|
|
67
|
+
default=0, description="Samples discarded due to red-flags"
|
|
68
|
+
)
|
|
69
|
+
margin: int = Field(default=0, description="Winning margin achieved")
|
|
70
|
+
converged: bool = Field(
|
|
71
|
+
default=False, description="Whether k-margin consensus was achieved"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MakerAgent(LlmAgent):
|
|
76
|
+
"""
|
|
77
|
+
MAKER: Massively decomposed Agentic processes with K-voting Error Reduction.
|
|
78
|
+
|
|
79
|
+
Implements first-to-ahead-by-k voting for statistical error correction.
|
|
80
|
+
Multiple samples are drawn from a worker agent, and the first response
|
|
81
|
+
to achieve a k-vote margin over all alternatives wins.
|
|
82
|
+
|
|
83
|
+
This approach enables:
|
|
84
|
+
- High reliability with cheap/small models
|
|
85
|
+
- Logarithmic cost scaling with task complexity
|
|
86
|
+
- Provable error bounds based on per-step success rate
|
|
87
|
+
|
|
88
|
+
Reference: "Solving a Million-Step LLM Task with Zero Errors"
|
|
89
|
+
https://arxiv.org/abs/2511.09030
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def agent_type(self) -> AgentType:
|
|
94
|
+
"""Return the type of this agent."""
|
|
95
|
+
return AgentType.MAKER
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
config: AgentConfig,
|
|
100
|
+
worker_agent: AgentProtocol,
|
|
101
|
+
k: int = 3,
|
|
102
|
+
max_samples: int = 50,
|
|
103
|
+
match_strategy: MatchStrategy = MatchStrategy.EXACT,
|
|
104
|
+
match_fn: Callable[[str], str] | None = None,
|
|
105
|
+
red_flag_max_length: int | None = None,
|
|
106
|
+
red_flag_validator: Callable[[str], bool] | None = None,
|
|
107
|
+
context: Optional[Any] = None,
|
|
108
|
+
**kwargs,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Initialize the MAKER agent.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
config: Agent configuration
|
|
115
|
+
worker_agent: The agent to sample from for voting
|
|
116
|
+
k: Margin required to declare a winner (first-to-ahead-by-k).
|
|
117
|
+
Higher k = more reliable but more samples needed.
|
|
118
|
+
Paper recommends k >= 3 for high reliability.
|
|
119
|
+
max_samples: Maximum samples before falling back to plurality vote
|
|
120
|
+
match_strategy: How to compare responses for voting
|
|
121
|
+
match_fn: Custom function to normalize responses for comparison.
|
|
122
|
+
If provided, overrides match_strategy.
|
|
123
|
+
red_flag_max_length: Discard responses longer than this (characters).
|
|
124
|
+
Per the paper, overly long responses correlate
|
|
125
|
+
with errors.
|
|
126
|
+
red_flag_validator: Custom validator function. Return False to
|
|
127
|
+
discard the response (red-flag it).
|
|
128
|
+
context: Optional context object
|
|
129
|
+
"""
|
|
130
|
+
super().__init__(config, context=context, **kwargs)
|
|
131
|
+
|
|
132
|
+
if not worker_agent:
|
|
133
|
+
raise AgentConfigError("Worker agent must be provided")
|
|
134
|
+
if k < 1:
|
|
135
|
+
raise AgentConfigError("k must be at least 1")
|
|
136
|
+
if max_samples < k:
|
|
137
|
+
raise AgentConfigError("max_samples must be at least k")
|
|
138
|
+
|
|
139
|
+
self.worker_agent = worker_agent
|
|
140
|
+
self.k = k
|
|
141
|
+
self.max_samples = max_samples
|
|
142
|
+
self.match_strategy = match_strategy
|
|
143
|
+
self.match_fn = match_fn
|
|
144
|
+
self.red_flag_max_length = red_flag_max_length
|
|
145
|
+
self.red_flag_validator = red_flag_validator
|
|
146
|
+
|
|
147
|
+
# Result tracking
|
|
148
|
+
self.last_result: MakerResult | None = None
|
|
149
|
+
|
|
150
|
+
def _normalize_response(self, response: str) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Normalize response for comparison based on configured strategy.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
response: Raw response text
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Normalized response for vote counting
|
|
159
|
+
"""
|
|
160
|
+
if self.match_fn:
|
|
161
|
+
return self.match_fn(response)
|
|
162
|
+
|
|
163
|
+
match self.match_strategy:
|
|
164
|
+
case MatchStrategy.EXACT:
|
|
165
|
+
return response
|
|
166
|
+
case MatchStrategy.NORMALIZED:
|
|
167
|
+
return " ".join(response.lower().split())
|
|
168
|
+
case MatchStrategy.STRUCTURED:
|
|
169
|
+
import json
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
parsed = json.loads(response)
|
|
173
|
+
return json.dumps(parsed, sort_keys=True)
|
|
174
|
+
except json.JSONDecodeError:
|
|
175
|
+
return response
|
|
176
|
+
|
|
177
|
+
return response
|
|
178
|
+
|
|
179
|
+
def _is_red_flagged(self, response: str) -> bool:
|
|
180
|
+
"""
|
|
181
|
+
Check if response should be discarded (red-flagged).
|
|
182
|
+
|
|
183
|
+
Per the MAKER paper, red-flagging improves effective success rate
|
|
184
|
+
by discarding responses that show signs of confusion:
|
|
185
|
+
- Overly long responses (model went off track)
|
|
186
|
+
- Malformed responses (parsing issues indicate confusion)
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
response: Response text to check
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if response should be discarded
|
|
193
|
+
"""
|
|
194
|
+
if self.red_flag_max_length and len(response) > self.red_flag_max_length:
|
|
195
|
+
logger.debug(
|
|
196
|
+
f"Red-flagged: response length {len(response)} > {self.red_flag_max_length}"
|
|
197
|
+
)
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
if self.red_flag_validator and not self.red_flag_validator(response):
|
|
201
|
+
logger.debug("Red-flagged: custom validator returned False")
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
def _check_winner(self, votes: dict[str, int]) -> str | None:
|
|
207
|
+
"""
|
|
208
|
+
Check if any response has achieved k-margin victory.
|
|
209
|
+
|
|
210
|
+
First-to-ahead-by-k: winner needs k more votes than the runner-up.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
votes: Current vote counts
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Winning response key if k-margin achieved, None otherwise
|
|
217
|
+
"""
|
|
218
|
+
if not votes:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
sorted_items = sorted(votes.items(), key=lambda x: x[1], reverse=True)
|
|
222
|
+
leader_key, leader_votes = sorted_items[0]
|
|
223
|
+
runner_up_votes = sorted_items[1][1] if len(sorted_items) > 1 else 0
|
|
224
|
+
|
|
225
|
+
if leader_votes - runner_up_votes >= self.k:
|
|
226
|
+
return leader_key
|
|
227
|
+
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
async def generate_impl(
|
|
231
|
+
self,
|
|
232
|
+
messages: List[PromptMessageExtended],
|
|
233
|
+
request_params: RequestParams | None = None,
|
|
234
|
+
tools: List[Tool] | None = None,
|
|
235
|
+
) -> PromptMessageExtended:
|
|
236
|
+
"""
|
|
237
|
+
Generate a response using first-to-ahead-by-k voting.
|
|
238
|
+
|
|
239
|
+
Samples from the worker agent until one response achieves a k-vote
|
|
240
|
+
margin over all alternatives, or max_samples is reached.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
messages: Input messages
|
|
244
|
+
request_params: Optional request parameters
|
|
245
|
+
tools: Optional tools (passed to worker)
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The winning response
|
|
249
|
+
"""
|
|
250
|
+
tracer = trace.get_tracer(__name__)
|
|
251
|
+
with tracer.start_as_current_span(f"Maker: '{self._name}' generate"):
|
|
252
|
+
votes: dict[str, int] = defaultdict(int)
|
|
253
|
+
response_map: dict[str, PromptMessageExtended] = {}
|
|
254
|
+
total_samples = 0
|
|
255
|
+
discarded_samples = 0
|
|
256
|
+
|
|
257
|
+
while total_samples < self.max_samples:
|
|
258
|
+
async with self.workflow_telemetry.start_step(
|
|
259
|
+
"maker.sample",
|
|
260
|
+
server_name=self.name,
|
|
261
|
+
arguments={
|
|
262
|
+
"agent": self.worker_agent.name,
|
|
263
|
+
"sample": total_samples + 1,
|
|
264
|
+
"current_votes": dict(votes),
|
|
265
|
+
},
|
|
266
|
+
) as step:
|
|
267
|
+
response = await self.worker_agent.generate(
|
|
268
|
+
messages, request_params
|
|
269
|
+
)
|
|
270
|
+
response_text = response.last_text() or ""
|
|
271
|
+
total_samples += 1
|
|
272
|
+
|
|
273
|
+
# Red-flag check
|
|
274
|
+
if self._is_red_flagged(response_text):
|
|
275
|
+
discarded_samples += 1
|
|
276
|
+
await step.finish(
|
|
277
|
+
False, text=f"Sample {total_samples} red-flagged, discarded"
|
|
278
|
+
)
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# Normalize and record vote
|
|
282
|
+
normalized = self._normalize_response(response_text)
|
|
283
|
+
votes[normalized] += 1
|
|
284
|
+
response_map[normalized] = response
|
|
285
|
+
|
|
286
|
+
await step.finish(
|
|
287
|
+
True,
|
|
288
|
+
text=f"Sample {total_samples}: {votes[normalized]} votes for this response",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Check for k-margin winner
|
|
292
|
+
winner_key = self._check_winner(votes)
|
|
293
|
+
if winner_key:
|
|
294
|
+
sorted_votes = sorted(votes.values(), reverse=True)
|
|
295
|
+
margin = sorted_votes[0] - (
|
|
296
|
+
sorted_votes[1] if len(sorted_votes) > 1 else 0
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
self.last_result = MakerResult(
|
|
300
|
+
winner=winner_key,
|
|
301
|
+
votes=dict(votes),
|
|
302
|
+
total_samples=total_samples,
|
|
303
|
+
discarded_samples=discarded_samples,
|
|
304
|
+
margin=margin,
|
|
305
|
+
converged=True,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
logger.debug(
|
|
309
|
+
f"MAKER converged: {votes[winner_key]} votes, "
|
|
310
|
+
f"margin {margin}, {total_samples} samples"
|
|
311
|
+
)
|
|
312
|
+
return response_map[winner_key]
|
|
313
|
+
|
|
314
|
+
# Max samples reached - fall back to plurality
|
|
315
|
+
logger.warning(
|
|
316
|
+
f"MAKER: max_samples ({self.max_samples}) reached without "
|
|
317
|
+
f"k-margin ({self.k}) consensus, using plurality"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if not votes:
|
|
321
|
+
# All samples were red-flagged
|
|
322
|
+
raise AgentConfigError(
|
|
323
|
+
f"All {total_samples} samples were red-flagged. "
|
|
324
|
+
"Consider relaxing red-flag criteria."
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
winner_key = max(votes, key=lambda x: votes[x])
|
|
328
|
+
sorted_votes = sorted(votes.values(), reverse=True)
|
|
329
|
+
margin = sorted_votes[0] - (
|
|
330
|
+
sorted_votes[1] if len(sorted_votes) > 1 else 0
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
self.last_result = MakerResult(
|
|
334
|
+
winner=winner_key,
|
|
335
|
+
votes=dict(votes),
|
|
336
|
+
total_samples=total_samples,
|
|
337
|
+
discarded_samples=discarded_samples,
|
|
338
|
+
margin=margin,
|
|
339
|
+
converged=False,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return response_map[winner_key]
|
|
343
|
+
|
|
344
|
+
async def structured_impl(
|
|
345
|
+
self,
|
|
346
|
+
messages: List[PromptMessageExtended],
|
|
347
|
+
model: Type[ModelT],
|
|
348
|
+
request_params: RequestParams | None = None,
|
|
349
|
+
) -> Tuple[ModelT | None, PromptMessageExtended]:
|
|
350
|
+
"""
|
|
351
|
+
Generate a voted response and parse into structured format.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
messages: Input messages
|
|
355
|
+
model: Pydantic model class for structured output
|
|
356
|
+
request_params: Optional request parameters
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Tuple of (parsed model or None, raw response)
|
|
360
|
+
"""
|
|
361
|
+
response = await self.generate_impl(messages, request_params)
|
|
362
|
+
return await self.worker_agent.structured(
|
|
363
|
+
[Prompt.user(response.all_text())], model, request_params
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
async def initialize(self) -> None:
|
|
367
|
+
"""Initialize the agent and its worker agent."""
|
|
368
|
+
await super().initialize()
|
|
369
|
+
if not self.worker_agent.initialized:
|
|
370
|
+
await self.worker_agent.initialize()
|
|
371
|
+
self.initialized = True
|
|
372
|
+
|
|
373
|
+
async def shutdown(self) -> None:
|
|
374
|
+
"""Shutdown the agent and its worker agent."""
|
|
375
|
+
await super().shutdown()
|
|
376
|
+
try:
|
|
377
|
+
await self.worker_agent.shutdown()
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.warning(f"Error shutting down worker agent: {str(e)}")
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
3
|
+
|
|
4
|
+
from fast_agent.agents.workflow.orchestrator_prompts import (
|
|
5
|
+
PLAN_RESULT_TEMPLATE,
|
|
6
|
+
STEP_RESULT_TEMPLATE,
|
|
7
|
+
TASK_RESULT_TEMPLATE,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Task(BaseModel):
|
|
12
|
+
"""An individual task that needs to be executed"""
|
|
13
|
+
|
|
14
|
+
description: str = Field(description="Description of the task")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ServerTask(Task):
|
|
18
|
+
"""An individual task that can be accomplished by one or more MCP servers"""
|
|
19
|
+
|
|
20
|
+
servers: list[str] = Field(
|
|
21
|
+
description="Names of MCP servers that the LLM has access to for this task",
|
|
22
|
+
default_factory=list,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentTask(Task):
|
|
27
|
+
"""An individual task that can be accomplished by an Agent."""
|
|
28
|
+
|
|
29
|
+
agent: str = Field(
|
|
30
|
+
description="Name of Agent from given list of agents that the LLM has access to for this task",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Step(BaseModel):
|
|
35
|
+
"""A step containing independent tasks that can be executed in parallel"""
|
|
36
|
+
|
|
37
|
+
description: str = Field(description="Description of the step")
|
|
38
|
+
|
|
39
|
+
tasks: list[AgentTask] = Field(
|
|
40
|
+
description="Subtasks that can be executed in parallel",
|
|
41
|
+
default_factory=list,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PlanningStep(Step):
|
|
46
|
+
"""Single next step in iterative planning"""
|
|
47
|
+
|
|
48
|
+
is_complete: bool = Field(description="Whether the overall plan objective is complete")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Plan(BaseModel):
|
|
52
|
+
"""Plan generated by the orchestrator planner."""
|
|
53
|
+
|
|
54
|
+
steps: list[Step] = Field(
|
|
55
|
+
description="List of steps to execute sequentially",
|
|
56
|
+
default_factory=list,
|
|
57
|
+
)
|
|
58
|
+
is_complete: bool = Field(description="Whether the overall plan objective is complete")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TaskWithResult(Task):
|
|
62
|
+
"""An individual task with its result"""
|
|
63
|
+
|
|
64
|
+
result: str = Field(description="Result of executing the task", default="Task completed")
|
|
65
|
+
|
|
66
|
+
agent: str = Field(description="Name of the agent that executed this task", default="")
|
|
67
|
+
|
|
68
|
+
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StepResult(BaseModel):
|
|
72
|
+
"""Result of executing a step"""
|
|
73
|
+
|
|
74
|
+
step: Step = Field(description="The step that was executed")
|
|
75
|
+
task_results: list[TaskWithResult] = Field(
|
|
76
|
+
description="Results of executing each task", default_factory=list
|
|
77
|
+
)
|
|
78
|
+
result: str = Field(description="Result of executing the step", default="Step completed")
|
|
79
|
+
|
|
80
|
+
def add_task_result(self, task_result: TaskWithResult) -> None:
|
|
81
|
+
"""Add a task result to this step"""
|
|
82
|
+
if not isinstance(self.task_results, list):
|
|
83
|
+
self.task_results = []
|
|
84
|
+
self.task_results.append(task_result)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class PlanResult(BaseModel):
|
|
88
|
+
"""Results of executing a plan"""
|
|
89
|
+
|
|
90
|
+
objective: str
|
|
91
|
+
"""Objective of the plan"""
|
|
92
|
+
|
|
93
|
+
plan: Plan | None = None
|
|
94
|
+
"""The plan that was executed"""
|
|
95
|
+
|
|
96
|
+
step_results: list[StepResult]
|
|
97
|
+
"""Results of executing each step"""
|
|
98
|
+
|
|
99
|
+
is_complete: bool = False
|
|
100
|
+
"""Whether the overall plan objective is complete"""
|
|
101
|
+
|
|
102
|
+
max_iterations_reached: bool = False
|
|
103
|
+
"""Whether the plan execution reached the maximum number of iterations without completing"""
|
|
104
|
+
|
|
105
|
+
result: str | None = None
|
|
106
|
+
"""Result of executing the plan"""
|
|
107
|
+
|
|
108
|
+
def add_step_result(self, step_result: StepResult) -> None:
|
|
109
|
+
"""Add a step result to this plan"""
|
|
110
|
+
if not isinstance(self.step_results, list):
|
|
111
|
+
self.step_results = []
|
|
112
|
+
self.step_results.append(step_result)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def format_task_result_text(task_result: TaskWithResult) -> str:
|
|
116
|
+
"""Format a task result as plain text for display"""
|
|
117
|
+
return TASK_RESULT_TEMPLATE.format(
|
|
118
|
+
task_description=task_result.description, task_result=task_result.result
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def format_step_result_text(step_result: StepResult) -> str:
|
|
123
|
+
"""Format a step result as plain text for display"""
|
|
124
|
+
tasks_str = "\n".join(
|
|
125
|
+
f" - {format_task_result_text(task)}" for task in step_result.task_results
|
|
126
|
+
)
|
|
127
|
+
return STEP_RESULT_TEMPLATE.format(
|
|
128
|
+
step_description=step_result.step.description,
|
|
129
|
+
step_result=step_result.result,
|
|
130
|
+
tasks_str=tasks_str,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def format_plan_result_text(plan_result: PlanResult) -> str:
|
|
135
|
+
"""Format the full plan execution state as plain text for display"""
|
|
136
|
+
steps_str = (
|
|
137
|
+
"\n\n".join(
|
|
138
|
+
f"{i + 1}:\n{format_step_result_text(step)}"
|
|
139
|
+
for i, step in enumerate(plan_result.step_results)
|
|
140
|
+
)
|
|
141
|
+
if plan_result.step_results
|
|
142
|
+
else "No steps executed yet"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return PLAN_RESULT_TEMPLATE.format(
|
|
146
|
+
plan_objective=plan_result.objective,
|
|
147
|
+
steps_str=steps_str,
|
|
148
|
+
plan_result=plan_result.result if plan_result.is_complete else "In Progress",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def format_task_result_xml(task_result: TaskWithResult) -> str:
|
|
153
|
+
"""Format a task result with XML tags for better semantic understanding"""
|
|
154
|
+
from fast_agent.llm.prompt_utils import format_fastagent_tag
|
|
155
|
+
|
|
156
|
+
return format_fastagent_tag(
|
|
157
|
+
"task-result",
|
|
158
|
+
f"\n<fastagent:description>{task_result.description}</fastagent:description>\n"
|
|
159
|
+
f"<fastagent:result>{task_result.result}</fastagent:result>\n",
|
|
160
|
+
{
|
|
161
|
+
"description": task_result.description[:50] + "..."
|
|
162
|
+
if len(task_result.description) > 50
|
|
163
|
+
else task_result.description
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def format_step_result_xml(step_result: StepResult) -> str:
|
|
169
|
+
"""Format a step result with XML tags for better semantic understanding"""
|
|
170
|
+
from fast_agent.llm.prompt_utils import format_fastagent_tag
|
|
171
|
+
|
|
172
|
+
# Format each task result with XML
|
|
173
|
+
task_results = []
|
|
174
|
+
for task in step_result.task_results:
|
|
175
|
+
task_results.append(format_task_result_xml(task))
|
|
176
|
+
|
|
177
|
+
# Combine task results
|
|
178
|
+
task_results_str = "\n".join(task_results)
|
|
179
|
+
|
|
180
|
+
# Build step result with metadata and tasks
|
|
181
|
+
step_content = (
|
|
182
|
+
f"<fastagent:description>{step_result.step.description}</fastagent:description>\n"
|
|
183
|
+
f"<fastagent:summary>{step_result.result}</fastagent:summary>\n"
|
|
184
|
+
f"<fastagent:task-results>\n{task_results_str}\n</fastagent:task-results>\n"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return format_fastagent_tag("step-result", step_content)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def format_plan_result(plan_result: PlanResult) -> str:
|
|
191
|
+
"""Format the full plan execution state with XML for better semantic understanding"""
|
|
192
|
+
from fast_agent.llm.prompt_utils import format_fastagent_tag
|
|
193
|
+
|
|
194
|
+
# Format objective
|
|
195
|
+
objective_tag = format_fastagent_tag("objective", plan_result.objective)
|
|
196
|
+
|
|
197
|
+
# Format step results
|
|
198
|
+
step_results = []
|
|
199
|
+
for step in plan_result.step_results:
|
|
200
|
+
step_results.append(format_step_result_xml(step))
|
|
201
|
+
|
|
202
|
+
# Build progress section
|
|
203
|
+
if step_results:
|
|
204
|
+
steps_content = "\n".join(step_results)
|
|
205
|
+
progress_content = (
|
|
206
|
+
f"{objective_tag}\n"
|
|
207
|
+
f"<fastagent:steps>\n{steps_content}\n</fastagent:steps>\n"
|
|
208
|
+
f"<fastagent:status>{plan_result.result if plan_result.is_complete else 'In Progress'}</fastagent:status>\n"
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
# No steps executed yet
|
|
212
|
+
progress_content = (
|
|
213
|
+
f"{objective_tag}\n"
|
|
214
|
+
f"<fastagent:steps>No steps executed yet</fastagent:steps>\n"
|
|
215
|
+
f"<fastagent:status>Not Started</fastagent:status>\n"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return format_fastagent_tag("progress", progress_content)
|