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.

Files changed (40) hide show
  1. {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/METADATA +1 -1
  2. {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/RECORD +39 -38
  3. mcp_agent/agents/agent.py +1 -24
  4. mcp_agent/app.py +0 -5
  5. mcp_agent/context.py +0 -2
  6. mcp_agent/core/agent_app.py +1 -1
  7. mcp_agent/core/agent_types.py +29 -2
  8. mcp_agent/core/decorators.py +1 -2
  9. mcp_agent/core/error_handling.py +1 -1
  10. mcp_agent/core/factory.py +2 -3
  11. mcp_agent/core/mcp_content.py +2 -3
  12. mcp_agent/core/request_params.py +43 -0
  13. mcp_agent/core/types.py +4 -2
  14. mcp_agent/core/validation.py +14 -15
  15. mcp_agent/logging/transport.py +2 -2
  16. mcp_agent/mcp/interfaces.py +37 -3
  17. mcp_agent/mcp/mcp_agent_client_session.py +1 -1
  18. mcp_agent/mcp/mcp_aggregator.py +5 -6
  19. mcp_agent/mcp/sampling.py +60 -53
  20. mcp_agent/mcp_server/__init__.py +1 -1
  21. mcp_agent/resources/examples/prompting/__init__.py +1 -1
  22. mcp_agent/ui/console_display.py +2 -2
  23. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +2 -2
  24. mcp_agent/workflows/llm/augmented_llm.py +42 -102
  25. mcp_agent/workflows/llm/augmented_llm_anthropic.py +4 -3
  26. mcp_agent/workflows/llm/augmented_llm_openai.py +4 -3
  27. mcp_agent/workflows/llm/augmented_llm_passthrough.py +33 -4
  28. mcp_agent/workflows/llm/model_factory.py +1 -1
  29. mcp_agent/workflows/llm/prompt_utils.py +42 -28
  30. mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +244 -140
  31. mcp_agent/workflows/llm/providers/multipart_converter_openai.py +230 -185
  32. mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +5 -204
  33. mcp_agent/workflows/llm/providers/sampling_converter_openai.py +9 -207
  34. mcp_agent/workflows/llm/sampling_converter.py +124 -0
  35. mcp_agent/workflows/llm/sampling_format_converter.py +0 -17
  36. mcp_agent/workflows/router/router_base.py +10 -10
  37. mcp_agent/workflows/llm/llm_selector.py +0 -345
  38. {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/WHEEL +0 -0
  39. {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
  40. {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, Tuple
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 the types for OpenAI API
22
- OpenAIContentBlock = Dict[str, Any]
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(OpenAIConverter._convert_text_content(item))
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
- # This assumes an InputAudioContent class structure with input_audio attribute
79
- if hasattr(item, "input_audio"):
80
- content_blocks.append(
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})
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
- combined_blocks = []
123
- current_text = ""
124
-
125
- for block in content_blocks:
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
- # Don't forget any remaining text
141
- if current_text:
142
- combined_blocks.append({"type": "text", "text": current_text})
144
+ @staticmethod
145
+ def _concatenate_text_blocks(blocks: List[ContentBlock]) -> List[ContentBlock]:
146
+ """
147
+ Combine adjacent text blocks into single blocks.
143
148
 
144
- content_blocks = combined_blocks
149
+ Args:
150
+ blocks: List of content blocks
145
151
 
146
- return {"role": role, "content": content_blocks}
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 _convert_text_content(content: TextContent) -> OpenAIContentBlock:
150
- """Convert TextContent to OpenAI text content block."""
151
- return {"type": "text", "text": content.text}
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) -> OpenAIContentBlock:
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[OpenAIContentBlock]:
174
- """Convert EmbeddedResource to appropriate OpenAI content block."""
175
- resource_content = resource.resource
176
- uri = resource_content.uri
243
+ ) -> Optional[ContentBlock]:
244
+ """
245
+ Convert EmbeddedResource to appropriate OpenAI content block.
177
246
 
178
- # Use mime_utils to guess MIME type if not provided
179
- if resource_content.mimeType is None and uri:
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
- is_url: bool = str(uri).startswith(("http://", "https://"))
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 image resources
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["url"] = str(uri)
264
+ return {"type": "image_url", "image_url": {"url": str(uri)}}
194
265
  elif hasattr(resource_content, "blob"):
195
- image_url["url"] = f"data:{mime_type};base64,{resource_content.blob}"
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
- # Check for detail level in annotations if available
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, only file_id or base64
213
- _logger.warning(f"PDF URL not directly supported in OpenAI API: {uri}")
214
- fallback_text = f"[PDF URL: {uri}]\nOpenAI requires PDF files to be uploaded or provided as base64 data."
215
- return {"type": "text", "text": fallback_text}
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 as text with fastagent:file tags
226
- elif mime_type == "image/svg+xml":
227
- if hasattr(resource_content, "text"):
228
- file_text = (
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
- }
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 - convert to text if possible
264
- if hasattr(resource_content, "text"):
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
- _logger.warning(f"Unable to convert resource with MIME type: {mime_type}")
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[OpenAIMessage, Tuple[OpenAIMessage, List[OpenAIMessage]]]:
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
- If the result contains non-text elements, those are converted to separate messages
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
- # First, separate text and non-text content
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
- # If we only have text content, process as before
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=True
403
+ temp_multipart, concatenate_text_blocks=concatenate_text_blocks
352
404
  )
353
405
 
354
- if isinstance(converted["content"], str):
355
- tool_message_content = converted["content"]
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)
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
- if non_text_content:
377
- # Create a multipart message with the non-text content
378
- non_text_multipart = PromptMessageMultipart(
379
- role="user", content=non_text_content
380
- )
426
+ non_text_multipart = PromptMessageMultipart(
427
+ role="user", content=non_text_content
428
+ )
381
429
 
382
- # Convert to OpenAI format
383
- user_message = OpenAIConverter.convert_to_openai(non_text_multipart)
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
- return (tool_message, [user_message])
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[OpenAIMessage]:
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