fast-agent-mcp 0.1.13__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/METADATA +3 -4
- fast_agent_mcp-0.2.0.dist-info/RECORD +123 -0
- mcp_agent/__init__.py +75 -0
- mcp_agent/agents/agent.py +59 -371
- mcp_agent/agents/base_agent.py +522 -0
- mcp_agent/agents/workflow/__init__.py +1 -0
- mcp_agent/agents/workflow/chain_agent.py +173 -0
- mcp_agent/agents/workflow/evaluator_optimizer.py +362 -0
- mcp_agent/agents/workflow/orchestrator_agent.py +591 -0
- mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_models.py +27 -11
- mcp_agent/agents/workflow/parallel_agent.py +182 -0
- mcp_agent/agents/workflow/router_agent.py +307 -0
- mcp_agent/app.py +3 -1
- mcp_agent/cli/commands/bootstrap.py +18 -7
- mcp_agent/cli/commands/setup.py +12 -4
- mcp_agent/cli/main.py +1 -1
- mcp_agent/cli/terminal.py +1 -1
- mcp_agent/config.py +24 -35
- mcp_agent/context.py +3 -1
- mcp_agent/context_dependent.py +3 -1
- mcp_agent/core/agent_types.py +10 -7
- mcp_agent/core/direct_agent_app.py +179 -0
- mcp_agent/core/direct_decorators.py +443 -0
- mcp_agent/core/direct_factory.py +476 -0
- mcp_agent/core/enhanced_prompt.py +15 -20
- mcp_agent/core/fastagent.py +151 -337
- mcp_agent/core/interactive_prompt.py +424 -0
- mcp_agent/core/mcp_content.py +19 -11
- mcp_agent/core/prompt.py +6 -2
- mcp_agent/core/validation.py +89 -16
- mcp_agent/executor/decorator_registry.py +6 -2
- mcp_agent/executor/temporal.py +35 -11
- mcp_agent/executor/workflow_signal.py +8 -2
- mcp_agent/human_input/handler.py +3 -1
- mcp_agent/llm/__init__.py +2 -0
- mcp_agent/{workflows/llm → llm}/augmented_llm.py +131 -256
- mcp_agent/{workflows/llm → llm}/augmented_llm_passthrough.py +35 -107
- mcp_agent/llm/augmented_llm_playback.py +83 -0
- mcp_agent/{workflows/llm → llm}/model_factory.py +26 -8
- mcp_agent/llm/providers/__init__.py +8 -0
- mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +5 -1
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +37 -141
- mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +112 -148
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +78 -35
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +73 -44
- mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +18 -4
- mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +3 -3
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +3 -3
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +3 -3
- mcp_agent/{workflows/llm → llm}/sampling_converter.py +0 -21
- mcp_agent/{workflows/llm → llm}/sampling_format_converter.py +16 -1
- mcp_agent/logging/logger.py +2 -2
- mcp_agent/mcp/gen_client.py +9 -3
- mcp_agent/mcp/interfaces.py +67 -45
- mcp_agent/mcp/logger_textio.py +97 -0
- mcp_agent/mcp/mcp_agent_client_session.py +12 -4
- mcp_agent/mcp/mcp_agent_server.py +3 -1
- mcp_agent/mcp/mcp_aggregator.py +124 -93
- mcp_agent/mcp/mcp_connection_manager.py +21 -7
- mcp_agent/mcp/prompt_message_multipart.py +59 -1
- mcp_agent/mcp/prompt_render.py +77 -0
- mcp_agent/mcp/prompt_serialization.py +20 -13
- mcp_agent/mcp/prompts/prompt_constants.py +18 -0
- mcp_agent/mcp/prompts/prompt_helpers.py +327 -0
- mcp_agent/mcp/prompts/prompt_load.py +15 -5
- mcp_agent/mcp/prompts/prompt_server.py +154 -87
- mcp_agent/mcp/prompts/prompt_template.py +26 -35
- mcp_agent/mcp/resource_utils.py +3 -1
- mcp_agent/mcp/sampling.py +24 -15
- mcp_agent/mcp_server/agent_server.py +8 -5
- mcp_agent/mcp_server_registry.py +22 -9
- mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +1 -1
- mcp_agent/resources/examples/{data-analysis → in_dev}/slides.py +1 -1
- mcp_agent/resources/examples/internal/agent.py +4 -2
- mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
- mcp_agent/resources/examples/prompting/image_server.py +3 -1
- mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
- mcp_agent/ui/console_display.py +27 -7
- fast_agent_mcp-0.1.13.dist-info/RECORD +0 -164
- mcp_agent/core/agent_app.py +0 -570
- mcp_agent/core/agent_utils.py +0 -69
- mcp_agent/core/decorators.py +0 -448
- mcp_agent/core/factory.py +0 -422
- mcp_agent/core/proxies.py +0 -278
- mcp_agent/core/types.py +0 -22
- mcp_agent/eval/__init__.py +0 -0
- mcp_agent/mcp/stdio.py +0 -114
- mcp_agent/resources/examples/data-analysis/analysis-campaign.py +0 -188
- mcp_agent/resources/examples/data-analysis/analysis.py +0 -65
- mcp_agent/resources/examples/data-analysis/fastagent.config.yaml +0 -41
- mcp_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -1471
- mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +0 -53
- mcp_agent/resources/examples/researcher/fastagent.config.yaml +0 -66
- mcp_agent/resources/examples/researcher/researcher-eval.py +0 -53
- mcp_agent/resources/examples/researcher/researcher-imp.py +0 -189
- mcp_agent/resources/examples/researcher/researcher.py +0 -39
- mcp_agent/resources/examples/workflows/chaining.py +0 -45
- mcp_agent/resources/examples/workflows/evaluator.py +0 -79
- mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
- mcp_agent/resources/examples/workflows/human_input.py +0 -26
- mcp_agent/resources/examples/workflows/orchestrator.py +0 -74
- mcp_agent/resources/examples/workflows/parallel.py +0 -79
- mcp_agent/resources/examples/workflows/router.py +0 -54
- mcp_agent/resources/examples/workflows/sse.py +0 -23
- mcp_agent/telemetry/__init__.py +0 -0
- mcp_agent/telemetry/usage_tracking.py +0 -19
- mcp_agent/workflows/__init__.py +0 -0
- mcp_agent/workflows/embedding/__init__.py +0 -0
- mcp_agent/workflows/embedding/embedding_base.py +0 -58
- mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
- mcp_agent/workflows/embedding/embedding_openai.py +0 -37
- mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -447
- mcp_agent/workflows/intent_classifier/__init__.py +0 -0
- mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -117
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -130
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -41
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -41
- mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -150
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -58
- mcp_agent/workflows/llm/__init__.py +0 -0
- mcp_agent/workflows/llm/augmented_llm_playback.py +0 -111
- mcp_agent/workflows/llm/providers/__init__.py +0 -8
- mcp_agent/workflows/orchestrator/__init__.py +0 -0
- mcp_agent/workflows/orchestrator/orchestrator.py +0 -535
- mcp_agent/workflows/parallel/__init__.py +0 -0
- mcp_agent/workflows/parallel/fan_in.py +0 -320
- mcp_agent/workflows/parallel/fan_out.py +0 -181
- mcp_agent/workflows/parallel/parallel_llm.py +0 -149
- mcp_agent/workflows/router/__init__.py +0 -0
- mcp_agent/workflows/router/router_base.py +0 -338
- mcp_agent/workflows/router/router_embedding.py +0 -226
- mcp_agent/workflows/router/router_embedding_cohere.py +0 -59
- mcp_agent/workflows/router/router_embedding_openai.py +0 -59
- mcp_agent/workflows/router/router_llm.py +0 -304
- mcp_agent/workflows/swarm/__init__.py +0 -0
- mcp_agent/workflows/swarm/swarm.py +0 -292
- mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
- mcp_agent/workflows/swarm/swarm_openai.py +0 -41
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- /mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_prompts.py +0 -0
- /mcp_agent/{workflows/llm → llm}/memory.py +0 -0
- /mcp_agent/{workflows/llm → llm}/prompt_utils.py +0 -0
@@ -1,19 +1,19 @@
|
|
1
1
|
import json # Import at the module level
|
2
|
-
from typing import Any, List, Optional,
|
2
|
+
from typing import Any, List, Optional, Union
|
3
3
|
|
4
|
-
from mcp import GetPromptResult
|
5
4
|
from mcp.types import PromptMessage
|
6
|
-
from pydantic_core import from_json
|
7
5
|
|
8
|
-
from mcp_agent.
|
9
|
-
from mcp_agent.
|
10
|
-
from mcp_agent.workflows.llm.augmented_llm import (
|
6
|
+
from mcp_agent.core.prompt import Prompt
|
7
|
+
from mcp_agent.llm.augmented_llm import (
|
11
8
|
AugmentedLLM,
|
12
9
|
MessageParamT,
|
13
|
-
MessageT,
|
14
|
-
ModelT,
|
15
10
|
RequestParams,
|
16
11
|
)
|
12
|
+
from mcp_agent.logging.logger import get_logger
|
13
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
14
|
+
|
15
|
+
CALL_TOOL_INDICATOR = "***CALL_TOOL"
|
16
|
+
FIXED_RESPONSE_INDICATOR = "***FIXED_RESPONSE"
|
17
17
|
|
18
18
|
|
19
19
|
class PassthroughLLM(AugmentedLLM):
|
@@ -25,21 +25,12 @@ class PassthroughLLM(AugmentedLLM):
|
|
25
25
|
parallel workflow where no fan-in aggregation is needed.
|
26
26
|
"""
|
27
27
|
|
28
|
-
def __init__(self, name: str = "Passthrough",
|
29
|
-
super().__init__(name=name,
|
28
|
+
def __init__(self, name: str = "Passthrough", **kwargs: dict[str, Any]) -> None:
|
29
|
+
super().__init__(name=name, **kwargs)
|
30
30
|
self.provider = "fast-agent"
|
31
|
-
# Initialize logger - keep it simple without name reference
|
32
31
|
self.logger = get_logger(__name__)
|
33
32
|
self._messages = [PromptMessage]
|
34
|
-
|
35
|
-
async def generate(
|
36
|
-
self,
|
37
|
-
message: Union[str, MessageParamT, List[MessageParamT]],
|
38
|
-
request_params: Optional[RequestParams] = None,
|
39
|
-
) -> Union[List[MessageT], Any]:
|
40
|
-
"""Simply return the input message as is."""
|
41
|
-
# Return in the format expected by the caller
|
42
|
-
return [message] if isinstance(message, list) else message
|
33
|
+
self._fixed_response: str | None = None
|
43
34
|
|
44
35
|
async def generate_str(
|
45
36
|
self,
|
@@ -143,92 +134,29 @@ class PassthroughLLM(AugmentedLLM):
|
|
143
134
|
|
144
135
|
return "\n".join(result_text)
|
145
136
|
|
146
|
-
async def
|
147
|
-
self,
|
148
|
-
message: Union[str, MessageParamT, List[MessageParamT]],
|
149
|
-
response_model: Type[ModelT],
|
150
|
-
request_params: Optional[RequestParams] = None,
|
151
|
-
) -> ModelT:
|
152
|
-
"""
|
153
|
-
Return the input message as the requested model type.
|
154
|
-
This is a best-effort implementation - it may fail if the
|
155
|
-
message cannot be converted to the requested model.
|
156
|
-
"""
|
157
|
-
if isinstance(message, response_model):
|
158
|
-
return message
|
159
|
-
elif isinstance(message, dict):
|
160
|
-
return response_model(**message)
|
161
|
-
elif isinstance(message, str):
|
162
|
-
return response_model.model_validate(from_json(message, allow_partial=True))
|
163
|
-
|
164
|
-
async def generate_prompt(self, prompt: "PromptMessageMultipart", request_params: RequestParams | None) -> str:
|
165
|
-
# Check if this prompt contains a tool call command
|
166
|
-
if prompt.content and prompt.content[0].text and prompt.content[0].text.startswith("***CALL_TOOL "):
|
167
|
-
return await self._call_tool_and_return_result(prompt.content[0].text)
|
168
|
-
|
169
|
-
# Process all parts of the PromptMessageMultipart
|
170
|
-
parts_text = []
|
171
|
-
for part in prompt.content:
|
172
|
-
parts_text.append(str(part))
|
173
|
-
|
174
|
-
# If no parts found, return empty string
|
175
|
-
if not parts_text:
|
176
|
-
return ""
|
177
|
-
|
178
|
-
# Join all parts and process with generate_str
|
179
|
-
return await self.generate_str("\n".join(parts_text), request_params)
|
180
|
-
|
181
|
-
async def apply_prompt(
|
137
|
+
async def _apply_prompt_provider_specific(
|
182
138
|
self,
|
183
139
|
multipart_messages: List["PromptMessageMultipart"],
|
184
|
-
request_params:
|
185
|
-
) ->
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
For PassthroughLLM, this returns all content concatenated together.
|
208
|
-
|
209
|
-
Args:
|
210
|
-
prompt_result: The GetPromptResult containing prompt messages
|
211
|
-
prompt_name: The name of the prompt being applied
|
212
|
-
|
213
|
-
Returns:
|
214
|
-
String representation of all message content concatenated together
|
215
|
-
"""
|
216
|
-
prompt_messages: List[PromptMessage] = prompt_result.messages
|
217
|
-
|
218
|
-
# Extract arguments if they were stored in the result
|
219
|
-
arguments = getattr(prompt_result, "arguments", None)
|
220
|
-
|
221
|
-
# Display information about the loaded prompt
|
222
|
-
await self.show_prompt_loaded(
|
223
|
-
prompt_name=prompt_name,
|
224
|
-
description=prompt_result.description,
|
225
|
-
message_count=len(prompt_messages),
|
226
|
-
arguments=arguments,
|
227
|
-
)
|
228
|
-
self._messages = prompt_messages
|
229
|
-
|
230
|
-
# Convert prompt messages to multipart format
|
231
|
-
multipart_messages = PromptMessageMultipart.to_multipart(prompt_messages)
|
232
|
-
|
233
|
-
# Use apply_prompt to handle the multipart messages
|
234
|
-
return await self.apply_prompt(multipart_messages)
|
140
|
+
request_params: RequestParams | None = None,
|
141
|
+
) -> PromptMessageMultipart:
|
142
|
+
last_message = multipart_messages[-1]
|
143
|
+
|
144
|
+
# TODO -- improve when we support Audio/Multimodal gen
|
145
|
+
if self.is_tool_call(last_message):
|
146
|
+
return Prompt.assistant(await self.generate_str(last_message.first_text()))
|
147
|
+
|
148
|
+
if last_message.first_text().startswith(FIXED_RESPONSE_INDICATOR):
|
149
|
+
self._fixed_response = (
|
150
|
+
last_message.first_text().split(FIXED_RESPONSE_INDICATOR, 1)[1].strip()
|
151
|
+
)
|
152
|
+
|
153
|
+
if self._fixed_response:
|
154
|
+
await self.show_assistant_message(self._fixed_response)
|
155
|
+
return Prompt.assistant(self._fixed_response)
|
156
|
+
else:
|
157
|
+
concatenated: str = "\n".join(message.all_text() for message in multipart_messages)
|
158
|
+
await self.show_assistant_message(concatenated)
|
159
|
+
return Prompt.assistant(concatenated)
|
160
|
+
|
161
|
+
def is_tool_call(self, message: PromptMessageMultipart) -> bool:
|
162
|
+
return message.first_text().startswith(CALL_TOOL_INDICATOR)
|
@@ -0,0 +1,83 @@
|
|
1
|
+
from typing import Any, List
|
2
|
+
|
3
|
+
from mcp_agent.core.prompt import Prompt
|
4
|
+
from mcp_agent.llm.augmented_llm import RequestParams
|
5
|
+
from mcp_agent.llm.augmented_llm_passthrough import PassthroughLLM
|
6
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
7
|
+
from mcp_agent.mcp.prompts.prompt_helpers import MessageContent
|
8
|
+
|
9
|
+
|
10
|
+
class PlaybackLLM(PassthroughLLM):
|
11
|
+
"""
|
12
|
+
A specialized LLM implementation that plays back assistant messages when loaded with prompts.
|
13
|
+
|
14
|
+
Unlike the PassthroughLLM which simply passes through messages without modification,
|
15
|
+
PlaybackLLM is designed to simulate a conversation by playing back prompt messages
|
16
|
+
in sequence when loaded with prompts through apply_prompt_template.
|
17
|
+
|
18
|
+
After apply_prompts has been called, each call to generate_str returns the next
|
19
|
+
"ASSISTANT" message in the loaded messages. If no messages are set or all messages have
|
20
|
+
been played back, it returns a message indicating that messages are exhausted.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, name: str = "Playback", **kwargs: dict[str, Any]) -> None:
|
24
|
+
super().__init__(name=name, **kwargs)
|
25
|
+
self._messages: List[PromptMessageMultipart] = []
|
26
|
+
self._current_index = -1
|
27
|
+
self._overage = -1
|
28
|
+
|
29
|
+
def _get_next_assistant_message(self) -> PromptMessageMultipart:
|
30
|
+
"""
|
31
|
+
Get the next assistant message from the loaded messages.
|
32
|
+
Increments the current message index and skips user messages.
|
33
|
+
"""
|
34
|
+
# Find next assistant message
|
35
|
+
while self._current_index < len(self._messages):
|
36
|
+
message = self._messages[self._current_index]
|
37
|
+
self._current_index += 1
|
38
|
+
if "assistant" != message.role:
|
39
|
+
continue
|
40
|
+
|
41
|
+
return message
|
42
|
+
|
43
|
+
self._overage += 1
|
44
|
+
return Prompt.assistant(
|
45
|
+
f"MESSAGES EXHAUSTED (list size {len(self._messages)}) ({self._overage} overage)"
|
46
|
+
)
|
47
|
+
|
48
|
+
async def generate(
|
49
|
+
self,
|
50
|
+
multipart_messages: List[PromptMessageMultipart],
|
51
|
+
request_params: RequestParams | None = None,
|
52
|
+
) -> PromptMessageMultipart:
|
53
|
+
"""
|
54
|
+
Handle playback of messages in two modes:
|
55
|
+
1. First call: store messages for playback and return "HISTORY LOADED"
|
56
|
+
2. Subsequent calls: return the next assistant message
|
57
|
+
"""
|
58
|
+
# If this is the first call (initialization) or we're loading a prompt template
|
59
|
+
# with multiple messages (comes from apply_prompt)
|
60
|
+
if -1 == self._current_index:
|
61
|
+
if len(multipart_messages) > 1:
|
62
|
+
self._messages = multipart_messages
|
63
|
+
else:
|
64
|
+
self._messages.extend(multipart_messages)
|
65
|
+
|
66
|
+
# Reset the index to the beginning for proper playback
|
67
|
+
self._current_index = 0
|
68
|
+
|
69
|
+
await self.show_assistant_message(
|
70
|
+
message_text=f"HISTORY LOADED ({len(self._messages)} messages)",
|
71
|
+
title="ASSISTANT/PLAYBACK",
|
72
|
+
)
|
73
|
+
|
74
|
+
# In PlaybackLLM, we always return "HISTORY LOADED" on initialization,
|
75
|
+
# regardless of the prompt content. The next call will return messages.
|
76
|
+
return Prompt.assistant("HISTORY LOADED")
|
77
|
+
|
78
|
+
response = self._get_next_assistant_message()
|
79
|
+
await self.show_assistant_message(
|
80
|
+
message_text=MessageContent.get_first_text(response), title="ASSISTANT/PLAYBACK"
|
81
|
+
)
|
82
|
+
|
83
|
+
return response
|
@@ -5,11 +5,15 @@ from typing import Callable, Dict, Optional, Type, Union
|
|
5
5
|
from mcp_agent.agents.agent import Agent
|
6
6
|
from mcp_agent.core.exceptions import ModelConfigError
|
7
7
|
from mcp_agent.core.request_params import RequestParams
|
8
|
+
from mcp_agent.llm.augmented_llm_passthrough import PassthroughLLM
|
9
|
+
from mcp_agent.llm.augmented_llm_playback import PlaybackLLM
|
10
|
+
from mcp_agent.llm.providers.augmented_llm_anthropic import AnthropicAugmentedLLM
|
11
|
+
from mcp_agent.llm.providers.augmented_llm_deepseek import DeepSeekAugmentedLLM
|
12
|
+
from mcp_agent.llm.providers.augmented_llm_openai import OpenAIAugmentedLLM
|
8
13
|
from mcp_agent.mcp.interfaces import AugmentedLLMProtocol
|
9
|
-
|
10
|
-
from mcp_agent.workflows.llm.
|
11
|
-
|
12
|
-
from mcp_agent.workflows.llm.augmented_llm_playback import PlaybackLLM
|
14
|
+
|
15
|
+
# from mcp_agent.workflows.llm.augmented_llm_deepseek import DeekSeekAugmentedLLM
|
16
|
+
|
13
17
|
|
14
18
|
# Type alias for LLM classes
|
15
19
|
LLMClass = Union[
|
@@ -17,6 +21,7 @@ LLMClass = Union[
|
|
17
21
|
Type[OpenAIAugmentedLLM],
|
18
22
|
Type[PassthroughLLM],
|
19
23
|
Type[PlaybackLLM],
|
24
|
+
Type[DeepSeekAugmentedLLM],
|
20
25
|
]
|
21
26
|
|
22
27
|
|
@@ -26,6 +31,7 @@ class Provider(Enum):
|
|
26
31
|
ANTHROPIC = auto()
|
27
32
|
OPENAI = auto()
|
28
33
|
FAST_AGENT = auto()
|
34
|
+
DEEPSEEK = auto()
|
29
35
|
|
30
36
|
|
31
37
|
class ReasoningEffort(Enum):
|
@@ -53,6 +59,7 @@ class ModelFactory:
|
|
53
59
|
"anthropic": Provider.ANTHROPIC,
|
54
60
|
"openai": Provider.OPENAI,
|
55
61
|
"fast-agent": Provider.FAST_AGENT,
|
62
|
+
"deepseek": Provider.DEEPSEEK,
|
56
63
|
}
|
57
64
|
|
58
65
|
# Mapping of effort strings to enum values
|
@@ -85,6 +92,8 @@ class ModelFactory:
|
|
85
92
|
"claude-3-7-sonnet-latest": Provider.ANTHROPIC,
|
86
93
|
"claude-3-opus-20240229": Provider.ANTHROPIC,
|
87
94
|
"claude-3-opus-latest": Provider.ANTHROPIC,
|
95
|
+
"deepseek-chat": Provider.DEEPSEEK,
|
96
|
+
# "deepseek-reasoner": Provider.DEEPSEEK, reinstate on release
|
88
97
|
}
|
89
98
|
|
90
99
|
MODEL_ALIASES = {
|
@@ -97,6 +106,8 @@ class ModelFactory:
|
|
97
106
|
"haiku35": "claude-3-5-haiku-latest",
|
98
107
|
"opus": "claude-3-opus-latest",
|
99
108
|
"opus3": "claude-3-opus-latest",
|
109
|
+
"deepseekv3": "deepseek-chat",
|
110
|
+
"deepseek": "deepseek-chat",
|
100
111
|
}
|
101
112
|
|
102
113
|
# Mapping of providers to their LLM classes
|
@@ -104,6 +115,7 @@ class ModelFactory:
|
|
104
115
|
Provider.ANTHROPIC: AnthropicAugmentedLLM,
|
105
116
|
Provider.OPENAI: OpenAIAugmentedLLM,
|
106
117
|
Provider.FAST_AGENT: PassthroughLLM,
|
118
|
+
Provider.DEEPSEEK: DeepSeekAugmentedLLM,
|
107
119
|
}
|
108
120
|
|
109
121
|
# Mapping of special model names to their specific LLM classes
|
@@ -145,10 +157,14 @@ class ModelFactory:
|
|
145
157
|
if provider is None:
|
146
158
|
raise ModelConfigError(f"Unknown model: {model_name}")
|
147
159
|
|
148
|
-
return ModelConfig(
|
160
|
+
return ModelConfig(
|
161
|
+
provider=provider, model_name=model_name, reasoning_effort=reasoning_effort
|
162
|
+
)
|
149
163
|
|
150
164
|
@classmethod
|
151
|
-
def create_factory(
|
165
|
+
def create_factory(
|
166
|
+
cls, model_string: str, request_params: Optional[RequestParams] = None
|
167
|
+
) -> Callable[..., AugmentedLLMProtocol]:
|
152
168
|
"""
|
153
169
|
Creates a factory function that follows the attach_llm protocol.
|
154
170
|
|
@@ -177,7 +193,9 @@ class ModelFactory:
|
|
177
193
|
params_dict = factory_params.model_dump()
|
178
194
|
params_dict.update(kwargs["default_request_params"].model_dump(exclude_unset=True))
|
179
195
|
factory_params = RequestParams(**params_dict)
|
180
|
-
factory_params.model =
|
196
|
+
factory_params.model = (
|
197
|
+
config.model_name
|
198
|
+
) # Ensure parsed model name isn't overwritten
|
181
199
|
|
182
200
|
# Forward all keyword arguments to LLM constructor
|
183
201
|
llm_args = {
|
@@ -196,7 +214,7 @@ class ModelFactory:
|
|
196
214
|
if key not in ["agent", "default_request_params", "name"]:
|
197
215
|
llm_args[key] = value
|
198
216
|
|
199
|
-
llm = llm_class(**llm_args)
|
217
|
+
llm: AugmentedLLMProtocol = llm_class(**llm_args)
|
200
218
|
return llm
|
201
219
|
|
202
220
|
return factory
|
@@ -0,0 +1,8 @@
|
|
1
|
+
from mcp_agent.llm.providers.sampling_converter_anthropic import (
|
2
|
+
AnthropicSamplingConverter,
|
3
|
+
)
|
4
|
+
from mcp_agent.llm.providers.sampling_converter_openai import (
|
5
|
+
OpenAISamplingConverter,
|
6
|
+
)
|
7
|
+
|
8
|
+
__all__ = ["AnthropicSamplingConverter", "OpenAISamplingConverter"]
|
@@ -47,7 +47,11 @@ def anthropic_message_param_to_prompt_message_multipart(
|
|
47
47
|
text = block.get("text", "")
|
48
48
|
|
49
49
|
# Check if this is a resource marker
|
50
|
-
if
|
50
|
+
if (
|
51
|
+
text
|
52
|
+
and (text.startswith("[Resource:") or text.startswith("[Binary Resource:"))
|
53
|
+
and "\n" in text
|
54
|
+
):
|
51
55
|
header, content_text = text.split("\n", 1)
|
52
56
|
if "MIME:" in header:
|
53
57
|
mime_match = header.split("MIME:", 1)[1].split("]")[0].strip()
|
@@ -1,18 +1,18 @@
|
|
1
1
|
import os
|
2
2
|
from typing import TYPE_CHECKING, List, Type
|
3
3
|
|
4
|
-
from mcp_agent.
|
4
|
+
from mcp_agent.core.prompt import Prompt
|
5
|
+
from mcp_agent.llm.providers.multipart_converter_anthropic import (
|
5
6
|
AnthropicConverter,
|
6
7
|
)
|
7
|
-
from mcp_agent.
|
8
|
+
from mcp_agent.llm.providers.sampling_converter_anthropic import (
|
8
9
|
AnthropicSamplingConverter,
|
9
10
|
)
|
11
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
10
12
|
|
11
13
|
if TYPE_CHECKING:
|
12
14
|
from mcp import ListToolsResult
|
13
15
|
|
14
|
-
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
15
|
-
|
16
16
|
|
17
17
|
from anthropic import Anthropic, AuthenticationError
|
18
18
|
from anthropic.types import (
|
@@ -32,12 +32,12 @@ from pydantic_core import from_json
|
|
32
32
|
from rich.text import Text
|
33
33
|
|
34
34
|
from mcp_agent.core.exceptions import ProviderKeyError
|
35
|
-
from mcp_agent.
|
36
|
-
from mcp_agent.workflows.llm.augmented_llm import (
|
35
|
+
from mcp_agent.llm.augmented_llm import (
|
37
36
|
AugmentedLLM,
|
38
37
|
ModelT,
|
39
38
|
RequestParams,
|
40
39
|
)
|
40
|
+
from mcp_agent.logging.logger import get_logger
|
41
41
|
|
42
42
|
DEFAULT_ANTHROPIC_MODEL = "claude-3-7-sonnet-latest"
|
43
43
|
|
@@ -69,9 +69,12 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
69
69
|
use_history=True,
|
70
70
|
)
|
71
71
|
|
72
|
-
|
72
|
+
def _base_url(self) -> str:
|
73
|
+
return self.context.config.anthropic.base_url if self.context.config.anthropic else None
|
74
|
+
|
75
|
+
async def generate_internal(
|
73
76
|
self,
|
74
|
-
|
77
|
+
message_param,
|
75
78
|
request_params: RequestParams | None = None,
|
76
79
|
):
|
77
80
|
"""
|
@@ -80,8 +83,12 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
80
83
|
"""
|
81
84
|
|
82
85
|
api_key = self._api_key(self.context.config)
|
86
|
+
base_url = self._base_url()
|
87
|
+
if base_url and base_url.endswith("/v1"):
|
88
|
+
base_url = base_url.rstrip("/v1")
|
89
|
+
|
83
90
|
try:
|
84
|
-
anthropic = Anthropic(api_key=api_key)
|
91
|
+
anthropic = Anthropic(api_key=api_key, base_url=base_url)
|
85
92
|
messages: List[MessageParam] = []
|
86
93
|
params = self.get_request_params(request_params)
|
87
94
|
except AuthenticationError as e:
|
@@ -94,12 +101,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
94
101
|
# if use_history is True
|
95
102
|
messages.extend(self.history.get(include_history=params.use_history))
|
96
103
|
|
97
|
-
|
98
|
-
messages.append({"role": "user", "content": message})
|
99
|
-
elif isinstance(message, list):
|
100
|
-
messages.extend(message)
|
101
|
-
else:
|
102
|
-
messages.append(message)
|
104
|
+
messages.append(message_param)
|
103
105
|
|
104
106
|
tool_list: ListToolsResult = await self.aggregator.list_tools()
|
105
107
|
available_tools: List[ToolParam] = [
|
@@ -112,13 +114,11 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
112
114
|
]
|
113
115
|
|
114
116
|
responses: List[Message] = []
|
115
|
-
|
116
|
-
|
117
|
-
self.show_user_message(str(message), model, chat_turn)
|
117
|
+
|
118
|
+
model = self.default_request_params.model
|
118
119
|
|
119
120
|
for i in range(params.max_iterations):
|
120
|
-
chat_turn
|
121
|
-
self._log_chat_progress(chat_turn, model=model)
|
121
|
+
self._log_chat_progress(self.chat_turn(), model=model)
|
122
122
|
arguments = {
|
123
123
|
"model": model,
|
124
124
|
"messages": messages,
|
@@ -191,7 +191,9 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
191
191
|
break
|
192
192
|
elif response.stop_reason == "stop_sequence":
|
193
193
|
# We have reached a stop sequence
|
194
|
-
self.logger.debug(
|
194
|
+
self.logger.debug(
|
195
|
+
f"Iteration {i}: Stopping because finish_reason is 'stop_sequence'"
|
196
|
+
)
|
195
197
|
break
|
196
198
|
elif response.stop_reason == "max_tokens":
|
197
199
|
# We have reached the max tokens limit
|
@@ -246,7 +248,9 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
246
248
|
params=CallToolRequestParams(name=tool_name, arguments=tool_args),
|
247
249
|
)
|
248
250
|
# TODO -- support MCP isError etc.
|
249
|
-
result = await self.call_tool(
|
251
|
+
result = await self.call_tool(
|
252
|
+
request=tool_call_request, tool_call_id=tool_use_id
|
253
|
+
)
|
250
254
|
self.show_tool_result(result)
|
251
255
|
|
252
256
|
# Add each result to our collection
|
@@ -293,7 +297,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
293
297
|
|
294
298
|
async def generate_str(
|
295
299
|
self,
|
296
|
-
|
300
|
+
message_param,
|
297
301
|
request_params: RequestParams | None = None,
|
298
302
|
) -> str:
|
299
303
|
"""
|
@@ -301,16 +305,10 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
301
305
|
The default implementation uses Claude as the LLM.
|
302
306
|
Override this method to use a different LLM.
|
303
307
|
|
304
|
-
Special commands:
|
305
|
-
- "***SAVE_HISTORY <filename.md>" - Saves the conversation history to the specified file
|
306
|
-
in MCP prompt format with user/assistant delimiters.
|
307
308
|
"""
|
308
|
-
# Check if this is a special command to save history
|
309
|
-
if isinstance(message, str) and message.startswith("***SAVE_HISTORY "):
|
310
|
-
return await self._save_history_to_file(message)
|
311
309
|
|
312
|
-
responses: List[Message] = await self.
|
313
|
-
|
310
|
+
responses: List[Message] = await self.generate_internal(
|
311
|
+
message_param=message_param,
|
314
312
|
request_params=request_params,
|
315
313
|
)
|
316
314
|
|
@@ -333,96 +331,32 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
333
331
|
# Join all collected text
|
334
332
|
return "\n".join(final_text)
|
335
333
|
|
336
|
-
async def
|
337
|
-
return await self.generate_str(AnthropicConverter.convert_to_anthropic(prompt), request_params)
|
338
|
-
|
339
|
-
async def _apply_prompt_template_provider_specific(
|
334
|
+
async def _apply_prompt_provider_specific(
|
340
335
|
self,
|
341
336
|
multipart_messages: List["PromptMessageMultipart"],
|
342
337
|
request_params: RequestParams | None = None,
|
343
|
-
) ->
|
344
|
-
"""
|
345
|
-
Anthropic-specific implementation of apply_prompt_template that handles
|
346
|
-
multimodal content natively.
|
347
|
-
|
348
|
-
Args:
|
349
|
-
multipart_messages: List of PromptMessageMultipart objects parsed from the prompt template
|
350
|
-
|
351
|
-
Returns:
|
352
|
-
String representation of the assistant's response if generated,
|
353
|
-
or the last assistant message in the prompt
|
354
|
-
"""
|
338
|
+
) -> PromptMessageMultipart:
|
355
339
|
# Check the last message role
|
356
340
|
last_message = multipart_messages[-1]
|
357
341
|
|
358
342
|
# Add all previous messages to history (or all messages if last is from assistant)
|
359
|
-
messages_to_add =
|
343
|
+
messages_to_add = (
|
344
|
+
multipart_messages[:-1] if last_message.role == "user" else multipart_messages
|
345
|
+
)
|
360
346
|
converted = []
|
361
347
|
for msg in messages_to_add:
|
362
348
|
converted.append(AnthropicConverter.convert_to_anthropic(msg))
|
349
|
+
|
363
350
|
self.history.extend(converted, is_prompt=True)
|
364
351
|
|
365
352
|
if last_message.role == "user":
|
366
|
-
# For user messages: Generate response to the last one
|
367
353
|
self.logger.debug("Last message in prompt is from user, generating assistant response")
|
368
354
|
message_param = AnthropicConverter.convert_to_anthropic(last_message)
|
369
|
-
return await self.generate_str(message_param, request_params)
|
355
|
+
return Prompt.assistant(await self.generate_str(message_param, request_params))
|
370
356
|
else:
|
371
357
|
# For assistant messages: Return the last message content as text
|
372
358
|
self.logger.debug("Last message in prompt is from assistant, returning it directly")
|
373
|
-
return
|
374
|
-
|
375
|
-
async def _save_history_to_file(self, command: str) -> str:
|
376
|
-
"""
|
377
|
-
Save the conversation history to a file in MCP prompt format.
|
378
|
-
|
379
|
-
Args:
|
380
|
-
command: The command string, expected format: "***SAVE_HISTORY <filename.md>"
|
381
|
-
|
382
|
-
Returns:
|
383
|
-
Success or error message
|
384
|
-
"""
|
385
|
-
try:
|
386
|
-
# Extract the filename from the command
|
387
|
-
parts = command.split(" ", 1)
|
388
|
-
if len(parts) != 2 or not parts[1].strip():
|
389
|
-
return "Error: Invalid format. Expected '***SAVE_HISTORY <filename.md>'"
|
390
|
-
|
391
|
-
filename = parts[1].strip()
|
392
|
-
|
393
|
-
# Get all messages from history
|
394
|
-
messages = self.history.get(include_history=True)
|
395
|
-
|
396
|
-
# Import required utilities
|
397
|
-
from mcp_agent.mcp.prompt_serialization import (
|
398
|
-
multipart_messages_to_delimited_format,
|
399
|
-
)
|
400
|
-
from mcp_agent.workflows.llm.anthropic_utils import (
|
401
|
-
anthropic_message_param_to_prompt_message_multipart,
|
402
|
-
)
|
403
|
-
|
404
|
-
# Convert message params to PromptMessageMultipart objects
|
405
|
-
multipart_messages = []
|
406
|
-
for msg in messages:
|
407
|
-
multipart_messages.append(anthropic_message_param_to_prompt_message_multipart(msg))
|
408
|
-
|
409
|
-
# Convert to delimited format
|
410
|
-
delimited_content = multipart_messages_to_delimited_format(
|
411
|
-
multipart_messages,
|
412
|
-
user_delimiter="---USER",
|
413
|
-
assistant_delimiter="---ASSISTANT",
|
414
|
-
)
|
415
|
-
|
416
|
-
# Write to file
|
417
|
-
with open(filename, "w", encoding="utf-8") as f:
|
418
|
-
f.write("\n\n".join(delimited_content))
|
419
|
-
|
420
|
-
self.logger.info(f"Saved conversation history to {filename}")
|
421
|
-
return f"Done. Saved conversation history to {filename}"
|
422
|
-
|
423
|
-
except Exception as e:
|
424
|
-
self.logger.error(f"Error saving history: {str(e)}")
|
425
|
-
return f"Error saving history: {str(e)}"
|
359
|
+
return last_message
|
426
360
|
|
427
361
|
async def generate_structured(
|
428
362
|
self,
|
@@ -461,41 +395,3 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
461
395
|
)
|
462
396
|
|
463
397
|
return MessageParam(role="assistant", content=content, **kwargs)
|
464
|
-
|
465
|
-
def message_param_str(self, message: MessageParam) -> str:
|
466
|
-
"""Convert an input message to a string representation."""
|
467
|
-
|
468
|
-
if message.get("content"):
|
469
|
-
content = message["content"]
|
470
|
-
if isinstance(content, str):
|
471
|
-
return content
|
472
|
-
else:
|
473
|
-
final_text: List[str] = []
|
474
|
-
for block in content:
|
475
|
-
if block.text:
|
476
|
-
final_text.append(str(block.text))
|
477
|
-
else:
|
478
|
-
final_text.append(str(block))
|
479
|
-
|
480
|
-
return "\n".join(final_text)
|
481
|
-
|
482
|
-
return str(message)
|
483
|
-
|
484
|
-
def message_str(self, message: Message) -> str:
|
485
|
-
"""Convert an output message to a string representation."""
|
486
|
-
content = message.content
|
487
|
-
|
488
|
-
if content:
|
489
|
-
if isinstance(content, list):
|
490
|
-
final_text: List[str] = []
|
491
|
-
for block in content:
|
492
|
-
if block.text:
|
493
|
-
final_text.append(str(block.text))
|
494
|
-
else:
|
495
|
-
final_text.append(str(block))
|
496
|
-
|
497
|
-
return "\n".join(final_text)
|
498
|
-
else:
|
499
|
-
return str(content)
|
500
|
-
|
501
|
-
return str(message)
|