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
@@ -1,25 +1,26 @@
|
|
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,
|
7
|
+
PromptMessage,
|
8
|
+
TextContent,
|
8
9
|
)
|
9
|
-
|
10
|
+
|
11
|
+
from mcp_agent.logging.logger import get_logger
|
10
12
|
from mcp_agent.mcp.mime_utils import (
|
11
13
|
guess_mime_type,
|
12
|
-
is_text_mime_type,
|
13
14
|
is_image_mime_type,
|
15
|
+
is_text_mime_type,
|
14
16
|
)
|
17
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
15
18
|
from mcp_agent.mcp.resource_utils import extract_title_from_uri
|
16
19
|
|
17
|
-
from mcp_agent.logging.logger import get_logger
|
18
|
-
|
19
20
|
_logger = get_logger("multipart_converter_openai")
|
20
21
|
|
21
|
-
# Define
|
22
|
-
|
22
|
+
# Define type aliases for content blocks
|
23
|
+
ContentBlock = Dict[str, Any]
|
23
24
|
OpenAIMessage = Dict[str, Any]
|
24
25
|
|
25
26
|
|
@@ -27,9 +28,20 @@ class OpenAIConverter:
|
|
27
28
|
"""Converts MCP message types to OpenAI API format."""
|
28
29
|
|
29
30
|
@staticmethod
|
30
|
-
def
|
31
|
-
|
32
|
-
|
31
|
+
def _is_supported_image_type(mime_type: str) -> bool:
|
32
|
+
"""
|
33
|
+
Check if the given MIME type is supported by OpenAI's image API.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
mime_type: The MIME type to check
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
True if the MIME type is generally supported, False otherwise
|
40
|
+
"""
|
41
|
+
return mime_type is not None and is_image_mime_type(mime_type) and mime_type != "image/svg+xml"
|
42
|
+
|
43
|
+
@staticmethod
|
44
|
+
def convert_to_openai(multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False) -> OpenAIMessage:
|
33
45
|
"""
|
34
46
|
Convert a PromptMessageMultipart message to OpenAI API format.
|
35
47
|
|
@@ -57,13 +69,23 @@ class OpenAIConverter:
|
|
57
69
|
|
58
70
|
return {"role": role, "content": content_text}
|
59
71
|
|
72
|
+
# System messages also only support string content
|
73
|
+
if role == "system":
|
74
|
+
# Extract text from all text content blocks
|
75
|
+
content_text = ""
|
76
|
+
for item in multipart_msg.content:
|
77
|
+
if isinstance(item, TextContent):
|
78
|
+
content_text += item.text
|
79
|
+
|
80
|
+
return {"role": role, "content": content_text}
|
81
|
+
|
60
82
|
# For user messages, convert each content block
|
61
|
-
content_blocks = []
|
83
|
+
content_blocks: List[ContentBlock] = []
|
62
84
|
|
63
85
|
for item in multipart_msg.content:
|
64
86
|
try:
|
65
87
|
if isinstance(item, TextContent):
|
66
|
-
content_blocks.append(
|
88
|
+
content_blocks.append({"type": "text", "text": item.text})
|
67
89
|
|
68
90
|
elif isinstance(item, ImageContent):
|
69
91
|
content_blocks.append(OpenAIConverter._convert_image_content(item))
|
@@ -75,23 +97,9 @@ class OpenAIConverter:
|
|
75
97
|
|
76
98
|
# Handle input_audio if implemented
|
77
99
|
elif hasattr(item, "type") and getattr(item, "type") == "input_audio":
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
{
|
82
|
-
"type": "input_audio",
|
83
|
-
"input_audio": {
|
84
|
-
"data": item.input_audio.get("data", ""),
|
85
|
-
"format": item.input_audio.get("format", "wav"),
|
86
|
-
},
|
87
|
-
}
|
88
|
-
)
|
89
|
-
else:
|
90
|
-
_logger.warning(
|
91
|
-
"InputAudio content missing input_audio attribute"
|
92
|
-
)
|
93
|
-
fallback_text = "[Audio content missing data]"
|
94
|
-
content_blocks.append({"type": "text", "text": fallback_text})
|
100
|
+
_logger.warning("Input audio content not supported in standard OpenAI types")
|
101
|
+
fallback_text = "[Audio content not directly supported]"
|
102
|
+
content_blocks.append({"type": "text", "text": fallback_text})
|
95
103
|
|
96
104
|
else:
|
97
105
|
_logger.warning(f"Unsupported content type: {type(item)}")
|
@@ -110,55 +118,79 @@ class OpenAIConverter:
|
|
110
118
|
return {"role": role, "content": ""}
|
111
119
|
|
112
120
|
# If we only have one text content and it's empty, return an empty string for content
|
113
|
-
if (
|
114
|
-
len(content_blocks) == 1
|
115
|
-
and content_blocks[0]["type"] == "text"
|
116
|
-
and not content_blocks[0]["text"]
|
117
|
-
):
|
121
|
+
if len(content_blocks) == 1 and content_blocks[0]["type"] == "text" and not content_blocks[0]["text"]:
|
118
122
|
return {"role": role, "content": ""}
|
119
123
|
|
120
124
|
# If concatenate_text_blocks is True, combine adjacent text blocks
|
121
125
|
if concatenate_text_blocks:
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
if block["type"] == "text":
|
127
|
-
# Add to current text accumulator
|
128
|
-
if current_text:
|
129
|
-
current_text += " " + block["text"]
|
130
|
-
else:
|
131
|
-
current_text = block["text"]
|
132
|
-
else:
|
133
|
-
# Non-text block found, flush accumulated text if any
|
134
|
-
if current_text:
|
135
|
-
combined_blocks.append({"type": "text", "text": current_text})
|
136
|
-
current_text = ""
|
137
|
-
# Add the non-text block
|
138
|
-
combined_blocks.append(block)
|
126
|
+
content_blocks = OpenAIConverter._concatenate_text_blocks(content_blocks)
|
127
|
+
|
128
|
+
# Return user message with content blocks
|
129
|
+
return {"role": role, "content": content_blocks}
|
139
130
|
|
140
|
-
|
141
|
-
|
142
|
-
|
131
|
+
@staticmethod
|
132
|
+
def _concatenate_text_blocks(blocks: List[ContentBlock]) -> List[ContentBlock]:
|
133
|
+
"""
|
134
|
+
Combine adjacent text blocks into single blocks.
|
143
135
|
|
144
|
-
|
136
|
+
Args:
|
137
|
+
blocks: List of content blocks
|
145
138
|
|
146
|
-
|
139
|
+
Returns:
|
140
|
+
List with adjacent text blocks combined
|
141
|
+
"""
|
142
|
+
if not blocks:
|
143
|
+
return []
|
144
|
+
|
145
|
+
combined_blocks: List[ContentBlock] = []
|
146
|
+
current_text = ""
|
147
|
+
|
148
|
+
for block in blocks:
|
149
|
+
if block["type"] == "text":
|
150
|
+
# Add to current text accumulator
|
151
|
+
if current_text:
|
152
|
+
current_text += " " + block["text"]
|
153
|
+
else:
|
154
|
+
current_text = block["text"]
|
155
|
+
else:
|
156
|
+
# Non-text block found, flush accumulated text if any
|
157
|
+
if current_text:
|
158
|
+
combined_blocks.append({"type": "text", "text": current_text})
|
159
|
+
current_text = ""
|
160
|
+
# Add the non-text block
|
161
|
+
combined_blocks.append(block)
|
162
|
+
|
163
|
+
# Don't forget any remaining text
|
164
|
+
if current_text:
|
165
|
+
combined_blocks.append({"type": "text", "text": current_text})
|
166
|
+
|
167
|
+
return combined_blocks
|
147
168
|
|
148
169
|
@staticmethod
|
149
|
-
def
|
150
|
-
"""
|
151
|
-
|
170
|
+
def convert_prompt_message_to_openai(message: PromptMessage, concatenate_text_blocks: bool = False) -> OpenAIMessage:
|
171
|
+
"""
|
172
|
+
Convert a standard PromptMessage to OpenAI API format.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
message: The PromptMessage to convert
|
176
|
+
concatenate_text_blocks: If True, adjacent text blocks will be combined
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
An OpenAI API message object
|
180
|
+
"""
|
181
|
+
# Convert the PromptMessage to a PromptMessageMultipart containing a single content item
|
182
|
+
multipart = PromptMessageMultipart(role=message.role, content=[message.content])
|
183
|
+
|
184
|
+
# Use the existing conversion method with the specified concatenation option
|
185
|
+
return OpenAIConverter.convert_to_openai(multipart, concatenate_text_blocks)
|
152
186
|
|
153
187
|
@staticmethod
|
154
|
-
def _convert_image_content(content: ImageContent) ->
|
188
|
+
def _convert_image_content(content: ImageContent) -> ContentBlock:
|
155
189
|
"""Convert ImageContent to OpenAI image_url content block."""
|
156
190
|
# OpenAI requires image URLs or data URIs for images
|
157
191
|
image_url = {"url": f"data:{content.mimeType};base64,{content.data}"}
|
158
192
|
|
159
193
|
# Check if the image has annotations for detail level
|
160
|
-
# This would depend on your ImageContent implementation
|
161
|
-
# If annotations are available, use them for the detail parameter
|
162
194
|
if hasattr(content, "annotations") and content.annotations:
|
163
195
|
if hasattr(content.annotations, "detail"):
|
164
196
|
detail = content.annotations.detail
|
@@ -167,52 +199,70 @@ class OpenAIConverter:
|
|
167
199
|
|
168
200
|
return {"type": "image_url", "image_url": image_url}
|
169
201
|
|
202
|
+
@staticmethod
|
203
|
+
def _determine_mime_type(resource_content) -> str:
|
204
|
+
"""
|
205
|
+
Determine the MIME type of a resource.
|
206
|
+
|
207
|
+
Args:
|
208
|
+
resource_content: The resource content to check
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
The determined MIME type as a string
|
212
|
+
"""
|
213
|
+
if hasattr(resource_content, "mimeType") and resource_content.mimeType:
|
214
|
+
return resource_content.mimeType
|
215
|
+
|
216
|
+
if hasattr(resource_content, "uri") and resource_content.uri:
|
217
|
+
mime_type = guess_mime_type(str(resource_content.uri))
|
218
|
+
return mime_type
|
219
|
+
|
220
|
+
if hasattr(resource_content, "blob"):
|
221
|
+
return "application/octet-stream"
|
222
|
+
|
223
|
+
return "text/plain"
|
224
|
+
|
170
225
|
@staticmethod
|
171
226
|
def _convert_embedded_resource(
|
172
227
|
resource: EmbeddedResource,
|
173
|
-
) -> Optional[
|
174
|
-
"""
|
175
|
-
|
176
|
-
uri = resource_content.uri
|
228
|
+
) -> Optional[ContentBlock]:
|
229
|
+
"""
|
230
|
+
Convert EmbeddedResource to appropriate OpenAI content block.
|
177
231
|
|
178
|
-
|
179
|
-
|
180
|
-
mime_type = guess_mime_type(str(uri))
|
181
|
-
_logger.info(f"MIME type not provided, guessed {mime_type} for {uri}")
|
182
|
-
else:
|
183
|
-
mime_type = resource_content.mimeType or "application/octet-stream"
|
232
|
+
Args:
|
233
|
+
resource: The embedded resource to convert
|
184
234
|
|
185
|
-
|
235
|
+
Returns:
|
236
|
+
An appropriate OpenAI content block or None if conversion failed
|
237
|
+
"""
|
238
|
+
resource_content = resource.resource
|
239
|
+
uri = getattr(resource_content, "uri", None)
|
240
|
+
is_url = uri and str(uri).startswith(("http://", "https://"))
|
186
241
|
title = extract_title_from_uri(uri) if uri else "resource"
|
242
|
+
mime_type = OpenAIConverter._determine_mime_type(resource_content)
|
187
243
|
|
188
|
-
# Handle
|
189
|
-
if is_image_mime_type(mime_type) and mime_type != "image/svg+xml":
|
190
|
-
image_url = {}
|
244
|
+
# Handle different resource types based on MIME type
|
191
245
|
|
246
|
+
# Handle images
|
247
|
+
if OpenAIConverter._is_supported_image_type(mime_type):
|
192
248
|
if is_url:
|
193
|
-
image_url
|
249
|
+
return {"type": "image_url", "image_url": {"url": str(uri)}}
|
194
250
|
elif hasattr(resource_content, "blob"):
|
195
|
-
|
251
|
+
return {
|
252
|
+
"type": "image_url",
|
253
|
+
"image_url": {"url": f"data:{mime_type};base64,{resource_content.blob}"},
|
254
|
+
}
|
196
255
|
else:
|
197
|
-
_logger.warning(f"Image resource missing both URL and blob data: {uri}")
|
198
256
|
return {"type": "text", "text": f"[Image missing data: {title}]"}
|
199
257
|
|
200
|
-
|
201
|
-
if hasattr(resource, "annotations") and resource.annotations:
|
202
|
-
if hasattr(resource.annotations, "detail"):
|
203
|
-
detail = resource.annotations.detail
|
204
|
-
if detail in ("auto", "low", "high"):
|
205
|
-
image_url["detail"] = detail
|
206
|
-
|
207
|
-
return {"type": "image_url", "image_url": image_url}
|
208
|
-
|
209
|
-
# Handle PDF resources - OpenAI has specific file format for PDFs
|
258
|
+
# Handle PDFs
|
210
259
|
elif mime_type == "application/pdf":
|
211
260
|
if is_url:
|
212
|
-
# OpenAI doesn't directly support PDF URLs,
|
213
|
-
|
214
|
-
|
215
|
-
|
261
|
+
# OpenAI doesn't directly support PDF URLs, explain this limitation
|
262
|
+
return {
|
263
|
+
"type": "text",
|
264
|
+
"text": f"[PDF URL: {uri}]\nOpenAI requires PDF files to be uploaded or provided as base64 data.",
|
265
|
+
}
|
216
266
|
elif hasattr(resource_content, "blob"):
|
217
267
|
return {
|
218
268
|
"type": "file",
|
@@ -222,65 +272,70 @@ class OpenAIConverter:
|
|
222
272
|
},
|
223
273
|
}
|
224
274
|
|
225
|
-
# Handle SVG
|
226
|
-
elif mime_type == "image/svg+xml":
|
227
|
-
|
228
|
-
|
229
|
-
f'<fastagent:file title="{title}" mimetype="{mime_type}">\n'
|
230
|
-
f"{resource_content.text}\n"
|
231
|
-
f"</fastagent:file>"
|
232
|
-
)
|
233
|
-
return {"type": "text", "text": file_text}
|
234
|
-
|
235
|
-
# Handle text resources with fastagent:file tags
|
236
|
-
elif is_text_mime_type(mime_type):
|
237
|
-
if hasattr(resource_content, "text"):
|
238
|
-
# Wrap in fastagent:file tags for text resources
|
239
|
-
file_text = (
|
240
|
-
f'<fastagent:file title="{title}" mimetype="{mime_type}">\n'
|
241
|
-
f"{resource_content.text}\n"
|
242
|
-
f"</fastagent:file>"
|
243
|
-
)
|
244
|
-
return {"type": "text", "text": file_text}
|
245
|
-
|
246
|
-
# Handle other binary formats that OpenAI supports with file type
|
247
|
-
# Currently, OpenAI supports PDFs for comprehensive viewing, but we can try
|
248
|
-
# to use the file type for other binary formats as well for future compatibility
|
249
|
-
elif hasattr(resource_content, "blob"):
|
250
|
-
# For now, we'll use file type for PDFs only, and use fallback for others
|
251
|
-
if mime_type == "application/pdf":
|
252
|
-
return {
|
253
|
-
"type": "file",
|
254
|
-
"file": {"file_name": title, "file_data": resource_content.blob},
|
255
|
-
}
|
256
|
-
else:
|
257
|
-
# For other binary formats, create a text message mentioning the resource
|
258
|
-
return {
|
259
|
-
"type": "text",
|
260
|
-
"text": f"[Binary resource: {title} ({mime_type})]",
|
261
|
-
}
|
275
|
+
# Handle SVG (convert to text)
|
276
|
+
elif mime_type == "image/svg+xml" and hasattr(resource_content, "text"):
|
277
|
+
file_text = f'<fastagent:file title="{title}" mimetype="{mime_type}">\n' f"{resource_content.text}\n" f"</fastagent:file>"
|
278
|
+
return {"type": "text", "text": file_text}
|
262
279
|
|
263
|
-
#
|
264
|
-
|
265
|
-
|
266
|
-
|
280
|
+
# Handle text files
|
281
|
+
elif is_text_mime_type(mime_type) and hasattr(resource_content, "text"):
|
282
|
+
file_text = f'<fastagent:file title="{title}" mimetype="{mime_type}">\n' f"{resource_content.text}\n" f"</fastagent:file>"
|
283
|
+
return {"type": "text", "text": file_text}
|
284
|
+
|
285
|
+
# Default fallback for text resources
|
286
|
+
elif hasattr(resource_content, "text"):
|
267
287
|
return {"type": "text", "text": resource_content.text}
|
268
288
|
|
269
|
-
|
289
|
+
# Default fallback for binary resources
|
290
|
+
elif hasattr(resource_content, "blob"):
|
291
|
+
return {
|
292
|
+
"type": "text",
|
293
|
+
"text": f"[Binary resource: {title} ({mime_type})]",
|
294
|
+
}
|
295
|
+
|
296
|
+
# Last resort fallback
|
270
297
|
return {
|
271
298
|
"type": "text",
|
272
299
|
"text": f"[Unsupported resource: {title} ({mime_type})]",
|
273
300
|
}
|
274
301
|
|
302
|
+
@staticmethod
|
303
|
+
def _extract_text_from_content_blocks(
|
304
|
+
content: Union[str, List[ContentBlock]],
|
305
|
+
) -> str:
|
306
|
+
"""
|
307
|
+
Extract and combine text from content blocks.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
content: Content blocks or string
|
311
|
+
|
312
|
+
Returns:
|
313
|
+
Combined text as a string
|
314
|
+
"""
|
315
|
+
if isinstance(content, str):
|
316
|
+
return content
|
317
|
+
|
318
|
+
if not content:
|
319
|
+
return ""
|
320
|
+
|
321
|
+
# Extract only text blocks
|
322
|
+
text_parts = []
|
323
|
+
for block in content:
|
324
|
+
if block.get("type") == "text":
|
325
|
+
text_parts.append(block.get("text", ""))
|
326
|
+
|
327
|
+
return " ".join(text_parts) if text_parts else "[Complex content converted to text]"
|
328
|
+
|
275
329
|
@staticmethod
|
276
330
|
def convert_tool_result_to_openai(
|
277
331
|
tool_result: CallToolResult,
|
278
332
|
tool_call_id: str,
|
279
333
|
concatenate_text_blocks: bool = False,
|
280
|
-
) -> Union[
|
334
|
+
) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[Dict[str, Any]]]]:
|
281
335
|
"""
|
282
336
|
Convert a CallToolResult to an OpenAI tool message.
|
283
|
-
|
337
|
+
|
338
|
+
If the result contains non-text elements, those are converted to separate user messages
|
284
339
|
since OpenAI tool messages can only contain text.
|
285
340
|
|
286
341
|
Args:
|
@@ -300,7 +355,7 @@ class OpenAIConverter:
|
|
300
355
|
"content": "[No content in tool result]",
|
301
356
|
}
|
302
357
|
|
303
|
-
#
|
358
|
+
# Separate text and non-text content
|
304
359
|
text_content = []
|
305
360
|
non_text_content = []
|
306
361
|
|
@@ -310,57 +365,15 @@ class OpenAIConverter:
|
|
310
365
|
else:
|
311
366
|
non_text_content.append(item)
|
312
367
|
|
313
|
-
#
|
314
|
-
if not non_text_content:
|
315
|
-
# Create a temporary PromptMessageMultipart to reuse the conversion logic
|
316
|
-
temp_multipart = PromptMessageMultipart(role="user", content=text_content)
|
317
|
-
|
318
|
-
# Convert using the same logic as user messages
|
319
|
-
converted = OpenAIConverter.convert_to_openai(
|
320
|
-
temp_multipart, concatenate_text_blocks=concatenate_text_blocks
|
321
|
-
)
|
322
|
-
|
323
|
-
# For tool messages, we need to extract and combine all text content
|
324
|
-
if isinstance(converted["content"], str):
|
325
|
-
content = converted["content"]
|
326
|
-
else:
|
327
|
-
# For compatibility with OpenAI's tool message format, combine all text blocks
|
328
|
-
all_text = all(
|
329
|
-
block.get("type") == "text" for block in converted["content"]
|
330
|
-
)
|
331
|
-
|
332
|
-
if all_text and len(converted["content"]) > 0:
|
333
|
-
# Combine all text blocks
|
334
|
-
content = " ".join(
|
335
|
-
block.get("text", "") for block in converted["content"]
|
336
|
-
)
|
337
|
-
else:
|
338
|
-
# Fallback for unexpected cases
|
339
|
-
content = "[Complex content converted to text]"
|
340
|
-
|
341
|
-
# Create a tool message with the converted content
|
342
|
-
return {"role": "tool", "tool_call_id": tool_call_id, "content": content}
|
343
|
-
|
344
|
-
# If we have mixed content or only non-text content
|
345
|
-
|
346
|
-
# Process text content for the tool message
|
368
|
+
# Create tool message with text content
|
347
369
|
tool_message_content = ""
|
348
370
|
if text_content:
|
371
|
+
# Convert text content to OpenAI format
|
349
372
|
temp_multipart = PromptMessageMultipart(role="user", content=text_content)
|
350
|
-
converted = OpenAIConverter.convert_to_openai(
|
351
|
-
temp_multipart, concatenate_text_blocks=True
|
352
|
-
)
|
373
|
+
converted = OpenAIConverter.convert_to_openai(temp_multipart, concatenate_text_blocks=concatenate_text_blocks)
|
353
374
|
|
354
|
-
|
355
|
-
|
356
|
-
else:
|
357
|
-
# Combine all text blocks
|
358
|
-
all_text = [
|
359
|
-
block.get("text", "")
|
360
|
-
for block in converted["content"]
|
361
|
-
if block.get("type") == "text"
|
362
|
-
]
|
363
|
-
tool_message_content = " ".join(all_text)
|
375
|
+
# Extract text from content blocks
|
376
|
+
tool_message_content = OpenAIConverter._extract_text_from_content_blocks(converted.get("content", ""))
|
364
377
|
|
365
378
|
if not tool_message_content:
|
366
379
|
tool_message_content = "[Tool returned non-text content]"
|
@@ -372,31 +385,28 @@ class OpenAIConverter:
|
|
372
385
|
"content": tool_message_content,
|
373
386
|
}
|
374
387
|
|
388
|
+
# If there's no non-text content, return just the tool message
|
389
|
+
if not non_text_content:
|
390
|
+
return tool_message
|
391
|
+
|
375
392
|
# Process non-text content as a separate user message
|
376
|
-
|
377
|
-
# Create a multipart message with the non-text content
|
378
|
-
non_text_multipart = PromptMessageMultipart(
|
379
|
-
role="user", content=non_text_content
|
380
|
-
)
|
393
|
+
non_text_multipart = PromptMessageMultipart(role="user", content=non_text_content)
|
381
394
|
|
382
|
-
|
383
|
-
|
384
|
-
# Add tool_call_id to associate with the tool call
|
385
|
-
user_message["tool_call_id"] = tool_call_id
|
395
|
+
# Convert to OpenAI format
|
396
|
+
user_message = OpenAIConverter.convert_to_openai(non_text_multipart)
|
386
397
|
|
387
|
-
|
398
|
+
# We need to add tool_call_id manually
|
399
|
+
user_message["tool_call_id"] = tool_call_id
|
388
400
|
|
389
|
-
return tool_message
|
401
|
+
return (tool_message, [user_message])
|
390
402
|
|
391
403
|
@staticmethod
|
392
404
|
def convert_function_results_to_openai(
|
393
405
|
results: List[Tuple[str, CallToolResult]],
|
394
406
|
concatenate_text_blocks: bool = False,
|
395
|
-
) -> List[
|
407
|
+
) -> List[Dict[str, Any]]:
|
396
408
|
"""
|
397
409
|
Convert a list of function call results to OpenAI messages.
|
398
|
-
Handles cases where tool results contain non-text content by creating
|
399
|
-
additional user messages as needed.
|
400
410
|
|
401
411
|
Args:
|
402
412
|
results: List of (tool_call_id, result) tuples
|