fast-agent-mcp 0.1.12__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.12.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 +61 -415
- 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 +11 -21
- mcp_agent/agents/workflow/parallel_agent.py +182 -0
- mcp_agent/agents/workflow/router_agent.py +307 -0
- mcp_agent/app.py +15 -19
- mcp_agent/cli/commands/bootstrap.py +19 -38
- mcp_agent/cli/commands/config.py +4 -4
- mcp_agent/cli/commands/setup.py +7 -14
- mcp_agent/cli/main.py +7 -10
- mcp_agent/cli/terminal.py +3 -3
- mcp_agent/config.py +25 -40
- mcp_agent/context.py +12 -21
- mcp_agent/context_dependent.py +3 -5
- 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 +23 -55
- mcp_agent/core/exceptions.py +8 -8
- mcp_agent/core/fastagent.py +145 -371
- mcp_agent/core/interactive_prompt.py +424 -0
- mcp_agent/core/mcp_content.py +17 -17
- mcp_agent/core/prompt.py +6 -9
- mcp_agent/core/request_params.py +6 -3
- mcp_agent/core/validation.py +92 -18
- mcp_agent/executor/decorator_registry.py +9 -17
- mcp_agent/executor/executor.py +8 -17
- mcp_agent/executor/task_registry.py +2 -4
- mcp_agent/executor/temporal.py +19 -41
- mcp_agent/executor/workflow.py +3 -5
- mcp_agent/executor/workflow_signal.py +15 -21
- mcp_agent/human_input/handler.py +4 -7
- mcp_agent/human_input/types.py +2 -3
- mcp_agent/llm/__init__.py +2 -0
- mcp_agent/llm/augmented_llm.py +450 -0
- mcp_agent/llm/augmented_llm_passthrough.py +162 -0
- mcp_agent/llm/augmented_llm_playback.py +83 -0
- mcp_agent/llm/memory.py +103 -0
- mcp_agent/{workflows/llm → llm}/model_factory.py +22 -16
- mcp_agent/{workflows/llm → llm}/prompt_utils.py +1 -3
- mcp_agent/llm/providers/__init__.py +8 -0
- mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +8 -25
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +56 -194
- mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +99 -190
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +72 -71
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +65 -71
- mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +16 -44
- mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +4 -4
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +9 -11
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +8 -12
- mcp_agent/{workflows/llm → llm}/sampling_converter.py +3 -31
- mcp_agent/llm/sampling_format_converter.py +37 -0
- 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 +17 -19
- 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 +1 -3
- mcp_agent/mcp/interfaces.py +117 -110
- mcp_agent/mcp/logger_textio.py +97 -0
- mcp_agent/mcp/mcp_agent_client_session.py +7 -7
- mcp_agent/mcp/mcp_agent_server.py +8 -8
- mcp_agent/mcp/mcp_aggregator.py +102 -143
- mcp_agent/mcp/mcp_connection_manager.py +20 -27
- mcp_agent/mcp/prompt_message_multipart.py +68 -16
- mcp_agent/mcp/prompt_render.py +77 -0
- mcp_agent/mcp/prompt_serialization.py +30 -48
- 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 +109 -0
- mcp_agent/mcp/prompts/prompt_server.py +155 -195
- mcp_agent/mcp/prompts/prompt_template.py +35 -66
- mcp_agent/mcp/resource_utils.py +7 -14
- mcp_agent/mcp/sampling.py +17 -17
- mcp_agent/mcp_server/agent_server.py +13 -17
- mcp_agent/mcp_server_registry.py +13 -22
- mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +3 -2
- mcp_agent/resources/examples/in_dev/slides.py +110 -0
- mcp_agent/resources/examples/internal/agent.py +6 -3
- mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
- 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/prompting/agent.py +2 -1
- mcp_agent/resources/examples/prompting/image_server.py +4 -8
- mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
- mcp_agent/ui/console_display.py +16 -20
- fast_agent_mcp-0.1.12.dist-info/RECORD +0 -161
- mcp_agent/core/agent_app.py +0 -646
- mcp_agent/core/agent_utils.py +0 -71
- mcp_agent/core/decorators.py +0 -455
- mcp_agent/core/factory.py +0 -463
- mcp_agent/core/proxies.py +0 -269
- mcp_agent/core/types.py +0 -24
- mcp_agent/eval/__init__.py +0 -0
- mcp_agent/mcp/stdio.py +0 -111
- 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 -190
- mcp_agent/resources/examples/researcher/researcher.py +0 -38
- mcp_agent/resources/examples/workflows/chaining.py +0 -44
- mcp_agent/resources/examples/workflows/evaluator.py +0 -78
- mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
- mcp_agent/resources/examples/workflows/human_input.py +0 -25
- mcp_agent/resources/examples/workflows/orchestrator.py +0 -73
- mcp_agent/resources/examples/workflows/parallel.py +0 -78
- mcp_agent/resources/examples/workflows/router.py +0 -53
- mcp_agent/resources/examples/workflows/sse.py +0 -23
- mcp_agent/telemetry/__init__.py +0 -0
- mcp_agent/telemetry/usage_tracking.py +0 -18
- mcp_agent/workflows/__init__.py +0 -0
- mcp_agent/workflows/embedding/__init__.py +0 -0
- mcp_agent/workflows/embedding/embedding_base.py +0 -61
- mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
- mcp_agent/workflows/embedding/embedding_openai.py +0 -46
- mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -481
- mcp_agent/workflows/intent_classifier/__init__.py +0 -0
- mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -120
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -134
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -45
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -45
- mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -161
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -60
- mcp_agent/workflows/llm/__init__.py +0 -0
- mcp_agent/workflows/llm/augmented_llm.py +0 -753
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +0 -241
- mcp_agent/workflows/llm/augmented_llm_playback.py +0 -109
- mcp_agent/workflows/llm/providers/__init__.py +0 -8
- mcp_agent/workflows/llm/sampling_format_converter.py +0 -22
- mcp_agent/workflows/orchestrator/__init__.py +0 -0
- mcp_agent/workflows/orchestrator/orchestrator.py +0 -578
- mcp_agent/workflows/parallel/__init__.py +0 -0
- mcp_agent/workflows/parallel/fan_in.py +0 -350
- mcp_agent/workflows/parallel/fan_out.py +0 -187
- mcp_agent/workflows/parallel/parallel_llm.py +0 -166
- mcp_agent/workflows/router/__init__.py +0 -0
- mcp_agent/workflows/router/router_base.py +0 -368
- mcp_agent/workflows/router/router_embedding.py +0 -240
- 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 -320
- mcp_agent/workflows/swarm/__init__.py +0 -0
- mcp_agent/workflows/swarm/swarm.py +0 -320
- mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
- mcp_agent/workflows/swarm/swarm_openai.py +0 -41
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.12.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
@@ -1,36 +1,43 @@
|
|
1
|
-
from typing import List,
|
1
|
+
from typing import List, 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
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
Base64PDFSourceParam,
|
28
|
-
URLPDFSourceParam,
|
29
|
-
PlainTextSourceParam,
|
30
|
-
ToolResultBlockParam,
|
31
|
-
ContentBlockParam,
|
32
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
33
|
+
from mcp_agent.mcp.prompts.prompt_helpers import (
|
34
|
+
get_image_data,
|
35
|
+
get_resource_uri,
|
36
|
+
get_text,
|
37
|
+
is_image_content,
|
38
|
+
is_resource_content,
|
39
|
+
is_text_content,
|
32
40
|
)
|
33
|
-
from mcp_agent.logging.logger import get_logger
|
34
41
|
from mcp_agent.mcp.resource_utils import extract_title_from_uri
|
35
42
|
|
36
43
|
_logger = get_logger("multipart_converter_anthropic")
|
@@ -126,38 +133,39 @@ class AnthropicConverter:
|
|
126
133
|
anthropic_blocks: List[ContentBlockParam] = []
|
127
134
|
|
128
135
|
for content_item in content_items:
|
129
|
-
if
|
130
|
-
|
131
|
-
|
132
|
-
)
|
133
|
-
|
134
|
-
elif
|
136
|
+
if is_text_content(content_item):
|
137
|
+
# Handle text content
|
138
|
+
text = get_text(content_item)
|
139
|
+
anthropic_blocks.append(TextBlockParam(type="text", text=text))
|
140
|
+
|
141
|
+
elif is_image_content(content_item):
|
142
|
+
# Handle image content
|
143
|
+
image_content = content_item # type: ImageContent
|
135
144
|
# Check if image MIME type is supported
|
136
|
-
if not AnthropicConverter._is_supported_image_type(
|
137
|
-
|
138
|
-
):
|
145
|
+
if not AnthropicConverter._is_supported_image_type(image_content.mimeType):
|
146
|
+
data_size = len(image_content.data) if image_content.data else 0
|
139
147
|
anthropic_blocks.append(
|
140
148
|
TextBlockParam(
|
141
149
|
type="text",
|
142
|
-
text=f"Image with unsupported format '{
|
150
|
+
text=f"Image with unsupported format '{image_content.mimeType}' ({data_size} bytes)",
|
143
151
|
)
|
144
152
|
)
|
145
153
|
else:
|
154
|
+
image_data = get_image_data(image_content)
|
146
155
|
anthropic_blocks.append(
|
147
156
|
ImageBlockParam(
|
148
157
|
type="image",
|
149
158
|
source=Base64ImageSourceParam(
|
150
159
|
type="base64",
|
151
|
-
media_type=
|
152
|
-
data=
|
160
|
+
media_type=image_content.mimeType,
|
161
|
+
data=image_data,
|
153
162
|
),
|
154
163
|
)
|
155
164
|
)
|
156
165
|
|
157
|
-
elif
|
158
|
-
|
159
|
-
|
160
|
-
)
|
166
|
+
elif is_resource_content(content_item):
|
167
|
+
# Handle embedded resource
|
168
|
+
block = AnthropicConverter._convert_embedded_resource(content_item, document_mode)
|
161
169
|
anthropic_blocks.append(block)
|
162
170
|
|
163
171
|
return anthropic_blocks
|
@@ -178,7 +186,8 @@ class AnthropicConverter:
|
|
178
186
|
An appropriate ContentBlockParam for the resource
|
179
187
|
"""
|
180
188
|
resource_content = resource.resource
|
181
|
-
|
189
|
+
uri_str = get_resource_uri(resource)
|
190
|
+
uri = getattr(resource_content, "uri", None)
|
182
191
|
is_url: bool = uri and uri.scheme in ("http", "https")
|
183
192
|
|
184
193
|
# Determine MIME type
|
@@ -197,27 +206,29 @@ class AnthropicConverter:
|
|
197
206
|
f"Image with unsupported format '{mime_type}'", resource
|
198
207
|
)
|
199
208
|
|
200
|
-
if is_url:
|
209
|
+
if is_url and uri_str:
|
201
210
|
return ImageBlockParam(
|
202
|
-
type="image", source=URLImageSourceParam(type="url", url=
|
211
|
+
type="image", source=URLImageSourceParam(type="url", url=uri_str)
|
203
212
|
)
|
204
|
-
|
213
|
+
|
214
|
+
# Try to get image data
|
215
|
+
image_data = get_image_data(resource)
|
216
|
+
if image_data:
|
205
217
|
return ImageBlockParam(
|
206
218
|
type="image",
|
207
219
|
source=Base64ImageSourceParam(
|
208
|
-
type="base64", media_type=mime_type, data=
|
220
|
+
type="base64", media_type=mime_type, data=image_data
|
209
221
|
),
|
210
222
|
)
|
211
|
-
|
212
|
-
|
213
|
-
)
|
223
|
+
|
224
|
+
return AnthropicConverter._create_fallback_text("Image missing data", resource)
|
214
225
|
|
215
226
|
elif mime_type == "application/pdf":
|
216
|
-
if is_url:
|
227
|
+
if is_url and uri_str:
|
217
228
|
return DocumentBlockParam(
|
218
229
|
type="document",
|
219
230
|
title=title,
|
220
|
-
source=URLPDFSourceParam(type="url", url=
|
231
|
+
source=URLPDFSourceParam(type="url", url=uri_str),
|
221
232
|
)
|
222
233
|
elif hasattr(resource_content, "blob"):
|
223
234
|
return DocumentBlockParam(
|
@@ -229,12 +240,11 @@ class AnthropicConverter:
|
|
229
240
|
data=resource_content.blob,
|
230
241
|
),
|
231
242
|
)
|
232
|
-
return TextBlockParam(
|
233
|
-
type="text", text=f"[PDF resource missing data: {title}]"
|
234
|
-
)
|
243
|
+
return TextBlockParam(type="text", text=f"[PDF resource missing data: {title}]")
|
235
244
|
|
236
245
|
elif is_text_mime_type(mime_type):
|
237
|
-
|
246
|
+
text = get_text(resource)
|
247
|
+
if not text:
|
238
248
|
return TextBlockParam(
|
239
249
|
type="text",
|
240
250
|
text=f"[Text content could not be extracted from {title}]",
|
@@ -248,16 +258,17 @@ class AnthropicConverter:
|
|
248
258
|
source=PlainTextSourceParam(
|
249
259
|
type="text",
|
250
260
|
media_type="text/plain",
|
251
|
-
data=
|
261
|
+
data=text,
|
252
262
|
),
|
253
263
|
)
|
254
264
|
|
255
265
|
# Return as simple text block when not in document mode
|
256
|
-
return TextBlockParam(type="text", text=
|
266
|
+
return TextBlockParam(type="text", text=text)
|
257
267
|
|
258
268
|
# Default fallback - convert to text if possible
|
259
|
-
|
260
|
-
|
269
|
+
text = get_text(resource)
|
270
|
+
if text:
|
271
|
+
return TextBlockParam(type="text", text=text)
|
261
272
|
|
262
273
|
# This is for binary resources - match the format expected by the test
|
263
274
|
if isinstance(resource.resource, BlobResourceContents) and hasattr(
|
@@ -359,16 +370,12 @@ class AnthropicConverter:
|
|
359
370
|
anthropic_content.append(resource_block)
|
360
371
|
elif isinstance(item, (TextContent, ImageContent)):
|
361
372
|
# For text and image, use standard conversion
|
362
|
-
blocks = AnthropicConverter._convert_content_items(
|
363
|
-
[item], document_mode=False
|
364
|
-
)
|
373
|
+
blocks = AnthropicConverter._convert_content_items([item], document_mode=False)
|
365
374
|
anthropic_content.extend(blocks)
|
366
375
|
|
367
376
|
# If we ended up with no valid content blocks, create a placeholder
|
368
377
|
if not anthropic_content:
|
369
|
-
anthropic_content = [
|
370
|
-
TextBlockParam(type="text", text="[No content in tool result]")
|
371
|
-
]
|
378
|
+
anthropic_content = [TextBlockParam(type="text", text="[No content in tool result]")]
|
372
379
|
|
373
380
|
# Create the tool result block
|
374
381
|
return ToolResultBlockParam(
|
@@ -401,9 +408,7 @@ class AnthropicConverter:
|
|
401
408
|
# Process each content item in the result
|
402
409
|
for item in result.content:
|
403
410
|
if isinstance(item, (TextContent, ImageContent)):
|
404
|
-
blocks = AnthropicConverter._convert_content_items(
|
405
|
-
[item], document_mode=False
|
406
|
-
)
|
411
|
+
blocks = AnthropicConverter._convert_content_items([item], document_mode=False)
|
407
412
|
tool_result_blocks.extend(blocks)
|
408
413
|
elif isinstance(item, EmbeddedResource):
|
409
414
|
resource_content = item.resource
|
@@ -437,11 +442,7 @@ class AnthropicConverter:
|
|
437
442
|
ToolResultBlockParam(
|
438
443
|
type="tool_result",
|
439
444
|
tool_use_id=tool_use_id,
|
440
|
-
content=[
|
441
|
-
TextBlockParam(
|
442
|
-
type="text", text="[No content in tool result]"
|
443
|
-
)
|
444
|
-
],
|
445
|
+
content=[TextBlockParam(type="text", text="[No content in tool result]")],
|
445
446
|
is_error=result.isError,
|
446
447
|
)
|
447
448
|
)
|
@@ -1,23 +1,31 @@
|
|
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,
|
16
|
+
)
|
17
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
18
|
+
from mcp_agent.mcp.prompts.prompt_helpers import (
|
19
|
+
MessageContent,
|
20
|
+
get_image_data,
|
21
|
+
get_resource_uri,
|
22
|
+
get_text,
|
23
|
+
is_image_content,
|
24
|
+
is_resource_content,
|
25
|
+
is_text_content,
|
15
26
|
)
|
16
27
|
from mcp_agent.mcp.resource_utils import extract_title_from_uri
|
17
28
|
|
18
|
-
|
19
|
-
from mcp_agent.logging.logger import get_logger
|
20
|
-
|
21
29
|
_logger = get_logger("multipart_converter_openai")
|
22
30
|
|
23
31
|
# Define type aliases for content blocks
|
@@ -40,9 +48,7 @@ class OpenAIConverter:
|
|
40
48
|
True if the MIME type is generally supported, False otherwise
|
41
49
|
"""
|
42
50
|
return (
|
43
|
-
mime_type is not None
|
44
|
-
and is_image_mime_type(mime_type)
|
45
|
-
and mime_type != "image/svg+xml"
|
51
|
+
mime_type is not None and is_image_mime_type(mime_type) and mime_type != "image/svg+xml"
|
46
52
|
)
|
47
53
|
|
48
54
|
@staticmethod
|
@@ -65,25 +71,10 @@ class OpenAIConverter:
|
|
65
71
|
if not multipart_msg.content:
|
66
72
|
return {"role": role, "content": ""}
|
67
73
|
|
68
|
-
# Assistant messages in OpenAI only support string content, not array of content blocks
|
69
|
-
if role == "assistant":
|
70
|
-
#
|
71
|
-
content_text = ""
|
72
|
-
for item in multipart_msg.content:
|
73
|
-
if isinstance(item, TextContent):
|
74
|
-
content_text += item.text
|
75
|
-
# Other types are ignored for assistant messages in OpenAI
|
76
|
-
|
77
|
-
return {"role": role, "content": content_text}
|
78
|
-
|
79
|
-
# System messages also only support string content
|
80
|
-
if role == "system":
|
81
|
-
# Extract text from all text content blocks
|
82
|
-
content_text = ""
|
83
|
-
for item in multipart_msg.content:
|
84
|
-
if isinstance(item, TextContent):
|
85
|
-
content_text += item.text
|
86
|
-
|
74
|
+
# Assistant and system messages in OpenAI only support string content, not array of content blocks
|
75
|
+
if role == "assistant" or role == "system":
|
76
|
+
# Use MessageContent helper to get all text
|
77
|
+
content_text = MessageContent.join_text(multipart_msg, separator="")
|
87
78
|
return {"role": role, "content": content_text}
|
88
79
|
|
89
80
|
# For user messages, convert each content block
|
@@ -91,22 +82,21 @@ class OpenAIConverter:
|
|
91
82
|
|
92
83
|
for item in multipart_msg.content:
|
93
84
|
try:
|
94
|
-
if
|
95
|
-
|
85
|
+
if is_text_content(item):
|
86
|
+
text = get_text(item)
|
87
|
+
content_blocks.append({"type": "text", "text": text})
|
96
88
|
|
97
|
-
elif
|
89
|
+
elif is_image_content(item):
|
98
90
|
content_blocks.append(OpenAIConverter._convert_image_content(item))
|
99
91
|
|
100
|
-
elif
|
92
|
+
elif is_resource_content(item):
|
101
93
|
block = OpenAIConverter._convert_embedded_resource(item)
|
102
94
|
if block:
|
103
95
|
content_blocks.append(block)
|
104
96
|
|
105
97
|
# Handle input_audio if implemented
|
106
98
|
elif hasattr(item, "type") and getattr(item, "type") == "input_audio":
|
107
|
-
_logger.warning(
|
108
|
-
"Input audio content not supported in standard OpenAI types"
|
109
|
-
)
|
99
|
+
_logger.warning("Input audio content not supported in standard OpenAI types")
|
110
100
|
fallback_text = "[Audio content not directly supported]"
|
111
101
|
content_blocks.append({"type": "text", "text": fallback_text})
|
112
102
|
|
@@ -202,8 +192,11 @@ class OpenAIConverter:
|
|
202
192
|
@staticmethod
|
203
193
|
def _convert_image_content(content: ImageContent) -> ContentBlock:
|
204
194
|
"""Convert ImageContent to OpenAI image_url content block."""
|
195
|
+
# Get image data using helper
|
196
|
+
image_data = get_image_data(content)
|
197
|
+
|
205
198
|
# OpenAI requires image URLs or data URIs for images
|
206
|
-
image_url = {"url": f"data:{content.mimeType};base64,{
|
199
|
+
image_url = {"url": f"data:{content.mimeType};base64,{image_data}"}
|
207
200
|
|
208
201
|
# Check if the image has annotations for detail level
|
209
202
|
if hasattr(content, "annotations") and content.annotations:
|
@@ -251,6 +244,7 @@ class OpenAIConverter:
|
|
251
244
|
An appropriate OpenAI content block or None if conversion failed
|
252
245
|
"""
|
253
246
|
resource_content = resource.resource
|
247
|
+
uri_str = get_resource_uri(resource)
|
254
248
|
uri = getattr(resource_content, "uri", None)
|
255
249
|
is_url = uri and str(uri).startswith(("http://", "https://"))
|
256
250
|
title = extract_title_from_uri(uri) if uri else "resource"
|
@@ -260,25 +254,26 @@ class OpenAIConverter:
|
|
260
254
|
|
261
255
|
# Handle images
|
262
256
|
if OpenAIConverter._is_supported_image_type(mime_type):
|
263
|
-
if is_url:
|
264
|
-
return {"type": "image_url", "image_url": {"url":
|
265
|
-
|
257
|
+
if is_url and uri_str:
|
258
|
+
return {"type": "image_url", "image_url": {"url": uri_str}}
|
259
|
+
|
260
|
+
# Try to get image data
|
261
|
+
image_data = get_image_data(resource)
|
262
|
+
if image_data:
|
266
263
|
return {
|
267
264
|
"type": "image_url",
|
268
|
-
"image_url": {
|
269
|
-
"url": f"data:{mime_type};base64,{resource_content.blob}"
|
270
|
-
},
|
265
|
+
"image_url": {"url": f"data:{mime_type};base64,{image_data}"},
|
271
266
|
}
|
272
267
|
else:
|
273
268
|
return {"type": "text", "text": f"[Image missing data: {title}]"}
|
274
269
|
|
275
270
|
# Handle PDFs
|
276
271
|
elif mime_type == "application/pdf":
|
277
|
-
if is_url:
|
272
|
+
if is_url and uri_str:
|
278
273
|
# OpenAI doesn't directly support PDF URLs, explain this limitation
|
279
274
|
return {
|
280
275
|
"type": "text",
|
281
|
-
"text": f"[PDF URL: {
|
276
|
+
"text": f"[PDF URL: {uri_str}]\nOpenAI requires PDF files to be uploaded or provided as base64 data.",
|
282
277
|
}
|
283
278
|
elif hasattr(resource_content, "blob"):
|
284
279
|
return {
|
@@ -290,26 +285,31 @@ class OpenAIConverter:
|
|
290
285
|
}
|
291
286
|
|
292
287
|
# Handle SVG (convert to text)
|
293
|
-
elif mime_type == "image/svg+xml"
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
288
|
+
elif mime_type == "image/svg+xml":
|
289
|
+
text = get_text(resource)
|
290
|
+
if text:
|
291
|
+
file_text = (
|
292
|
+
f'<fastagent:file title="{title}" mimetype="{mime_type}">\n'
|
293
|
+
f"{text}\n"
|
294
|
+
f"</fastagent:file>"
|
295
|
+
)
|
296
|
+
return {"type": "text", "text": file_text}
|
300
297
|
|
301
298
|
# Handle text files
|
302
|
-
elif is_text_mime_type(mime_type)
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
299
|
+
elif is_text_mime_type(mime_type):
|
300
|
+
text = get_text(resource)
|
301
|
+
if text:
|
302
|
+
file_text = (
|
303
|
+
f'<fastagent:file title="{title}" mimetype="{mime_type}">\n'
|
304
|
+
f"{text}\n"
|
305
|
+
f"</fastagent:file>"
|
306
|
+
)
|
307
|
+
return {"type": "text", "text": file_text}
|
309
308
|
|
310
309
|
# Default fallback for text resources
|
311
|
-
|
312
|
-
|
310
|
+
text = get_text(resource)
|
311
|
+
if text:
|
312
|
+
return {"type": "text", "text": text}
|
313
313
|
|
314
314
|
# Default fallback for binary resources
|
315
315
|
elif hasattr(resource_content, "blob"):
|
@@ -349,11 +349,7 @@ class OpenAIConverter:
|
|
349
349
|
if block.get("type") == "text":
|
350
350
|
text_parts.append(block.get("text", ""))
|
351
351
|
|
352
|
-
return (
|
353
|
-
" ".join(text_parts)
|
354
|
-
if text_parts
|
355
|
-
else "[Complex content converted to text]"
|
356
|
-
)
|
352
|
+
return " ".join(text_parts) if text_parts else "[Complex content converted to text]"
|
357
353
|
|
358
354
|
@staticmethod
|
359
355
|
def convert_tool_result_to_openai(
|
@@ -423,9 +419,7 @@ class OpenAIConverter:
|
|
423
419
|
return tool_message
|
424
420
|
|
425
421
|
# Process non-text content as a separate user message
|
426
|
-
non_text_multipart = PromptMessageMultipart(
|
427
|
-
role="user", content=non_text_content
|
428
|
-
)
|
422
|
+
non_text_multipart = PromptMessageMultipart(role="user", content=non_text_content)
|
429
423
|
|
430
424
|
# Convert to OpenAI format
|
431
425
|
user_message = OpenAIConverter.convert_to_openai(non_text_multipart)
|
@@ -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,27 +63,16 @@ 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
73
|
if (
|
83
74
|
text
|
84
|
-
and (
|
85
|
-
text.startswith("[Resource:")
|
86
|
-
or text.startswith("[Binary Resource:")
|
87
|
-
)
|
75
|
+
and (text.startswith("[Resource:") or text.startswith("[Binary Resource:"))
|
88
76
|
and "\n" in text
|
89
77
|
):
|
90
78
|
header, content_text = text.split("\n", 1)
|
@@ -93,15 +81,8 @@ def _openai_message_to_multipart(
|
|
93
81
|
|
94
82
|
# If not text/plain, create an embedded resource
|
95
83
|
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
|
-
)
|
84
|
+
if "Resource:" in header and "Binary Resource:" not in header:
|
85
|
+
uri = header.split("Resource:", 1)[1].split(",")[0].strip()
|
105
86
|
mcp_contents.append(
|
106
87
|
EmbeddedResource(
|
107
88
|
type="resource",
|
@@ -139,11 +120,7 @@ def _openai_message_to_multipart(
|
|
139
120
|
)
|
140
121
|
|
141
122
|
# Handle explicit resource types
|
142
|
-
elif (
|
143
|
-
part_type == "resource"
|
144
|
-
and isinstance(part, dict)
|
145
|
-
and "resource" in part
|
146
|
-
):
|
123
|
+
elif part_type == "resource" and isinstance(part, dict) and "resource" in part:
|
147
124
|
resource = part["resource"]
|
148
125
|
if isinstance(resource, dict):
|
149
126
|
# Text resource
|
@@ -152,9 +129,7 @@ def _openai_message_to_multipart(
|
|
152
129
|
uri = resource.get("uri", "resource://unknown")
|
153
130
|
|
154
131
|
if mime_type == "text/plain":
|
155
|
-
mcp_contents.append(
|
156
|
-
TextContent(type="text", text=resource["text"])
|
157
|
-
)
|
132
|
+
mcp_contents.append(TextContent(type="text", text=resource["text"]))
|
158
133
|
else:
|
159
134
|
mcp_contents.append(
|
160
135
|
EmbeddedResource(
|
@@ -171,10 +146,7 @@ def _openai_message_to_multipart(
|
|
171
146
|
mime_type = resource["mimeType"]
|
172
147
|
uri = resource.get("uri", "resource://unknown")
|
173
148
|
|
174
|
-
if (
|
175
|
-
mime_type.startswith("image/")
|
176
|
-
and mime_type != "image/svg+xml"
|
177
|
-
):
|
149
|
+
if mime_type.startswith("image/") and mime_type != "image/svg+xml":
|
178
150
|
mcp_contents.append(
|
179
151
|
ImageContent(
|
180
152
|
type="image",
|
@@ -5,18 +5,18 @@ 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,
|
12
12
|
ChatCompletionMessageParam,
|
13
13
|
)
|
14
14
|
|
15
|
-
from mcp_agent.
|
16
|
-
from mcp_agent.
|
17
|
-
from mcp_agent.workflows.llm.providers.openai_multipart import (
|
15
|
+
from mcp_agent.llm.providers.multipart_converter_openai import OpenAIConverter
|
16
|
+
from mcp_agent.llm.providers.openai_multipart import (
|
18
17
|
openai_to_multipart,
|
19
18
|
)
|
19
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
20
20
|
|
21
21
|
|
22
22
|
def openai_message_to_prompt_message_multipart(
|
@@ -1,24 +1,22 @@
|
|
1
|
-
from
|
2
|
-
|
3
|
-
|
1
|
+
from anthropic.types import (
|
2
|
+
Message,
|
3
|
+
MessageParam,
|
4
4
|
)
|
5
|
-
from
|
6
|
-
|
5
|
+
from mcp import StopReason
|
7
6
|
from mcp.types import (
|
8
7
|
PromptMessage,
|
9
8
|
)
|
10
9
|
|
11
|
-
from
|
12
|
-
|
13
|
-
MessageParam,
|
10
|
+
from mcp_agent.llm.providers.multipart_converter_anthropic import (
|
11
|
+
AnthropicConverter,
|
14
12
|
)
|
15
|
-
|
13
|
+
from mcp_agent.llm.sampling_format_converter import ProviderFormatConverter
|
16
14
|
from mcp_agent.logging.logger import get_logger
|
17
15
|
|
18
16
|
_logger = get_logger(__name__)
|
19
17
|
|
20
18
|
|
21
|
-
class AnthropicSamplingConverter(
|
19
|
+
class AnthropicSamplingConverter(ProviderFormatConverter[MessageParam, Message]):
|
22
20
|
"""
|
23
21
|
Convert between Anthropic and MCP types.
|
24
22
|
"""
|
@@ -46,7 +44,7 @@ def mcp_stop_reason_to_anthropic_stop_reason(stop_reason: StopReason):
|
|
46
44
|
|
47
45
|
def anthropic_stop_reason_to_mcp_stop_reason(stop_reason: str) -> StopReason:
|
48
46
|
if not stop_reason:
|
49
|
-
return
|
47
|
+
return "end_turn"
|
50
48
|
elif stop_reason == "end_turn":
|
51
49
|
return "endTurn"
|
52
50
|
elif stop_reason == "max_tokens":
|