fast-agent-mcp 0.1.12__py3-none-any.whl → 0.1.13__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.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/METADATA +1 -1
- fast_agent_mcp-0.1.13.dist-info/RECORD +164 -0
- mcp_agent/agents/agent.py +37 -79
- mcp_agent/app.py +16 -22
- mcp_agent/cli/commands/bootstrap.py +22 -52
- mcp_agent/cli/commands/config.py +4 -4
- mcp_agent/cli/commands/setup.py +11 -26
- mcp_agent/cli/main.py +6 -9
- mcp_agent/cli/terminal.py +2 -2
- mcp_agent/config.py +1 -5
- mcp_agent/context.py +13 -24
- mcp_agent/context_dependent.py +3 -7
- mcp_agent/core/agent_app.py +45 -121
- mcp_agent/core/agent_utils.py +3 -5
- mcp_agent/core/decorators.py +5 -12
- mcp_agent/core/enhanced_prompt.py +25 -52
- mcp_agent/core/exceptions.py +8 -8
- mcp_agent/core/factory.py +29 -70
- mcp_agent/core/fastagent.py +48 -88
- mcp_agent/core/mcp_content.py +8 -16
- mcp_agent/core/prompt.py +8 -15
- mcp_agent/core/proxies.py +34 -25
- mcp_agent/core/request_params.py +6 -3
- mcp_agent/core/types.py +4 -6
- mcp_agent/core/validation.py +4 -3
- mcp_agent/executor/decorator_registry.py +11 -23
- mcp_agent/executor/executor.py +8 -17
- mcp_agent/executor/task_registry.py +2 -4
- mcp_agent/executor/temporal.py +28 -74
- mcp_agent/executor/workflow.py +3 -5
- mcp_agent/executor/workflow_signal.py +17 -29
- mcp_agent/human_input/handler.py +4 -9
- mcp_agent/human_input/types.py +2 -3
- mcp_agent/logging/events.py +1 -5
- mcp_agent/logging/json_serializer.py +7 -6
- mcp_agent/logging/listeners.py +20 -23
- mcp_agent/logging/logger.py +15 -17
- mcp_agent/logging/rich_progress.py +10 -8
- mcp_agent/logging/tracing.py +4 -6
- mcp_agent/logging/transport.py +22 -22
- mcp_agent/mcp/gen_client.py +4 -12
- mcp_agent/mcp/interfaces.py +71 -86
- mcp_agent/mcp/mcp_agent_client_session.py +11 -19
- mcp_agent/mcp/mcp_agent_server.py +8 -10
- mcp_agent/mcp/mcp_aggregator.py +45 -117
- mcp_agent/mcp/mcp_connection_manager.py +16 -37
- mcp_agent/mcp/prompt_message_multipart.py +12 -18
- mcp_agent/mcp/prompt_serialization.py +13 -38
- mcp_agent/mcp/prompts/prompt_load.py +99 -0
- mcp_agent/mcp/prompts/prompt_server.py +21 -128
- mcp_agent/mcp/prompts/prompt_template.py +20 -42
- mcp_agent/mcp/resource_utils.py +8 -17
- mcp_agent/mcp/sampling.py +5 -14
- mcp_agent/mcp/stdio.py +11 -8
- mcp_agent/mcp_server/agent_server.py +10 -17
- mcp_agent/mcp_server_registry.py +13 -35
- mcp_agent/resources/examples/data-analysis/analysis-campaign.py +1 -1
- mcp_agent/resources/examples/data-analysis/analysis.py +1 -1
- mcp_agent/resources/examples/data-analysis/slides.py +110 -0
- mcp_agent/resources/examples/internal/agent.py +2 -1
- mcp_agent/resources/examples/internal/job.py +2 -1
- mcp_agent/resources/examples/internal/prompt_category.py +1 -1
- mcp_agent/resources/examples/internal/prompt_sizing.py +3 -5
- mcp_agent/resources/examples/internal/sizer.py +2 -1
- mcp_agent/resources/examples/internal/social.py +2 -1
- mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +1 -1
- mcp_agent/resources/examples/prompting/agent.py +2 -1
- mcp_agent/resources/examples/prompting/image_server.py +5 -11
- mcp_agent/resources/examples/researcher/researcher-eval.py +1 -1
- mcp_agent/resources/examples/researcher/researcher-imp.py +3 -4
- mcp_agent/resources/examples/researcher/researcher.py +2 -1
- mcp_agent/resources/examples/workflows/agent_build.py +2 -1
- mcp_agent/resources/examples/workflows/chaining.py +2 -1
- mcp_agent/resources/examples/workflows/evaluator.py +2 -1
- mcp_agent/resources/examples/workflows/human_input.py +2 -1
- mcp_agent/resources/examples/workflows/orchestrator.py +2 -1
- mcp_agent/resources/examples/workflows/parallel.py +2 -1
- mcp_agent/resources/examples/workflows/router.py +2 -1
- mcp_agent/resources/examples/workflows/sse.py +1 -1
- mcp_agent/telemetry/usage_tracking.py +2 -1
- mcp_agent/ui/console_display.py +15 -39
- mcp_agent/workflows/embedding/embedding_base.py +1 -4
- mcp_agent/workflows/embedding/embedding_cohere.py +2 -2
- mcp_agent/workflows/embedding/embedding_openai.py +4 -13
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +23 -57
- mcp_agent/workflows/intent_classifier/intent_classifier_base.py +5 -8
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +7 -11
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +4 -8
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +4 -8
- mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +11 -22
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +3 -3
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +4 -6
- mcp_agent/workflows/llm/anthropic_utils.py +8 -29
- mcp_agent/workflows/llm/augmented_llm.py +69 -247
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +39 -73
- mcp_agent/workflows/llm/augmented_llm_openai.py +42 -97
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +13 -20
- mcp_agent/workflows/llm/augmented_llm_playback.py +8 -6
- mcp_agent/workflows/llm/memory.py +103 -0
- mcp_agent/workflows/llm/model_factory.py +8 -20
- mcp_agent/workflows/llm/openai_utils.py +1 -1
- mcp_agent/workflows/llm/prompt_utils.py +1 -3
- mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +47 -89
- mcp_agent/workflows/llm/providers/multipart_converter_openai.py +20 -55
- mcp_agent/workflows/llm/providers/openai_multipart.py +19 -61
- mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +10 -12
- mcp_agent/workflows/llm/providers/sampling_converter_openai.py +7 -11
- mcp_agent/workflows/llm/sampling_converter.py +4 -11
- mcp_agent/workflows/llm/sampling_format_converter.py +12 -12
- mcp_agent/workflows/orchestrator/orchestrator.py +24 -67
- mcp_agent/workflows/orchestrator/orchestrator_models.py +14 -40
- mcp_agent/workflows/parallel/fan_in.py +17 -47
- mcp_agent/workflows/parallel/fan_out.py +6 -12
- mcp_agent/workflows/parallel/parallel_llm.py +9 -26
- mcp_agent/workflows/router/router_base.py +19 -49
- mcp_agent/workflows/router/router_embedding.py +11 -25
- mcp_agent/workflows/router/router_embedding_cohere.py +2 -2
- mcp_agent/workflows/router/router_embedding_openai.py +2 -2
- mcp_agent/workflows/router/router_llm.py +12 -28
- mcp_agent/workflows/swarm/swarm.py +20 -48
- mcp_agent/workflows/swarm/swarm_anthropic.py +2 -2
- mcp_agent/workflows/swarm/swarm_openai.py +2 -2
- fast_agent_mcp-0.1.12.dist-info/RECORD +0 -161
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,13 @@
|
|
1
|
-
from typing import List, Optional, Union
|
1
|
+
from typing import TYPE_CHECKING, List, Optional, Union
|
2
|
+
|
2
3
|
from mcp import GetPromptResult
|
3
|
-
|
4
|
+
|
4
5
|
from mcp_agent.workflows.llm.augmented_llm import MessageParamT, RequestParams
|
5
6
|
from mcp_agent.workflows.llm.augmented_llm_passthrough import PassthroughLLM
|
6
7
|
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from mcp.types import PromptMessage
|
10
|
+
|
7
11
|
|
8
12
|
# TODO -- support tool calling
|
9
13
|
class PlaybackLLM(PassthroughLLM):
|
@@ -19,7 +23,7 @@ class PlaybackLLM(PassthroughLLM):
|
|
19
23
|
been played back, it returns a message indicating that messages are exhausted.
|
20
24
|
"""
|
21
25
|
|
22
|
-
def __init__(self, name: str = "Playback", **kwargs):
|
26
|
+
def __init__(self, name: str = "Playback", **kwargs) -> None:
|
23
27
|
super().__init__(name=name, **kwargs)
|
24
28
|
self._messages: List[PromptMessage] = []
|
25
29
|
self._current_index = 0
|
@@ -70,9 +74,7 @@ class PlaybackLLM(PassthroughLLM):
|
|
70
74
|
# If we get here, we've run out of assistant messages
|
71
75
|
return f"MESSAGES EXHAUSTED (list size {len(self._messages)})"
|
72
76
|
|
73
|
-
async def apply_prompt_template(
|
74
|
-
self, prompt_result: GetPromptResult, prompt_name: str
|
75
|
-
) -> str:
|
77
|
+
async def apply_prompt_template(self, prompt_result: GetPromptResult, prompt_name: str) -> str:
|
76
78
|
"""
|
77
79
|
Apply a prompt template by adding its messages to the playback queue.
|
78
80
|
|
@@ -0,0 +1,103 @@
|
|
1
|
+
from typing import Generic, List, Protocol, TypeVar
|
2
|
+
|
3
|
+
# Define our own type variable for implementation use
|
4
|
+
MessageParamT = TypeVar("MessageParamT")
|
5
|
+
|
6
|
+
|
7
|
+
class Memory(Protocol, Generic[MessageParamT]):
|
8
|
+
"""
|
9
|
+
Simple memory management for storing past interactions in-memory.
|
10
|
+
"""
|
11
|
+
|
12
|
+
# TODO: saqadri - add checkpointing and other advanced memory capabilities
|
13
|
+
|
14
|
+
def __init__(self) -> None: ...
|
15
|
+
|
16
|
+
def extend(self, messages: List[MessageParamT], is_prompt: bool = False) -> None: ...
|
17
|
+
|
18
|
+
def set(self, messages: List[MessageParamT], is_prompt: bool = False) -> None: ...
|
19
|
+
|
20
|
+
def append(self, message: MessageParamT, is_prompt: bool = False) -> None: ...
|
21
|
+
|
22
|
+
def get(self, include_history: bool = True) -> List[MessageParamT]: ...
|
23
|
+
|
24
|
+
def clear(self, clear_prompts: bool = False) -> None: ...
|
25
|
+
|
26
|
+
|
27
|
+
class SimpleMemory(Memory, Generic[MessageParamT]):
|
28
|
+
"""
|
29
|
+
Simple memory management for storing past interactions in-memory.
|
30
|
+
|
31
|
+
Maintains both prompt messages (which are always included) and
|
32
|
+
generated conversation history (which is included based on use_history setting).
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self) -> None:
|
36
|
+
self.history: List[MessageParamT] = []
|
37
|
+
self.prompt_messages: List[MessageParamT] = [] # Always included
|
38
|
+
|
39
|
+
def extend(self, messages: List[MessageParamT], is_prompt: bool = False) -> None:
|
40
|
+
"""
|
41
|
+
Add multiple messages to history.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
messages: Messages to add
|
45
|
+
is_prompt: If True, add to prompt_messages instead of regular history
|
46
|
+
"""
|
47
|
+
if is_prompt:
|
48
|
+
self.prompt_messages.extend(messages)
|
49
|
+
else:
|
50
|
+
self.history.extend(messages)
|
51
|
+
|
52
|
+
def set(self, messages: List[MessageParamT], is_prompt: bool = False) -> None:
|
53
|
+
"""
|
54
|
+
Replace messages in history.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
messages: Messages to set
|
58
|
+
is_prompt: If True, replace prompt_messages instead of regular history
|
59
|
+
"""
|
60
|
+
if is_prompt:
|
61
|
+
self.prompt_messages = messages.copy()
|
62
|
+
else:
|
63
|
+
self.history = messages.copy()
|
64
|
+
|
65
|
+
def append(self, message: MessageParamT, is_prompt: bool = False) -> None:
|
66
|
+
"""
|
67
|
+
Add a single message to history.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
message: Message to add
|
71
|
+
is_prompt: If True, add to prompt_messages instead of regular history
|
72
|
+
"""
|
73
|
+
if is_prompt:
|
74
|
+
self.prompt_messages.append(message)
|
75
|
+
else:
|
76
|
+
self.history.append(message)
|
77
|
+
|
78
|
+
def get(self, include_history: bool = True) -> List[MessageParamT]:
|
79
|
+
"""
|
80
|
+
Get all messages in memory.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
include_history: If True, include regular history messages
|
84
|
+
If False, only return prompt messages
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
Combined list of prompt messages and optionally history messages
|
88
|
+
"""
|
89
|
+
if include_history:
|
90
|
+
return self.prompt_messages + self.history
|
91
|
+
else:
|
92
|
+
return self.prompt_messages.copy()
|
93
|
+
|
94
|
+
def clear(self, clear_prompts: bool = False) -> None:
|
95
|
+
"""
|
96
|
+
Clear history and optionally prompt messages.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
clear_prompts: If True, also clear prompt messages
|
100
|
+
"""
|
101
|
+
self.history = []
|
102
|
+
if clear_prompts:
|
103
|
+
self.prompt_messages = []
|
@@ -1,16 +1,16 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
2
|
from enum import Enum, auto
|
3
|
-
from typing import
|
3
|
+
from typing import Callable, Dict, Optional, Type, Union
|
4
4
|
|
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.mcp.interfaces import AugmentedLLMProtocol
|
8
9
|
from mcp_agent.workflows.llm.augmented_llm_anthropic import AnthropicAugmentedLLM
|
9
10
|
from mcp_agent.workflows.llm.augmented_llm_openai import OpenAIAugmentedLLM
|
10
11
|
from mcp_agent.workflows.llm.augmented_llm_passthrough import PassthroughLLM
|
11
12
|
from mcp_agent.workflows.llm.augmented_llm_playback import PlaybackLLM
|
12
13
|
|
13
|
-
|
14
14
|
# Type alias for LLM classes
|
15
15
|
LLMClass = Union[
|
16
16
|
Type[AnthropicAugmentedLLM],
|
@@ -145,14 +145,10 @@ class ModelFactory:
|
|
145
145
|
if provider is None:
|
146
146
|
raise ModelConfigError(f"Unknown model: {model_name}")
|
147
147
|
|
148
|
-
return ModelConfig(
|
149
|
-
provider=provider, model_name=model_name, reasoning_effort=reasoning_effort
|
150
|
-
)
|
148
|
+
return ModelConfig(provider=provider, model_name=model_name, reasoning_effort=reasoning_effort)
|
151
149
|
|
152
150
|
@classmethod
|
153
|
-
def create_factory(
|
154
|
-
cls, model_string: str, request_params: Optional[RequestParams] = None
|
155
|
-
) -> Callable[..., LLMClass]:
|
151
|
+
def create_factory(cls, model_string: str, request_params: Optional[RequestParams] = None) -> Callable[..., AugmentedLLMProtocol]:
|
156
152
|
"""
|
157
153
|
Creates a factory function that follows the attach_llm protocol.
|
158
154
|
|
@@ -173,23 +169,15 @@ class ModelFactory:
|
|
173
169
|
# Create a factory function matching the attach_llm protocol
|
174
170
|
def factory(agent: Agent, **kwargs) -> LLMClass:
|
175
171
|
# Create merged params with parsed model name
|
176
|
-
factory_params = (
|
177
|
-
|
178
|
-
)
|
179
|
-
factory_params.model = (
|
180
|
-
config.model_name
|
181
|
-
) # Use the parsed model name, not the alias
|
172
|
+
factory_params = request_params.model_copy() if request_params else RequestParams()
|
173
|
+
factory_params.model = config.model_name # Use the parsed model name, not the alias
|
182
174
|
|
183
175
|
# Merge with any provided default_request_params
|
184
176
|
if "default_request_params" in kwargs and kwargs["default_request_params"]:
|
185
177
|
params_dict = factory_params.model_dump()
|
186
|
-
params_dict.update(
|
187
|
-
kwargs["default_request_params"].model_dump(exclude_unset=True)
|
188
|
-
)
|
178
|
+
params_dict.update(kwargs["default_request_params"].model_dump(exclude_unset=True))
|
189
179
|
factory_params = RequestParams(**params_dict)
|
190
|
-
factory_params.model =
|
191
|
-
config.model_name
|
192
|
-
) # Ensure parsed model name isn't overwritten
|
180
|
+
factory_params.model = config.model_name # Ensure parsed model name isn't overwritten
|
193
181
|
|
194
182
|
# Forward all keyword arguments to LLM constructor
|
195
183
|
llm_args = {
|
@@ -5,7 +5,7 @@ This file provides backward compatibility with the existing API while
|
|
5
5
|
delegating to the proper implementations in the providers/ directory.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from typing import
|
8
|
+
from typing import Any, Dict, Union
|
9
9
|
|
10
10
|
from openai.types.chat import (
|
11
11
|
ChatCompletionMessage,
|
@@ -97,9 +97,7 @@ def format_server_info(
|
|
97
97
|
|
98
98
|
# Combine all components
|
99
99
|
server_content = "\n".join(components)
|
100
|
-
return format_fastagent_tag(
|
101
|
-
"server", f"\n{server_content}\n", {"name": server_name}
|
102
|
-
)
|
100
|
+
return format_fastagent_tag("server", f"\n{server_content}\n", {"name": server_name})
|
103
101
|
|
104
102
|
|
105
103
|
def format_agent_info(
|
@@ -1,38 +1,40 @@
|
|
1
|
-
from typing import List,
|
1
|
+
from typing import TYPE_CHECKING, List, Optional, Sequence, Union
|
2
2
|
|
3
|
+
from anthropic.types import (
|
4
|
+
Base64ImageSourceParam,
|
5
|
+
Base64PDFSourceParam,
|
6
|
+
ContentBlockParam,
|
7
|
+
DocumentBlockParam,
|
8
|
+
ImageBlockParam,
|
9
|
+
MessageParam,
|
10
|
+
PlainTextSourceParam,
|
11
|
+
TextBlockParam,
|
12
|
+
ToolResultBlockParam,
|
13
|
+
URLImageSourceParam,
|
14
|
+
URLPDFSourceParam,
|
15
|
+
)
|
3
16
|
from mcp.types import (
|
4
|
-
TextContent,
|
5
|
-
ImageContent,
|
6
|
-
EmbeddedResource,
|
7
|
-
CallToolResult,
|
8
|
-
TextResourceContents,
|
9
17
|
BlobResourceContents,
|
18
|
+
CallToolResult,
|
19
|
+
EmbeddedResource,
|
20
|
+
ImageContent,
|
10
21
|
PromptMessage,
|
22
|
+
TextContent,
|
23
|
+
TextResourceContents,
|
11
24
|
)
|
12
|
-
|
13
|
-
from mcp_agent.
|
25
|
+
|
26
|
+
from mcp_agent.logging.logger import get_logger
|
14
27
|
from mcp_agent.mcp.mime_utils import (
|
15
28
|
guess_mime_type,
|
16
|
-
is_text_mime_type,
|
17
29
|
is_image_mime_type,
|
30
|
+
is_text_mime_type,
|
18
31
|
)
|
19
|
-
|
20
|
-
from anthropic.types import (
|
21
|
-
MessageParam,
|
22
|
-
TextBlockParam,
|
23
|
-
ImageBlockParam,
|
24
|
-
DocumentBlockParam,
|
25
|
-
Base64ImageSourceParam,
|
26
|
-
URLImageSourceParam,
|
27
|
-
Base64PDFSourceParam,
|
28
|
-
URLPDFSourceParam,
|
29
|
-
PlainTextSourceParam,
|
30
|
-
ToolResultBlockParam,
|
31
|
-
ContentBlockParam,
|
32
|
-
)
|
33
|
-
from mcp_agent.logging.logger import get_logger
|
32
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
34
33
|
from mcp_agent.mcp.resource_utils import extract_title_from_uri
|
35
34
|
|
35
|
+
if TYPE_CHECKING:
|
36
|
+
from pydantic import AnyUrl
|
37
|
+
|
36
38
|
_logger = get_logger("multipart_converter_anthropic")
|
37
39
|
|
38
40
|
# List of image MIME types supported by Anthropic API
|
@@ -72,9 +74,7 @@ class AnthropicConverter:
|
|
72
74
|
return MessageParam(role=role, content=[])
|
73
75
|
|
74
76
|
# Convert content blocks
|
75
|
-
anthropic_blocks = AnthropicConverter._convert_content_items(
|
76
|
-
multipart_msg.content, document_mode=True
|
77
|
-
)
|
77
|
+
anthropic_blocks = AnthropicConverter._convert_content_items(multipart_msg.content, document_mode=True)
|
78
78
|
|
79
79
|
# Filter blocks based on role (assistant can only have text blocks)
|
80
80
|
if role == "assistant":
|
@@ -83,9 +83,7 @@ class AnthropicConverter:
|
|
83
83
|
if block.get("type") == "text":
|
84
84
|
text_blocks.append(block)
|
85
85
|
else:
|
86
|
-
_logger.warning(
|
87
|
-
f"Removing non-text block from assistant message: {block.get('type')}"
|
88
|
-
)
|
86
|
+
_logger.warning(f"Removing non-text block from assistant message: {block.get('type')}")
|
89
87
|
anthropic_blocks = text_blocks
|
90
88
|
|
91
89
|
# Create the Anthropic message
|
@@ -127,15 +125,11 @@ class AnthropicConverter:
|
|
127
125
|
|
128
126
|
for content_item in content_items:
|
129
127
|
if isinstance(content_item, TextContent):
|
130
|
-
anthropic_blocks.append(
|
131
|
-
TextBlockParam(type="text", text=content_item.text)
|
132
|
-
)
|
128
|
+
anthropic_blocks.append(TextBlockParam(type="text", text=content_item.text))
|
133
129
|
|
134
130
|
elif isinstance(content_item, ImageContent):
|
135
131
|
# Check if image MIME type is supported
|
136
|
-
if not AnthropicConverter._is_supported_image_type(
|
137
|
-
content_item.mimeType
|
138
|
-
):
|
132
|
+
if not AnthropicConverter._is_supported_image_type(content_item.mimeType):
|
139
133
|
anthropic_blocks.append(
|
140
134
|
TextBlockParam(
|
141
135
|
type="text",
|
@@ -155,9 +149,7 @@ class AnthropicConverter:
|
|
155
149
|
)
|
156
150
|
|
157
151
|
elif isinstance(content_item, EmbeddedResource):
|
158
|
-
block = AnthropicConverter._convert_embedded_resource(
|
159
|
-
content_item, document_mode
|
160
|
-
)
|
152
|
+
block = AnthropicConverter._convert_embedded_resource(content_item, document_mode)
|
161
153
|
anthropic_blocks.append(block)
|
162
154
|
|
163
155
|
return anthropic_blocks
|
@@ -193,24 +185,16 @@ class AnthropicConverter:
|
|
193
185
|
|
194
186
|
elif is_image_mime_type(mime_type):
|
195
187
|
if not AnthropicConverter._is_supported_image_type(mime_type):
|
196
|
-
return AnthropicConverter._create_fallback_text(
|
197
|
-
f"Image with unsupported format '{mime_type}'", resource
|
198
|
-
)
|
188
|
+
return AnthropicConverter._create_fallback_text(f"Image with unsupported format '{mime_type}'", resource)
|
199
189
|
|
200
190
|
if is_url:
|
201
|
-
return ImageBlockParam(
|
202
|
-
type="image", source=URLImageSourceParam(type="url", url=str(uri))
|
203
|
-
)
|
191
|
+
return ImageBlockParam(type="image", source=URLImageSourceParam(type="url", url=str(uri)))
|
204
192
|
elif hasattr(resource_content, "blob"):
|
205
193
|
return ImageBlockParam(
|
206
194
|
type="image",
|
207
|
-
source=Base64ImageSourceParam(
|
208
|
-
type="base64", media_type=mime_type, data=resource_content.blob
|
209
|
-
),
|
195
|
+
source=Base64ImageSourceParam(type="base64", media_type=mime_type, data=resource_content.blob),
|
210
196
|
)
|
211
|
-
return AnthropicConverter._create_fallback_text(
|
212
|
-
"Image missing data", resource
|
213
|
-
)
|
197
|
+
return AnthropicConverter._create_fallback_text("Image missing data", resource)
|
214
198
|
|
215
199
|
elif mime_type == "application/pdf":
|
216
200
|
if is_url:
|
@@ -229,9 +213,7 @@ class AnthropicConverter:
|
|
229
213
|
data=resource_content.blob,
|
230
214
|
),
|
231
215
|
)
|
232
|
-
return TextBlockParam(
|
233
|
-
type="text", text=f"[PDF resource missing data: {title}]"
|
234
|
-
)
|
216
|
+
return TextBlockParam(type="text", text=f"[PDF resource missing data: {title}]")
|
235
217
|
|
236
218
|
elif is_text_mime_type(mime_type):
|
237
219
|
if not hasattr(resource_content, "text"):
|
@@ -260,18 +242,14 @@ class AnthropicConverter:
|
|
260
242
|
return TextBlockParam(type="text", text=resource_content.text)
|
261
243
|
|
262
244
|
# This is for binary resources - match the format expected by the test
|
263
|
-
if isinstance(resource.resource, BlobResourceContents) and hasattr(
|
264
|
-
resource.resource, "blob"
|
265
|
-
):
|
245
|
+
if isinstance(resource.resource, BlobResourceContents) and hasattr(resource.resource, "blob"):
|
266
246
|
blob_length = len(resource.resource.blob)
|
267
247
|
return TextBlockParam(
|
268
248
|
type="text",
|
269
249
|
text=f"Embedded Resource {uri._url} with unsupported format {mime_type} ({blob_length} characters)",
|
270
250
|
)
|
271
251
|
|
272
|
-
return AnthropicConverter._create_fallback_text(
|
273
|
-
f"Unsupported resource ({mime_type})", resource
|
274
|
-
)
|
252
|
+
return AnthropicConverter._create_fallback_text(f"Unsupported resource ({mime_type})", resource)
|
275
253
|
|
276
254
|
@staticmethod
|
277
255
|
def _determine_mime_type(
|
@@ -314,9 +292,7 @@ class AnthropicConverter:
|
|
314
292
|
return TextBlockParam(type="text", text="[SVG content could not be extracted]")
|
315
293
|
|
316
294
|
@staticmethod
|
317
|
-
def _create_fallback_text(
|
318
|
-
message: str, resource: Union[TextContent, ImageContent, EmbeddedResource]
|
319
|
-
) -> TextBlockParam:
|
295
|
+
def _create_fallback_text(message: str, resource: Union[TextContent, ImageContent, EmbeddedResource]) -> TextBlockParam:
|
320
296
|
"""
|
321
297
|
Create a fallback text block for unsupported resource types.
|
322
298
|
|
@@ -334,9 +310,7 @@ class AnthropicConverter:
|
|
334
310
|
return TextBlockParam(type="text", text=f"[{message}]")
|
335
311
|
|
336
312
|
@staticmethod
|
337
|
-
def convert_tool_result_to_anthropic(
|
338
|
-
tool_result: CallToolResult, tool_use_id: str
|
339
|
-
) -> ToolResultBlockParam:
|
313
|
+
def convert_tool_result_to_anthropic(tool_result: CallToolResult, tool_use_id: str) -> ToolResultBlockParam:
|
340
314
|
"""
|
341
315
|
Convert an MCP CallToolResult to an Anthropic ToolResultBlockParam.
|
342
316
|
|
@@ -353,22 +327,16 @@ class AnthropicConverter:
|
|
353
327
|
for item in tool_result.content:
|
354
328
|
if isinstance(item, EmbeddedResource):
|
355
329
|
# For embedded resources, always use text mode in tool results
|
356
|
-
resource_block = AnthropicConverter._convert_embedded_resource(
|
357
|
-
item, document_mode=False
|
358
|
-
)
|
330
|
+
resource_block = AnthropicConverter._convert_embedded_resource(item, document_mode=False)
|
359
331
|
anthropic_content.append(resource_block)
|
360
332
|
elif isinstance(item, (TextContent, ImageContent)):
|
361
333
|
# For text and image, use standard conversion
|
362
|
-
blocks = AnthropicConverter._convert_content_items(
|
363
|
-
[item], document_mode=False
|
364
|
-
)
|
334
|
+
blocks = AnthropicConverter._convert_content_items([item], document_mode=False)
|
365
335
|
anthropic_content.extend(blocks)
|
366
336
|
|
367
337
|
# If we ended up with no valid content blocks, create a placeholder
|
368
338
|
if not anthropic_content:
|
369
|
-
anthropic_content = [
|
370
|
-
TextBlockParam(type="text", text="[No content in tool result]")
|
371
|
-
]
|
339
|
+
anthropic_content = [TextBlockParam(type="text", text="[No content in tool result]")]
|
372
340
|
|
373
341
|
# Create the tool result block
|
374
342
|
return ToolResultBlockParam(
|
@@ -401,24 +369,18 @@ class AnthropicConverter:
|
|
401
369
|
# Process each content item in the result
|
402
370
|
for item in result.content:
|
403
371
|
if isinstance(item, (TextContent, ImageContent)):
|
404
|
-
blocks = AnthropicConverter._convert_content_items(
|
405
|
-
[item], document_mode=False
|
406
|
-
)
|
372
|
+
blocks = AnthropicConverter._convert_content_items([item], document_mode=False)
|
407
373
|
tool_result_blocks.extend(blocks)
|
408
374
|
elif isinstance(item, EmbeddedResource):
|
409
375
|
resource_content = item.resource
|
410
376
|
|
411
377
|
# Text resources go in tool results, others go as separate blocks
|
412
378
|
if isinstance(resource_content, TextResourceContents):
|
413
|
-
block = AnthropicConverter._convert_embedded_resource(
|
414
|
-
item, document_mode=False
|
415
|
-
)
|
379
|
+
block = AnthropicConverter._convert_embedded_resource(item, document_mode=False)
|
416
380
|
tool_result_blocks.append(block)
|
417
381
|
else:
|
418
382
|
# For binary resources like PDFs, add as separate block
|
419
|
-
block = AnthropicConverter._convert_embedded_resource(
|
420
|
-
item, document_mode=True
|
421
|
-
)
|
383
|
+
block = AnthropicConverter._convert_embedded_resource(item, document_mode=True)
|
422
384
|
separate_blocks.append(block)
|
423
385
|
|
424
386
|
# Create the tool result block if we have content
|
@@ -437,11 +399,7 @@ class AnthropicConverter:
|
|
437
399
|
ToolResultBlockParam(
|
438
400
|
type="tool_result",
|
439
401
|
tool_use_id=tool_use_id,
|
440
|
-
content=[
|
441
|
-
TextBlockParam(
|
442
|
-
type="text", text="[No content in tool result]"
|
443
|
-
)
|
444
|
-
],
|
402
|
+
content=[TextBlockParam(type="text", text="[No content in tool result]")],
|
445
403
|
is_error=result.isError,
|
446
404
|
)
|
447
405
|
)
|
@@ -1,23 +1,22 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
2
2
|
|
3
3
|
from mcp.types import (
|
4
|
-
TextContent,
|
5
|
-
ImageContent,
|
6
|
-
EmbeddedResource,
|
7
4
|
CallToolResult,
|
5
|
+
EmbeddedResource,
|
6
|
+
ImageContent,
|
8
7
|
PromptMessage,
|
8
|
+
TextContent,
|
9
9
|
)
|
10
|
-
|
10
|
+
|
11
|
+
from mcp_agent.logging.logger import get_logger
|
11
12
|
from mcp_agent.mcp.mime_utils import (
|
12
13
|
guess_mime_type,
|
13
|
-
is_text_mime_type,
|
14
14
|
is_image_mime_type,
|
15
|
+
is_text_mime_type,
|
15
16
|
)
|
17
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
16
18
|
from mcp_agent.mcp.resource_utils import extract_title_from_uri
|
17
19
|
|
18
|
-
|
19
|
-
from mcp_agent.logging.logger import get_logger
|
20
|
-
|
21
20
|
_logger = get_logger("multipart_converter_openai")
|
22
21
|
|
23
22
|
# Define type aliases for content blocks
|
@@ -39,16 +38,10 @@ class OpenAIConverter:
|
|
39
38
|
Returns:
|
40
39
|
True if the MIME type is generally supported, False otherwise
|
41
40
|
"""
|
42
|
-
return (
|
43
|
-
mime_type is not None
|
44
|
-
and is_image_mime_type(mime_type)
|
45
|
-
and mime_type != "image/svg+xml"
|
46
|
-
)
|
41
|
+
return mime_type is not None and is_image_mime_type(mime_type) and mime_type != "image/svg+xml"
|
47
42
|
|
48
43
|
@staticmethod
|
49
|
-
def convert_to_openai(
|
50
|
-
multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False
|
51
|
-
) -> OpenAIMessage:
|
44
|
+
def convert_to_openai(multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False) -> OpenAIMessage:
|
52
45
|
"""
|
53
46
|
Convert a PromptMessageMultipart message to OpenAI API format.
|
54
47
|
|
@@ -104,9 +97,7 @@ class OpenAIConverter:
|
|
104
97
|
|
105
98
|
# Handle input_audio if implemented
|
106
99
|
elif hasattr(item, "type") and getattr(item, "type") == "input_audio":
|
107
|
-
_logger.warning(
|
108
|
-
"Input audio content not supported in standard OpenAI types"
|
109
|
-
)
|
100
|
+
_logger.warning("Input audio content not supported in standard OpenAI types")
|
110
101
|
fallback_text = "[Audio content not directly supported]"
|
111
102
|
content_blocks.append({"type": "text", "text": fallback_text})
|
112
103
|
|
@@ -127,11 +118,7 @@ class OpenAIConverter:
|
|
127
118
|
return {"role": role, "content": ""}
|
128
119
|
|
129
120
|
# If we only have one text content and it's empty, return an empty string for content
|
130
|
-
if (
|
131
|
-
len(content_blocks) == 1
|
132
|
-
and content_blocks[0]["type"] == "text"
|
133
|
-
and not content_blocks[0]["text"]
|
134
|
-
):
|
121
|
+
if len(content_blocks) == 1 and content_blocks[0]["type"] == "text" and not content_blocks[0]["text"]:
|
135
122
|
return {"role": role, "content": ""}
|
136
123
|
|
137
124
|
# If concatenate_text_blocks is True, combine adjacent text blocks
|
@@ -180,9 +167,7 @@ class OpenAIConverter:
|
|
180
167
|
return combined_blocks
|
181
168
|
|
182
169
|
@staticmethod
|
183
|
-
def convert_prompt_message_to_openai(
|
184
|
-
message: PromptMessage, concatenate_text_blocks: bool = False
|
185
|
-
) -> OpenAIMessage:
|
170
|
+
def convert_prompt_message_to_openai(message: PromptMessage, concatenate_text_blocks: bool = False) -> OpenAIMessage:
|
186
171
|
"""
|
187
172
|
Convert a standard PromptMessage to OpenAI API format.
|
188
173
|
|
@@ -265,9 +250,7 @@ class OpenAIConverter:
|
|
265
250
|
elif hasattr(resource_content, "blob"):
|
266
251
|
return {
|
267
252
|
"type": "image_url",
|
268
|
-
"image_url": {
|
269
|
-
"url": f"data:{mime_type};base64,{resource_content.blob}"
|
270
|
-
},
|
253
|
+
"image_url": {"url": f"data:{mime_type};base64,{resource_content.blob}"},
|
271
254
|
}
|
272
255
|
else:
|
273
256
|
return {"type": "text", "text": f"[Image missing data: {title}]"}
|
@@ -291,20 +274,12 @@ class OpenAIConverter:
|
|
291
274
|
|
292
275
|
# Handle SVG (convert to text)
|
293
276
|
elif mime_type == "image/svg+xml" and hasattr(resource_content, "text"):
|
294
|
-
file_text =
|
295
|
-
f'<fastagent:file title="{title}" mimetype="{mime_type}">\n'
|
296
|
-
f"{resource_content.text}\n"
|
297
|
-
f"</fastagent:file>"
|
298
|
-
)
|
277
|
+
file_text = f'<fastagent:file title="{title}" mimetype="{mime_type}">\n' f"{resource_content.text}\n" f"</fastagent:file>"
|
299
278
|
return {"type": "text", "text": file_text}
|
300
279
|
|
301
280
|
# Handle text files
|
302
281
|
elif is_text_mime_type(mime_type) and hasattr(resource_content, "text"):
|
303
|
-
file_text =
|
304
|
-
f'<fastagent:file title="{title}" mimetype="{mime_type}">\n'
|
305
|
-
f"{resource_content.text}\n"
|
306
|
-
f"</fastagent:file>"
|
307
|
-
)
|
282
|
+
file_text = f'<fastagent:file title="{title}" mimetype="{mime_type}">\n' f"{resource_content.text}\n" f"</fastagent:file>"
|
308
283
|
return {"type": "text", "text": file_text}
|
309
284
|
|
310
285
|
# Default fallback for text resources
|
@@ -349,11 +324,7 @@ class OpenAIConverter:
|
|
349
324
|
if block.get("type") == "text":
|
350
325
|
text_parts.append(block.get("text", ""))
|
351
326
|
|
352
|
-
return (
|
353
|
-
" ".join(text_parts)
|
354
|
-
if text_parts
|
355
|
-
else "[Complex content converted to text]"
|
356
|
-
)
|
327
|
+
return " ".join(text_parts) if text_parts else "[Complex content converted to text]"
|
357
328
|
|
358
329
|
@staticmethod
|
359
330
|
def convert_tool_result_to_openai(
|
@@ -399,14 +370,10 @@ class OpenAIConverter:
|
|
399
370
|
if text_content:
|
400
371
|
# Convert text content to OpenAI format
|
401
372
|
temp_multipart = PromptMessageMultipart(role="user", content=text_content)
|
402
|
-
converted = OpenAIConverter.convert_to_openai(
|
403
|
-
temp_multipart, concatenate_text_blocks=concatenate_text_blocks
|
404
|
-
)
|
373
|
+
converted = OpenAIConverter.convert_to_openai(temp_multipart, concatenate_text_blocks=concatenate_text_blocks)
|
405
374
|
|
406
375
|
# Extract text from content blocks
|
407
|
-
tool_message_content = OpenAIConverter._extract_text_from_content_blocks(
|
408
|
-
converted.get("content", "")
|
409
|
-
)
|
376
|
+
tool_message_content = OpenAIConverter._extract_text_from_content_blocks(converted.get("content", ""))
|
410
377
|
|
411
378
|
if not tool_message_content:
|
412
379
|
tool_message_content = "[Tool returned non-text content]"
|
@@ -423,9 +390,7 @@ class OpenAIConverter:
|
|
423
390
|
return tool_message
|
424
391
|
|
425
392
|
# Process non-text content as a separate user message
|
426
|
-
non_text_multipart = PromptMessageMultipart(
|
427
|
-
role="user", content=non_text_content
|
428
|
-
)
|
393
|
+
non_text_multipart = PromptMessageMultipart(role="user", content=non_text_content)
|
429
394
|
|
430
395
|
# Convert to OpenAI format
|
431
396
|
user_message = OpenAIConverter.convert_to_openai(non_text_multipart)
|