fast-agent-mcp 0.1.11__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.11.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 -102
- mcp_agent/app.py +16 -27
- 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 -26
- mcp_agent/context_dependent.py +3 -7
- mcp_agent/core/agent_app.py +46 -122
- mcp_agent/core/agent_types.py +29 -2
- mcp_agent/core/agent_utils.py +3 -5
- mcp_agent/core/decorators.py +6 -14
- mcp_agent/core/enhanced_prompt.py +25 -52
- mcp_agent/core/error_handling.py +1 -1
- mcp_agent/core/exceptions.py +8 -8
- mcp_agent/core/factory.py +30 -72
- mcp_agent/core/fastagent.py +48 -88
- mcp_agent/core/mcp_content.py +10 -19
- mcp_agent/core/prompt.py +8 -15
- mcp_agent/core/proxies.py +34 -25
- mcp_agent/core/request_params.py +46 -0
- mcp_agent/core/types.py +6 -6
- mcp_agent/core/validation.py +16 -16
- 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 +24 -24
- mcp_agent/mcp/gen_client.py +4 -12
- mcp_agent/mcp/interfaces.py +107 -88
- 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 +49 -122
- 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 +62 -64
- mcp_agent/mcp/stdio.py +11 -8
- mcp_agent/mcp_server/__init__.py +1 -1
- 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/__init__.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 +17 -41
- 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 +94 -332
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +43 -76
- mcp_agent/workflows/llm/augmented_llm_openai.py +46 -100
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +42 -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 +9 -21
- mcp_agent/workflows/llm/openai_utils.py +1 -1
- mcp_agent/workflows/llm/prompt_utils.py +39 -27
- mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +246 -184
- mcp_agent/workflows/llm/providers/multipart_converter_openai.py +212 -202
- mcp_agent/workflows/llm/providers/openai_multipart.py +19 -61
- mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +11 -212
- mcp_agent/workflows/llm/providers/sampling_converter_openai.py +13 -215
- mcp_agent/workflows/llm/sampling_converter.py +117 -0
- mcp_agent/workflows/llm/sampling_format_converter.py +12 -29
- 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 +29 -59
- 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.11.dist-info/RECORD +0 -160
- mcp_agent/workflows/llm/llm_selector.py +0 -345
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
@@ -4,19 +4,18 @@ Clean utilities for converting between PromptMessageMultipart and OpenAI message
|
|
4
4
|
Each function handles all content types consistently and is designed for simple testing.
|
5
5
|
"""
|
6
6
|
|
7
|
-
from typing import Dict,
|
8
|
-
|
9
|
-
from openai.types.chat import (
|
10
|
-
ChatCompletionMessage,
|
11
|
-
ChatCompletionMessageParam,
|
12
|
-
)
|
7
|
+
from typing import Any, Dict, List, Union
|
13
8
|
|
14
9
|
from mcp.types import (
|
15
|
-
|
16
|
-
ImageContent,
|
10
|
+
BlobResourceContents,
|
17
11
|
EmbeddedResource,
|
12
|
+
ImageContent,
|
13
|
+
TextContent,
|
18
14
|
TextResourceContents,
|
19
|
-
|
15
|
+
)
|
16
|
+
from openai.types.chat import (
|
17
|
+
ChatCompletionMessage,
|
18
|
+
ChatCompletionMessageParam,
|
20
19
|
)
|
21
20
|
|
22
21
|
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
@@ -64,44 +63,22 @@ def _openai_message_to_multipart(
|
|
64
63
|
# Handle list of content parts
|
65
64
|
elif isinstance(content, list):
|
66
65
|
for part in content:
|
67
|
-
part_type = (
|
68
|
-
part.get("type")
|
69
|
-
if isinstance(part, dict)
|
70
|
-
else getattr(part, "type", None)
|
71
|
-
)
|
66
|
+
part_type = part.get("type") if isinstance(part, dict) else getattr(part, "type", None)
|
72
67
|
|
73
68
|
# Handle text content
|
74
69
|
if part_type == "text":
|
75
|
-
text = (
|
76
|
-
part.get("text")
|
77
|
-
if isinstance(part, dict)
|
78
|
-
else getattr(part, "text", "")
|
79
|
-
)
|
70
|
+
text = part.get("text") if isinstance(part, dict) else getattr(part, "text", "")
|
80
71
|
|
81
72
|
# Check if this is a resource marker
|
82
|
-
if (
|
83
|
-
text
|
84
|
-
and (
|
85
|
-
text.startswith("[Resource:")
|
86
|
-
or text.startswith("[Binary Resource:")
|
87
|
-
)
|
88
|
-
and "\n" in text
|
89
|
-
):
|
73
|
+
if text and (text.startswith("[Resource:") or text.startswith("[Binary Resource:")) and "\n" in text:
|
90
74
|
header, content_text = text.split("\n", 1)
|
91
75
|
if "MIME:" in header:
|
92
76
|
mime_match = header.split("MIME:", 1)[1].split("]")[0].strip()
|
93
77
|
|
94
78
|
# If not text/plain, create an embedded resource
|
95
79
|
if mime_match != "text/plain":
|
96
|
-
if
|
97
|
-
"Resource:"
|
98
|
-
and "Binary Resource:" not in header
|
99
|
-
):
|
100
|
-
uri = (
|
101
|
-
header.split("Resource:", 1)[1]
|
102
|
-
.split(",")[0]
|
103
|
-
.strip()
|
104
|
-
)
|
80
|
+
if "Resource:" in header and "Binary Resource:" not in header:
|
81
|
+
uri = header.split("Resource:", 1)[1].split(",")[0].strip()
|
105
82
|
mcp_contents.append(
|
106
83
|
EmbeddedResource(
|
107
84
|
type="resource",
|
@@ -119,31 +96,17 @@ def _openai_message_to_multipart(
|
|
119
96
|
|
120
97
|
# Handle image content
|
121
98
|
elif part_type == "image_url":
|
122
|
-
image_url = (
|
123
|
-
part.get("image_url", {})
|
124
|
-
if isinstance(part, dict)
|
125
|
-
else getattr(part, "image_url", None)
|
126
|
-
)
|
99
|
+
image_url = part.get("image_url", {}) if isinstance(part, dict) else getattr(part, "image_url", None)
|
127
100
|
if image_url:
|
128
|
-
url = (
|
129
|
-
image_url.get("url")
|
130
|
-
if isinstance(image_url, dict)
|
131
|
-
else getattr(image_url, "url", "")
|
132
|
-
)
|
101
|
+
url = image_url.get("url") if isinstance(image_url, dict) else getattr(image_url, "url", "")
|
133
102
|
if url and url.startswith("data:image/"):
|
134
103
|
# Handle base64 data URLs
|
135
104
|
mime_type = url.split(";")[0].replace("data:", "")
|
136
105
|
data = url.split(",")[1]
|
137
|
-
mcp_contents.append(
|
138
|
-
ImageContent(type="image", data=data, mimeType=mime_type)
|
139
|
-
)
|
106
|
+
mcp_contents.append(ImageContent(type="image", data=data, mimeType=mime_type))
|
140
107
|
|
141
108
|
# Handle explicit resource types
|
142
|
-
elif (
|
143
|
-
part_type == "resource"
|
144
|
-
and isinstance(part, dict)
|
145
|
-
and "resource" in part
|
146
|
-
):
|
109
|
+
elif part_type == "resource" and isinstance(part, dict) and "resource" in part:
|
147
110
|
resource = part["resource"]
|
148
111
|
if isinstance(resource, dict):
|
149
112
|
# Text resource
|
@@ -152,9 +115,7 @@ def _openai_message_to_multipart(
|
|
152
115
|
uri = resource.get("uri", "resource://unknown")
|
153
116
|
|
154
117
|
if mime_type == "text/plain":
|
155
|
-
mcp_contents.append(
|
156
|
-
TextContent(type="text", text=resource["text"])
|
157
|
-
)
|
118
|
+
mcp_contents.append(TextContent(type="text", text=resource["text"]))
|
158
119
|
else:
|
159
120
|
mcp_contents.append(
|
160
121
|
EmbeddedResource(
|
@@ -171,10 +132,7 @@ def _openai_message_to_multipart(
|
|
171
132
|
mime_type = resource["mimeType"]
|
172
133
|
uri = resource.get("uri", "resource://unknown")
|
173
134
|
|
174
|
-
if (
|
175
|
-
mime_type.startswith("image/")
|
176
|
-
and mime_type != "image/svg+xml"
|
177
|
-
):
|
135
|
+
if mime_type.startswith("image/") and mime_type != "image/svg+xml":
|
178
136
|
mcp_contents.append(
|
179
137
|
ImageContent(
|
180
138
|
type="image",
|
@@ -1,204 +1,30 @@
|
|
1
|
-
import json
|
2
|
-
from typing import Iterable, List
|
3
|
-
from mcp import CreateMessageResult, SamplingMessage, StopReason
|
4
|
-
from pydantic import BaseModel
|
5
|
-
from mcp_agent.workflows.llm.sampling_format_converter import SamplingFormatConverter
|
6
|
-
|
7
|
-
from mcp.types import (
|
8
|
-
PromptMessage,
|
9
|
-
EmbeddedResource,
|
10
|
-
ImageContent,
|
11
|
-
TextContent,
|
12
|
-
TextResourceContents,
|
13
|
-
)
|
14
|
-
|
15
1
|
from anthropic.types import (
|
16
|
-
ContentBlock,
|
17
|
-
DocumentBlockParam,
|
18
2
|
Message,
|
19
3
|
MessageParam,
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
ToolUseBlockParam,
|
4
|
+
)
|
5
|
+
from mcp import StopReason
|
6
|
+
from mcp.types import (
|
7
|
+
PromptMessage,
|
25
8
|
)
|
26
9
|
|
27
10
|
from mcp_agent.logging.logger import get_logger
|
11
|
+
from mcp_agent.workflows.llm.providers.multipart_converter_anthropic import (
|
12
|
+
AnthropicConverter,
|
13
|
+
)
|
14
|
+
from mcp_agent.workflows.llm.sampling_format_converter import ProviderFormatConverter
|
28
15
|
|
29
16
|
_logger = get_logger(__name__)
|
30
17
|
|
31
18
|
|
32
|
-
class AnthropicSamplingConverter(
|
19
|
+
class AnthropicSamplingConverter(ProviderFormatConverter[MessageParam, Message]):
|
33
20
|
"""
|
34
21
|
Convert between Anthropic and MCP types.
|
35
22
|
"""
|
36
23
|
|
37
|
-
@classmethod
|
38
|
-
def from_sampling_result(cls, result: CreateMessageResult) -> Message:
|
39
|
-
# -> Message
|
40
|
-
if result.role != "assistant":
|
41
|
-
raise ValueError(
|
42
|
-
f"Expected role to be 'assistant' but got '{result.role}' instead."
|
43
|
-
)
|
44
|
-
|
45
|
-
return Message(
|
46
|
-
role="assistant",
|
47
|
-
type="message",
|
48
|
-
content=[mcp_content_to_anthropic_content(result.content)],
|
49
|
-
stop_reason=mcp_stop_reason_to_anthropic_stop_reason(result.stopReason),
|
50
|
-
model=result.model,
|
51
|
-
usage={"input_tokens": 0, "output_tokens": 0},
|
52
|
-
id="sampling_id",
|
53
|
-
# TODO -- incorporate usage info and message identity
|
54
|
-
)
|
55
|
-
|
56
|
-
@classmethod
|
57
|
-
def to_sampling_result(cls, result: Message) -> CreateMessageResult:
|
58
|
-
contents = anthropic_content_to_mcp_content(result.content)
|
59
|
-
if len(contents) > 1:
|
60
|
-
raise NotImplementedError(
|
61
|
-
"Multiple content elements in a single message are not supported in MCP yet"
|
62
|
-
)
|
63
|
-
mcp_content = contents[0]
|
64
|
-
|
65
|
-
# Create a dictionary with required fields
|
66
|
-
result_dict = {
|
67
|
-
"role": result.role,
|
68
|
-
"content": mcp_content,
|
69
|
-
"model": result.model,
|
70
|
-
"stopReason": anthropic_stop_reason_to_mcp_stop_reason(result.stop_reason),
|
71
|
-
}
|
72
|
-
|
73
|
-
# Add any other fields from the original message that might be needed
|
74
|
-
extras = result.model_dump(exclude={"role", "content", "model", "stop_reason"})
|
75
|
-
if extras:
|
76
|
-
# Only include compatible fields to avoid validation errors
|
77
|
-
# Skip fields that would cause validation issues with CreateMessageResult
|
78
|
-
safe_extras = {
|
79
|
-
k: v for k, v in extras.items() if k in CreateMessageResult.model_fields
|
80
|
-
}
|
81
|
-
result_dict.update(safe_extras)
|
82
|
-
|
83
|
-
return CreateMessageResult(**result_dict)
|
84
|
-
|
85
|
-
@classmethod
|
86
|
-
def from_sampling_message(cls, param: SamplingMessage) -> MessageParam:
|
87
|
-
extras = param.model_dump(exclude={"role", "content"})
|
88
|
-
return MessageParam(
|
89
|
-
role=param.role,
|
90
|
-
content=[mcp_content_to_anthropic_content(param.content)],
|
91
|
-
**extras,
|
92
|
-
)
|
93
|
-
|
94
|
-
@classmethod
|
95
|
-
def to_sampling_message(cls, param: MessageParam) -> SamplingMessage:
|
96
|
-
# Implement the conversion from ChatCompletionMessage to MCP message param
|
97
|
-
|
98
|
-
contents = anthropic_content_to_mcp_content(param["content"])
|
99
|
-
|
100
|
-
# TODO: saqadri - the mcp_content can have multiple elements
|
101
|
-
# while sampling message content has a single content element
|
102
|
-
# Right now we error out if there are > 1 elements in mcp_content
|
103
|
-
# We need to handle this case properly going forward
|
104
|
-
if len(contents) > 1:
|
105
|
-
raise NotImplementedError(
|
106
|
-
"Multiple content elements in a single message are not supported"
|
107
|
-
)
|
108
|
-
mcp_content = contents[0]
|
109
|
-
|
110
|
-
# Only include fields that are valid for SamplingMessage
|
111
|
-
extras = {
|
112
|
-
k: v
|
113
|
-
for k, v in param.items()
|
114
|
-
if k not in ["role", "content"] and k in SamplingMessage.model_fields
|
115
|
-
}
|
116
|
-
|
117
|
-
return SamplingMessage(
|
118
|
-
role=param["role"],
|
119
|
-
content=mcp_content,
|
120
|
-
**extras,
|
121
|
-
)
|
122
|
-
|
123
24
|
@classmethod
|
124
25
|
def from_prompt_message(cls, message: PromptMessage) -> MessageParam:
|
125
26
|
"""Convert an MCP PromptMessage to an Anthropic MessageParam."""
|
126
|
-
|
127
|
-
# Extract content text
|
128
|
-
content_text = (
|
129
|
-
message.content.text
|
130
|
-
if hasattr(message.content, "text")
|
131
|
-
else str(message.content)
|
132
|
-
)
|
133
|
-
|
134
|
-
# Extract extras for flexibility
|
135
|
-
extras = message.model_dump(exclude={"role", "content"})
|
136
|
-
|
137
|
-
# Handle based on role
|
138
|
-
if message.role == "user":
|
139
|
-
return {"role": "user", "content": content_text, **extras}
|
140
|
-
elif message.role == "assistant":
|
141
|
-
return {
|
142
|
-
"role": "assistant",
|
143
|
-
"content": [{"type": "text", "text": content_text}],
|
144
|
-
**extras,
|
145
|
-
}
|
146
|
-
else:
|
147
|
-
# Fall back to user for any unrecognized role, including "system"
|
148
|
-
_logger.warning(
|
149
|
-
f"Unsupported role '{message.role}' in PromptMessage. Falling back to 'user' role."
|
150
|
-
)
|
151
|
-
return {
|
152
|
-
"role": "user",
|
153
|
-
"content": f"[{message.role.upper()}] {content_text}",
|
154
|
-
**extras,
|
155
|
-
}
|
156
|
-
|
157
|
-
|
158
|
-
def anthropic_content_to_mcp_content(
|
159
|
-
content: str
|
160
|
-
| Iterable[
|
161
|
-
TextBlockParam
|
162
|
-
| ImageBlockParam
|
163
|
-
| ToolUseBlockParam
|
164
|
-
| ToolResultBlockParam
|
165
|
-
| DocumentBlockParam
|
166
|
-
| ContentBlock
|
167
|
-
],
|
168
|
-
) -> List[TextContent | ImageContent | EmbeddedResource]:
|
169
|
-
mcp_content = []
|
170
|
-
|
171
|
-
if isinstance(content, str):
|
172
|
-
mcp_content.append(TextContent(type="text", text=content))
|
173
|
-
else:
|
174
|
-
for block in content:
|
175
|
-
if block.type == "text":
|
176
|
-
mcp_content.append(TextContent(type="text", text=block.text))
|
177
|
-
elif block.type == "image":
|
178
|
-
raise NotImplementedError("Image content conversion not implemented")
|
179
|
-
elif block.type == "tool_use":
|
180
|
-
# Best effort to convert a tool use to text (since there's no ToolUseContent)
|
181
|
-
mcp_content.append(
|
182
|
-
TextContent(
|
183
|
-
type="text",
|
184
|
-
text=to_string(block),
|
185
|
-
)
|
186
|
-
)
|
187
|
-
elif block.type == "tool_result":
|
188
|
-
# Best effort to convert a tool result to text (since there's no ToolResultContent)
|
189
|
-
mcp_content.append(
|
190
|
-
TextContent(
|
191
|
-
type="text",
|
192
|
-
text=to_string(block),
|
193
|
-
)
|
194
|
-
)
|
195
|
-
elif block.type == "document":
|
196
|
-
raise NotImplementedError("Document content conversion not implemented")
|
197
|
-
else:
|
198
|
-
# Last effort to convert the content to a string
|
199
|
-
mcp_content.append(TextContent(type="text", text=str(block)))
|
200
|
-
|
201
|
-
return mcp_content
|
27
|
+
return AnthropicConverter.convert_prompt_message_to_anthropic(message)
|
202
28
|
|
203
29
|
|
204
30
|
def mcp_stop_reason_to_anthropic_stop_reason(stop_reason: StopReason):
|
@@ -218,7 +44,7 @@ def mcp_stop_reason_to_anthropic_stop_reason(stop_reason: StopReason):
|
|
218
44
|
|
219
45
|
def anthropic_stop_reason_to_mcp_stop_reason(stop_reason: str) -> StopReason:
|
220
46
|
if not stop_reason:
|
221
|
-
return
|
47
|
+
return "end_turn"
|
222
48
|
elif stop_reason == "end_turn":
|
223
49
|
return "endTurn"
|
224
50
|
elif stop_reason == "max_tokens":
|
@@ -229,30 +55,3 @@ def anthropic_stop_reason_to_mcp_stop_reason(stop_reason: str) -> StopReason:
|
|
229
55
|
return "toolUse"
|
230
56
|
else:
|
231
57
|
return stop_reason
|
232
|
-
|
233
|
-
|
234
|
-
def mcp_content_to_anthropic_content(
|
235
|
-
content: TextContent | ImageContent | EmbeddedResource,
|
236
|
-
) -> ContentBlock:
|
237
|
-
if isinstance(content, TextContent):
|
238
|
-
return TextBlock(type=content.type, text=content.text)
|
239
|
-
elif isinstance(content, ImageContent):
|
240
|
-
# Best effort to convert an image to text (since there's no ImageBlock)
|
241
|
-
return TextBlock(type="text", text=f"{content.mimeType}:{content.data}")
|
242
|
-
elif isinstance(content, EmbeddedResource):
|
243
|
-
if isinstance(content.resource, TextResourceContents):
|
244
|
-
return TextBlock(type="text", text=content.resource.text)
|
245
|
-
else: # BlobResourceContents
|
246
|
-
return TextBlock(
|
247
|
-
type="text", text=f"{content.resource.mimeType}:{content.resource.blob}"
|
248
|
-
)
|
249
|
-
else:
|
250
|
-
# Last effort to convert the content to a string
|
251
|
-
return TextBlock(type="text", text=str(content))
|
252
|
-
|
253
|
-
|
254
|
-
def to_string(obj: BaseModel | dict) -> str:
|
255
|
-
if isinstance(obj, BaseModel):
|
256
|
-
return obj.model_dump_json()
|
257
|
-
else:
|
258
|
-
return json.dumps(obj)
|
@@ -1,229 +1,27 @@
|
|
1
|
-
from typing import
|
2
|
-
from mcp import CreateMessageResult, SamplingMessage
|
3
|
-
from openai.types.chat import (
|
4
|
-
ChatCompletionMessage,
|
5
|
-
ChatCompletionUserMessageParam,
|
6
|
-
ChatCompletionAssistantMessageParam,
|
7
|
-
ChatCompletionMessageParam,
|
8
|
-
ChatCompletionContentPartTextParam,
|
9
|
-
ChatCompletionContentPartParam,
|
10
|
-
ChatCompletionContentPartRefusalParam,
|
11
|
-
)
|
1
|
+
from typing import Any, Dict
|
12
2
|
|
13
3
|
from mcp.types import (
|
14
4
|
PromptMessage,
|
15
|
-
TextContent,
|
16
|
-
ImageContent,
|
17
|
-
EmbeddedResource,
|
18
|
-
TextResourceContents,
|
19
5
|
)
|
20
|
-
|
21
|
-
|
22
|
-
SamplingFormatConverter,
|
23
|
-
typed_dict_extras,
|
6
|
+
from openai.types.chat import (
|
7
|
+
ChatCompletionMessage,
|
24
8
|
)
|
25
9
|
|
26
10
|
from mcp_agent.logging.logger import get_logger
|
11
|
+
from mcp_agent.workflows.llm.sampling_format_converter import (
|
12
|
+
ProviderFormatConverter,
|
13
|
+
)
|
27
14
|
|
28
15
|
_logger = get_logger(__name__)
|
29
16
|
|
30
17
|
|
31
|
-
class OpenAISamplingConverter(
|
32
|
-
SamplingFormatConverter[ChatCompletionMessageParam, ChatCompletionMessage]
|
33
|
-
):
|
34
|
-
"""
|
35
|
-
Convert between OpenAI and MCP types.
|
36
|
-
"""
|
37
|
-
|
38
|
-
@classmethod
|
39
|
-
def from_sampling_result(cls, result: CreateMessageResult) -> ChatCompletionMessage:
|
40
|
-
"""Convert an MCP message result to an OpenAI ChatCompletionMessage."""
|
41
|
-
# Basic implementation - would need to be expanded
|
42
|
-
|
43
|
-
if result.role != "assistant":
|
44
|
-
raise ValueError(
|
45
|
-
f"Expected role to be 'assistant' but got '{result.role}' instead."
|
46
|
-
)
|
47
|
-
# TODO -- add image support for sampling
|
48
|
-
return ChatCompletionMessage(
|
49
|
-
role=result.role,
|
50
|
-
content=result.content.text or "image",
|
51
|
-
)
|
52
|
-
|
53
|
-
@classmethod
|
54
|
-
def to_sampling_result(cls, result: ChatCompletionMessage) -> CreateMessageResult:
|
55
|
-
"""Convert an OpenAI ChatCompletionMessage to an MCP message result."""
|
56
|
-
content = result.content
|
57
|
-
if content is None:
|
58
|
-
content = ""
|
59
|
-
|
60
|
-
return CreateMessageResult(
|
61
|
-
role=result.role,
|
62
|
-
content=TextContent(type="text", text=content),
|
63
|
-
model="unknown", # Model is required by CreateMessageResult
|
64
|
-
)
|
65
|
-
|
66
|
-
@classmethod
|
67
|
-
def from_sampling_message(
|
68
|
-
cls, param: SamplingMessage
|
69
|
-
) -> ChatCompletionMessageParam:
|
70
|
-
if param.role == "assistant":
|
71
|
-
return ChatCompletionAssistantMessageParam(
|
72
|
-
role="assistant",
|
73
|
-
content=mcp_to_openai_blocks(param.content),
|
74
|
-
)
|
75
|
-
elif param.role == "user":
|
76
|
-
return ChatCompletionUserMessageParam(
|
77
|
-
role="user",
|
78
|
-
content=mcp_to_openai_blocks(param.content),
|
79
|
-
)
|
80
|
-
else:
|
81
|
-
raise ValueError(
|
82
|
-
f"Unexpected role: {param.role}, MCP only supports 'assistant' and 'user'"
|
83
|
-
)
|
84
|
-
|
18
|
+
class OpenAISamplingConverter(ProviderFormatConverter[Dict[str, Any], ChatCompletionMessage]):
|
85
19
|
@classmethod
|
86
|
-
def
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
# while sampling message content has a single content element
|
91
|
-
# Right now we error out if there are > 1 elements in mcp_content
|
92
|
-
# We need to handle this case properly going forward
|
93
|
-
if len(contents) > 1:
|
94
|
-
raise NotImplementedError(
|
95
|
-
"Multiple content elements in a single message are not supported"
|
96
|
-
)
|
97
|
-
mcp_content: TextContent | ImageContent | EmbeddedResource = contents[0]
|
98
|
-
|
99
|
-
if param["role"] == "assistant":
|
100
|
-
return SamplingMessage(
|
101
|
-
role="assistant",
|
102
|
-
content=mcp_content,
|
103
|
-
**typed_dict_extras(param, ["role", "content"]),
|
104
|
-
)
|
105
|
-
elif param["role"] == "user":
|
106
|
-
return SamplingMessage(
|
107
|
-
role="user",
|
108
|
-
content=mcp_content,
|
109
|
-
**typed_dict_extras(param, ["role", "content"]),
|
110
|
-
)
|
111
|
-
elif param.role == "tool":
|
112
|
-
raise NotImplementedError(
|
113
|
-
"Tool messages are not supported in SamplingMessage yet"
|
114
|
-
)
|
115
|
-
elif param.role == "system":
|
116
|
-
raise NotImplementedError(
|
117
|
-
"System messages are not supported in SamplingMessage yet"
|
118
|
-
)
|
119
|
-
elif param.role == "developer":
|
120
|
-
raise NotImplementedError(
|
121
|
-
"Developer messages are not supported in SamplingMessage yet"
|
122
|
-
)
|
123
|
-
elif param.role == "function":
|
124
|
-
raise NotImplementedError(
|
125
|
-
"Function messages are not supported in SamplingMessage yet"
|
126
|
-
)
|
127
|
-
else:
|
128
|
-
raise ValueError(
|
129
|
-
f"Unexpected role: {param.role}, MCP only supports 'assistant', 'user', 'tool', 'system', 'developer', and 'function'"
|
130
|
-
)
|
131
|
-
|
132
|
-
@classmethod
|
133
|
-
def from_prompt_message(cls, message: PromptMessage) -> ChatCompletionMessageParam:
|
134
|
-
"""Convert an MCP PromptMessage to an OpenAI ChatCompletionMessageParam."""
|
135
|
-
content_text = (
|
136
|
-
message.content.text
|
137
|
-
if hasattr(message.content, "text")
|
138
|
-
else str(message.content)
|
139
|
-
)
|
140
|
-
|
141
|
-
return {
|
142
|
-
"role": message.role,
|
143
|
-
"content": content_text,
|
144
|
-
}
|
145
|
-
|
146
|
-
|
147
|
-
def mcp_to_openai_blocks(
|
148
|
-
content: TextContent | ImageContent | EmbeddedResource,
|
149
|
-
) -> ChatCompletionContentPartTextParam:
|
150
|
-
if isinstance(content, list):
|
151
|
-
# Handle list of content items
|
152
|
-
return ChatCompletionContentPartTextParam(
|
153
|
-
type="text",
|
154
|
-
text="\n".join(mcp_to_openai_blocks(c) for c in content),
|
155
|
-
)
|
156
|
-
|
157
|
-
if isinstance(content, TextContent):
|
158
|
-
return ChatCompletionContentPartTextParam(type="text", text=content.text)
|
159
|
-
elif isinstance(content, ImageContent):
|
160
|
-
# Best effort to convert an image to text
|
161
|
-
return ChatCompletionContentPartTextParam(
|
162
|
-
type="text", text=f"{content.mimeType}:{content.data}"
|
20
|
+
def from_prompt_message(cls, message: PromptMessage) -> Dict[str, Any]:
|
21
|
+
"""Convert an MCP PromptMessage to an OpenAI message dict."""
|
22
|
+
from mcp_agent.workflows.llm.providers.multipart_converter_openai import (
|
23
|
+
OpenAIConverter,
|
163
24
|
)
|
164
|
-
elif isinstance(content, EmbeddedResource):
|
165
|
-
if isinstance(content.resource, TextResourceContents):
|
166
|
-
return ChatCompletionContentPartTextParam(
|
167
|
-
type="text", text=content.resource.text
|
168
|
-
)
|
169
|
-
else: # BlobResourceContents
|
170
|
-
return ChatCompletionContentPartTextParam(
|
171
|
-
type="text", text=f"{content.resource.mimeType}:{content.resource.blob}"
|
172
|
-
)
|
173
|
-
else:
|
174
|
-
# Last effort to convert the content to a string
|
175
|
-
return ChatCompletionContentPartTextParam(type="text", text=str(content))
|
176
|
-
|
177
|
-
|
178
|
-
def openai_to_mcp_blocks(
|
179
|
-
content: str
|
180
|
-
| Iterable[ChatCompletionContentPartParam | ChatCompletionContentPartRefusalParam],
|
181
|
-
) -> Iterable[TextContent | ImageContent | EmbeddedResource]:
|
182
|
-
mcp_content = []
|
183
|
-
|
184
|
-
if isinstance(content, str):
|
185
|
-
mcp_content = [TextContent(type="text", text=content)]
|
186
|
-
|
187
|
-
else:
|
188
|
-
mcp_content = [TextContent(type="text", text=content["content"])]
|
189
|
-
|
190
|
-
return mcp_content
|
191
|
-
|
192
|
-
# # TODO: saqadri - this is a best effort conversion, we should handle all possible content types
|
193
|
-
# for c in content["content"]:
|
194
|
-
# # TODO: evalstate, need to go through all scenarios here
|
195
|
-
# if isinstance(c, str):
|
196
|
-
# mcp_content.append(TextContent(type="text", text=c))
|
197
|
-
# break
|
198
25
|
|
199
|
-
|
200
|
-
|
201
|
-
# TextContent(
|
202
|
-
# type="text", text=c.text, **typed_dict_extras(c, ["text"])
|
203
|
-
# )
|
204
|
-
# )
|
205
|
-
# elif (
|
206
|
-
# c.type == "image_url"
|
207
|
-
# ): # isinstance(c, ChatCompletionContentPartImageParam):
|
208
|
-
# raise NotImplementedError("Image content conversion not implemented")
|
209
|
-
# # TODO: saqadri - need to download the image into a base64-encoded string
|
210
|
-
# # Download image from c.image_url
|
211
|
-
# # return ImageContent(
|
212
|
-
# # type="image",
|
213
|
-
# # data=downloaded_image,
|
214
|
-
# # **c
|
215
|
-
# # )
|
216
|
-
# elif (
|
217
|
-
# c.type == "input_audio"
|
218
|
-
# ): # isinstance(c, ChatCompletionContentPartInputAudioParam):
|
219
|
-
# raise NotImplementedError("Audio content conversion not implemented")
|
220
|
-
# elif (
|
221
|
-
# c.type == "refusal"
|
222
|
-
# ): # isinstance(c, ChatCompletionContentPartRefusalParam):
|
223
|
-
# mcp_content.append(
|
224
|
-
# TextContent(
|
225
|
-
# type="text", text=c.refusal, **typed_dict_extras(c, ["refusal"])
|
226
|
-
# )
|
227
|
-
# )
|
228
|
-
# else:
|
229
|
-
# raise ValueError(f"Unexpected content type: {c.type}")
|
26
|
+
# Use the full-featured OpenAI converter for consistent handling
|
27
|
+
return OpenAIConverter.convert_prompt_message_to_openai(message)
|