fast-agent-mcp 0.1.11__py3-none-any.whl → 0.1.12__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.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/METADATA +1 -1
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/RECORD +39 -38
- mcp_agent/agents/agent.py +1 -24
- mcp_agent/app.py +0 -5
- mcp_agent/context.py +0 -2
- mcp_agent/core/agent_app.py +1 -1
- mcp_agent/core/agent_types.py +29 -2
- mcp_agent/core/decorators.py +1 -2
- mcp_agent/core/error_handling.py +1 -1
- mcp_agent/core/factory.py +2 -3
- mcp_agent/core/mcp_content.py +2 -3
- mcp_agent/core/request_params.py +43 -0
- mcp_agent/core/types.py +4 -2
- mcp_agent/core/validation.py +14 -15
- mcp_agent/logging/transport.py +2 -2
- mcp_agent/mcp/interfaces.py +37 -3
- mcp_agent/mcp/mcp_agent_client_session.py +1 -1
- mcp_agent/mcp/mcp_aggregator.py +5 -6
- mcp_agent/mcp/sampling.py +60 -53
- mcp_agent/mcp_server/__init__.py +1 -1
- mcp_agent/resources/examples/prompting/__init__.py +1 -1
- mcp_agent/ui/console_display.py +2 -2
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +2 -2
- mcp_agent/workflows/llm/augmented_llm.py +42 -102
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +4 -3
- mcp_agent/workflows/llm/augmented_llm_openai.py +4 -3
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +33 -4
- mcp_agent/workflows/llm/model_factory.py +1 -1
- mcp_agent/workflows/llm/prompt_utils.py +42 -28
- mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +244 -140
- mcp_agent/workflows/llm/providers/multipart_converter_openai.py +230 -185
- mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +5 -204
- mcp_agent/workflows/llm/providers/sampling_converter_openai.py +9 -207
- mcp_agent/workflows/llm/sampling_converter.py +124 -0
- mcp_agent/workflows/llm/sampling_format_converter.py +0 -17
- mcp_agent/workflows/router/router_base.py +10 -10
- mcp_agent/workflows/llm/llm_selector.py +0 -345
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,11 @@
|
|
1
|
-
from typing import List, Union, Optional, Dict, Any
|
1
|
+
from typing import List, Union, Optional, Tuple, Dict, Any
|
2
2
|
|
3
3
|
from mcp.types import (
|
4
4
|
TextContent,
|
5
5
|
ImageContent,
|
6
6
|
EmbeddedResource,
|
7
7
|
CallToolResult,
|
8
|
+
PromptMessage,
|
8
9
|
)
|
9
10
|
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
10
11
|
from mcp_agent.mcp.mime_utils import (
|
@@ -14,18 +15,36 @@ from mcp_agent.mcp.mime_utils import (
|
|
14
15
|
)
|
15
16
|
from mcp_agent.mcp.resource_utils import extract_title_from_uri
|
16
17
|
|
18
|
+
|
17
19
|
from mcp_agent.logging.logger import get_logger
|
18
20
|
|
19
21
|
_logger = get_logger("multipart_converter_openai")
|
20
22
|
|
21
|
-
# Define
|
22
|
-
|
23
|
+
# Define type aliases for content blocks
|
24
|
+
ContentBlock = Dict[str, Any]
|
23
25
|
OpenAIMessage = Dict[str, Any]
|
24
26
|
|
25
27
|
|
26
28
|
class OpenAIConverter:
|
27
29
|
"""Converts MCP message types to OpenAI API format."""
|
28
30
|
|
31
|
+
@staticmethod
|
32
|
+
def _is_supported_image_type(mime_type: str) -> bool:
|
33
|
+
"""
|
34
|
+
Check if the given MIME type is supported by OpenAI's image API.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
mime_type: The MIME type to check
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
True if the MIME type is generally supported, False otherwise
|
41
|
+
"""
|
42
|
+
return (
|
43
|
+
mime_type is not None
|
44
|
+
and is_image_mime_type(mime_type)
|
45
|
+
and mime_type != "image/svg+xml"
|
46
|
+
)
|
47
|
+
|
29
48
|
@staticmethod
|
30
49
|
def convert_to_openai(
|
31
50
|
multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False
|
@@ -57,13 +76,23 @@ class OpenAIConverter:
|
|
57
76
|
|
58
77
|
return {"role": role, "content": content_text}
|
59
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
|
+
|
87
|
+
return {"role": role, "content": content_text}
|
88
|
+
|
60
89
|
# For user messages, convert each content block
|
61
|
-
content_blocks = []
|
90
|
+
content_blocks: List[ContentBlock] = []
|
62
91
|
|
63
92
|
for item in multipart_msg.content:
|
64
93
|
try:
|
65
94
|
if isinstance(item, TextContent):
|
66
|
-
content_blocks.append(
|
95
|
+
content_blocks.append({"type": "text", "text": item.text})
|
67
96
|
|
68
97
|
elif isinstance(item, ImageContent):
|
69
98
|
content_blocks.append(OpenAIConverter._convert_image_content(item))
|
@@ -75,23 +104,11 @@ class OpenAIConverter:
|
|
75
104
|
|
76
105
|
# Handle input_audio if implemented
|
77
106
|
elif hasattr(item, "type") and getattr(item, "type") == "input_audio":
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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})
|
107
|
+
_logger.warning(
|
108
|
+
"Input audio content not supported in standard OpenAI types"
|
109
|
+
)
|
110
|
+
fallback_text = "[Audio content not directly supported]"
|
111
|
+
content_blocks.append({"type": "text", "text": fallback_text})
|
95
112
|
|
96
113
|
else:
|
97
114
|
_logger.warning(f"Unsupported content type: {type(item)}")
|
@@ -119,46 +136,76 @@ class OpenAIConverter:
|
|
119
136
|
|
120
137
|
# If concatenate_text_blocks is True, combine adjacent text blocks
|
121
138
|
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)
|
139
|
+
content_blocks = OpenAIConverter._concatenate_text_blocks(content_blocks)
|
140
|
+
|
141
|
+
# Return user message with content blocks
|
142
|
+
return {"role": role, "content": content_blocks}
|
139
143
|
|
140
|
-
|
141
|
-
|
142
|
-
|
144
|
+
@staticmethod
|
145
|
+
def _concatenate_text_blocks(blocks: List[ContentBlock]) -> List[ContentBlock]:
|
146
|
+
"""
|
147
|
+
Combine adjacent text blocks into single blocks.
|
143
148
|
|
144
|
-
|
149
|
+
Args:
|
150
|
+
blocks: List of content blocks
|
145
151
|
|
146
|
-
|
152
|
+
Returns:
|
153
|
+
List with adjacent text blocks combined
|
154
|
+
"""
|
155
|
+
if not blocks:
|
156
|
+
return []
|
157
|
+
|
158
|
+
combined_blocks: List[ContentBlock] = []
|
159
|
+
current_text = ""
|
160
|
+
|
161
|
+
for block in blocks:
|
162
|
+
if block["type"] == "text":
|
163
|
+
# Add to current text accumulator
|
164
|
+
if current_text:
|
165
|
+
current_text += " " + block["text"]
|
166
|
+
else:
|
167
|
+
current_text = block["text"]
|
168
|
+
else:
|
169
|
+
# Non-text block found, flush accumulated text if any
|
170
|
+
if current_text:
|
171
|
+
combined_blocks.append({"type": "text", "text": current_text})
|
172
|
+
current_text = ""
|
173
|
+
# Add the non-text block
|
174
|
+
combined_blocks.append(block)
|
175
|
+
|
176
|
+
# Don't forget any remaining text
|
177
|
+
if current_text:
|
178
|
+
combined_blocks.append({"type": "text", "text": current_text})
|
179
|
+
|
180
|
+
return combined_blocks
|
147
181
|
|
148
182
|
@staticmethod
|
149
|
-
def
|
150
|
-
|
151
|
-
|
183
|
+
def convert_prompt_message_to_openai(
|
184
|
+
message: PromptMessage, concatenate_text_blocks: bool = False
|
185
|
+
) -> OpenAIMessage:
|
186
|
+
"""
|
187
|
+
Convert a standard PromptMessage to OpenAI API format.
|
188
|
+
|
189
|
+
Args:
|
190
|
+
message: The PromptMessage to convert
|
191
|
+
concatenate_text_blocks: If True, adjacent text blocks will be combined
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
An OpenAI API message object
|
195
|
+
"""
|
196
|
+
# Convert the PromptMessage to a PromptMessageMultipart containing a single content item
|
197
|
+
multipart = PromptMessageMultipart(role=message.role, content=[message.content])
|
198
|
+
|
199
|
+
# Use the existing conversion method with the specified concatenation option
|
200
|
+
return OpenAIConverter.convert_to_openai(multipart, concatenate_text_blocks)
|
152
201
|
|
153
202
|
@staticmethod
|
154
|
-
def _convert_image_content(content: ImageContent) ->
|
203
|
+
def _convert_image_content(content: ImageContent) -> ContentBlock:
|
155
204
|
"""Convert ImageContent to OpenAI image_url content block."""
|
156
205
|
# OpenAI requires image URLs or data URIs for images
|
157
206
|
image_url = {"url": f"data:{content.mimeType};base64,{content.data}"}
|
158
207
|
|
159
208
|
# 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
209
|
if hasattr(content, "annotations") and content.annotations:
|
163
210
|
if hasattr(content.annotations, "detail"):
|
164
211
|
detail = content.annotations.detail
|
@@ -167,52 +214,72 @@ class OpenAIConverter:
|
|
167
214
|
|
168
215
|
return {"type": "image_url", "image_url": image_url}
|
169
216
|
|
217
|
+
@staticmethod
|
218
|
+
def _determine_mime_type(resource_content) -> str:
|
219
|
+
"""
|
220
|
+
Determine the MIME type of a resource.
|
221
|
+
|
222
|
+
Args:
|
223
|
+
resource_content: The resource content to check
|
224
|
+
|
225
|
+
Returns:
|
226
|
+
The determined MIME type as a string
|
227
|
+
"""
|
228
|
+
if hasattr(resource_content, "mimeType") and resource_content.mimeType:
|
229
|
+
return resource_content.mimeType
|
230
|
+
|
231
|
+
if hasattr(resource_content, "uri") and resource_content.uri:
|
232
|
+
mime_type = guess_mime_type(str(resource_content.uri))
|
233
|
+
return mime_type
|
234
|
+
|
235
|
+
if hasattr(resource_content, "blob"):
|
236
|
+
return "application/octet-stream"
|
237
|
+
|
238
|
+
return "text/plain"
|
239
|
+
|
170
240
|
@staticmethod
|
171
241
|
def _convert_embedded_resource(
|
172
242
|
resource: EmbeddedResource,
|
173
|
-
) -> Optional[
|
174
|
-
"""
|
175
|
-
|
176
|
-
uri = resource_content.uri
|
243
|
+
) -> Optional[ContentBlock]:
|
244
|
+
"""
|
245
|
+
Convert EmbeddedResource to appropriate OpenAI content block.
|
177
246
|
|
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"
|
247
|
+
Args:
|
248
|
+
resource: The embedded resource to convert
|
184
249
|
|
185
|
-
|
250
|
+
Returns:
|
251
|
+
An appropriate OpenAI content block or None if conversion failed
|
252
|
+
"""
|
253
|
+
resource_content = resource.resource
|
254
|
+
uri = getattr(resource_content, "uri", None)
|
255
|
+
is_url = uri and str(uri).startswith(("http://", "https://"))
|
186
256
|
title = extract_title_from_uri(uri) if uri else "resource"
|
257
|
+
mime_type = OpenAIConverter._determine_mime_type(resource_content)
|
187
258
|
|
188
|
-
# Handle
|
189
|
-
if is_image_mime_type(mime_type) and mime_type != "image/svg+xml":
|
190
|
-
image_url = {}
|
259
|
+
# Handle different resource types based on MIME type
|
191
260
|
|
261
|
+
# Handle images
|
262
|
+
if OpenAIConverter._is_supported_image_type(mime_type):
|
192
263
|
if is_url:
|
193
|
-
image_url
|
264
|
+
return {"type": "image_url", "image_url": {"url": str(uri)}}
|
194
265
|
elif hasattr(resource_content, "blob"):
|
195
|
-
|
266
|
+
return {
|
267
|
+
"type": "image_url",
|
268
|
+
"image_url": {
|
269
|
+
"url": f"data:{mime_type};base64,{resource_content.blob}"
|
270
|
+
},
|
271
|
+
}
|
196
272
|
else:
|
197
|
-
_logger.warning(f"Image resource missing both URL and blob data: {uri}")
|
198
273
|
return {"type": "text", "text": f"[Image missing data: {title}]"}
|
199
274
|
|
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
|
275
|
+
# Handle PDFs
|
210
276
|
elif mime_type == "application/pdf":
|
211
277
|
if is_url:
|
212
|
-
# OpenAI doesn't directly support PDF URLs,
|
213
|
-
|
214
|
-
|
215
|
-
|
278
|
+
# OpenAI doesn't directly support PDF URLs, explain this limitation
|
279
|
+
return {
|
280
|
+
"type": "text",
|
281
|
+
"text": f"[PDF URL: {uri}]\nOpenAI requires PDF files to be uploaded or provided as base64 data.",
|
282
|
+
}
|
216
283
|
elif hasattr(resource_content, "blob"):
|
217
284
|
return {
|
218
285
|
"type": "file",
|
@@ -222,65 +289,82 @@ class OpenAIConverter:
|
|
222
289
|
},
|
223
290
|
}
|
224
291
|
|
225
|
-
# Handle SVG
|
226
|
-
elif mime_type == "image/svg+xml":
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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
|
-
}
|
292
|
+
# Handle SVG (convert to text)
|
293
|
+
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
|
+
)
|
299
|
+
return {"type": "text", "text": file_text}
|
300
|
+
|
301
|
+
# Handle text files
|
302
|
+
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
|
+
)
|
308
|
+
return {"type": "text", "text": file_text}
|
262
309
|
|
263
|
-
# Default fallback
|
264
|
-
|
265
|
-
# For anything with text content that isn't handled specially above,
|
266
|
-
# use the raw text without special formatting
|
310
|
+
# Default fallback for text resources
|
311
|
+
elif hasattr(resource_content, "text"):
|
267
312
|
return {"type": "text", "text": resource_content.text}
|
268
313
|
|
269
|
-
|
314
|
+
# Default fallback for binary resources
|
315
|
+
elif hasattr(resource_content, "blob"):
|
316
|
+
return {
|
317
|
+
"type": "text",
|
318
|
+
"text": f"[Binary resource: {title} ({mime_type})]",
|
319
|
+
}
|
320
|
+
|
321
|
+
# Last resort fallback
|
270
322
|
return {
|
271
323
|
"type": "text",
|
272
324
|
"text": f"[Unsupported resource: {title} ({mime_type})]",
|
273
325
|
}
|
274
326
|
|
327
|
+
@staticmethod
|
328
|
+
def _extract_text_from_content_blocks(
|
329
|
+
content: Union[str, List[ContentBlock]],
|
330
|
+
) -> str:
|
331
|
+
"""
|
332
|
+
Extract and combine text from content blocks.
|
333
|
+
|
334
|
+
Args:
|
335
|
+
content: Content blocks or string
|
336
|
+
|
337
|
+
Returns:
|
338
|
+
Combined text as a string
|
339
|
+
"""
|
340
|
+
if isinstance(content, str):
|
341
|
+
return content
|
342
|
+
|
343
|
+
if not content:
|
344
|
+
return ""
|
345
|
+
|
346
|
+
# Extract only text blocks
|
347
|
+
text_parts = []
|
348
|
+
for block in content:
|
349
|
+
if block.get("type") == "text":
|
350
|
+
text_parts.append(block.get("text", ""))
|
351
|
+
|
352
|
+
return (
|
353
|
+
" ".join(text_parts)
|
354
|
+
if text_parts
|
355
|
+
else "[Complex content converted to text]"
|
356
|
+
)
|
357
|
+
|
275
358
|
@staticmethod
|
276
359
|
def convert_tool_result_to_openai(
|
277
360
|
tool_result: CallToolResult,
|
278
361
|
tool_call_id: str,
|
279
362
|
concatenate_text_blocks: bool = False,
|
280
|
-
) -> Union[
|
363
|
+
) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[Dict[str, Any]]]]:
|
281
364
|
"""
|
282
365
|
Convert a CallToolResult to an OpenAI tool message.
|
283
|
-
|
366
|
+
|
367
|
+
If the result contains non-text elements, those are converted to separate user messages
|
284
368
|
since OpenAI tool messages can only contain text.
|
285
369
|
|
286
370
|
Args:
|
@@ -300,7 +384,7 @@ class OpenAIConverter:
|
|
300
384
|
"content": "[No content in tool result]",
|
301
385
|
}
|
302
386
|
|
303
|
-
#
|
387
|
+
# Separate text and non-text content
|
304
388
|
text_content = []
|
305
389
|
non_text_content = []
|
306
390
|
|
@@ -310,57 +394,19 @@ class OpenAIConverter:
|
|
310
394
|
else:
|
311
395
|
non_text_content.append(item)
|
312
396
|
|
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
|
397
|
+
# Create tool message with text content
|
347
398
|
tool_message_content = ""
|
348
399
|
if text_content:
|
400
|
+
# Convert text content to OpenAI format
|
349
401
|
temp_multipart = PromptMessageMultipart(role="user", content=text_content)
|
350
402
|
converted = OpenAIConverter.convert_to_openai(
|
351
|
-
temp_multipart, concatenate_text_blocks=
|
403
|
+
temp_multipart, concatenate_text_blocks=concatenate_text_blocks
|
352
404
|
)
|
353
405
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
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)
|
406
|
+
# Extract text from content blocks
|
407
|
+
tool_message_content = OpenAIConverter._extract_text_from_content_blocks(
|
408
|
+
converted.get("content", "")
|
409
|
+
)
|
364
410
|
|
365
411
|
if not tool_message_content:
|
366
412
|
tool_message_content = "[Tool returned non-text content]"
|
@@ -372,31 +418,30 @@ class OpenAIConverter:
|
|
372
418
|
"content": tool_message_content,
|
373
419
|
}
|
374
420
|
|
421
|
+
# If there's no non-text content, return just the tool message
|
422
|
+
if not non_text_content:
|
423
|
+
return tool_message
|
424
|
+
|
375
425
|
# Process non-text content as a separate user message
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
role="user", content=non_text_content
|
380
|
-
)
|
426
|
+
non_text_multipart = PromptMessageMultipart(
|
427
|
+
role="user", content=non_text_content
|
428
|
+
)
|
381
429
|
|
382
|
-
|
383
|
-
|
384
|
-
# Add tool_call_id to associate with the tool call
|
385
|
-
user_message["tool_call_id"] = tool_call_id
|
430
|
+
# Convert to OpenAI format
|
431
|
+
user_message = OpenAIConverter.convert_to_openai(non_text_multipart)
|
386
432
|
|
387
|
-
|
433
|
+
# We need to add tool_call_id manually
|
434
|
+
user_message["tool_call_id"] = tool_call_id
|
388
435
|
|
389
|
-
return tool_message
|
436
|
+
return (tool_message, [user_message])
|
390
437
|
|
391
438
|
@staticmethod
|
392
439
|
def convert_function_results_to_openai(
|
393
440
|
results: List[Tuple[str, CallToolResult]],
|
394
441
|
concatenate_text_blocks: bool = False,
|
395
|
-
) -> List[
|
442
|
+
) -> List[Dict[str, Any]]:
|
396
443
|
"""
|
397
444
|
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
445
|
|
401
446
|
Args:
|
402
447
|
results: List of (tool_call_id, result) tuples
|