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.
Files changed (131) hide show
  1. {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.13.dist-info}/METADATA +1 -1
  2. fast_agent_mcp-0.1.13.dist-info/RECORD +164 -0
  3. mcp_agent/agents/agent.py +37 -102
  4. mcp_agent/app.py +16 -27
  5. mcp_agent/cli/commands/bootstrap.py +22 -52
  6. mcp_agent/cli/commands/config.py +4 -4
  7. mcp_agent/cli/commands/setup.py +11 -26
  8. mcp_agent/cli/main.py +6 -9
  9. mcp_agent/cli/terminal.py +2 -2
  10. mcp_agent/config.py +1 -5
  11. mcp_agent/context.py +13 -26
  12. mcp_agent/context_dependent.py +3 -7
  13. mcp_agent/core/agent_app.py +46 -122
  14. mcp_agent/core/agent_types.py +29 -2
  15. mcp_agent/core/agent_utils.py +3 -5
  16. mcp_agent/core/decorators.py +6 -14
  17. mcp_agent/core/enhanced_prompt.py +25 -52
  18. mcp_agent/core/error_handling.py +1 -1
  19. mcp_agent/core/exceptions.py +8 -8
  20. mcp_agent/core/factory.py +30 -72
  21. mcp_agent/core/fastagent.py +48 -88
  22. mcp_agent/core/mcp_content.py +10 -19
  23. mcp_agent/core/prompt.py +8 -15
  24. mcp_agent/core/proxies.py +34 -25
  25. mcp_agent/core/request_params.py +46 -0
  26. mcp_agent/core/types.py +6 -6
  27. mcp_agent/core/validation.py +16 -16
  28. mcp_agent/executor/decorator_registry.py +11 -23
  29. mcp_agent/executor/executor.py +8 -17
  30. mcp_agent/executor/task_registry.py +2 -4
  31. mcp_agent/executor/temporal.py +28 -74
  32. mcp_agent/executor/workflow.py +3 -5
  33. mcp_agent/executor/workflow_signal.py +17 -29
  34. mcp_agent/human_input/handler.py +4 -9
  35. mcp_agent/human_input/types.py +2 -3
  36. mcp_agent/logging/events.py +1 -5
  37. mcp_agent/logging/json_serializer.py +7 -6
  38. mcp_agent/logging/listeners.py +20 -23
  39. mcp_agent/logging/logger.py +15 -17
  40. mcp_agent/logging/rich_progress.py +10 -8
  41. mcp_agent/logging/tracing.py +4 -6
  42. mcp_agent/logging/transport.py +24 -24
  43. mcp_agent/mcp/gen_client.py +4 -12
  44. mcp_agent/mcp/interfaces.py +107 -88
  45. mcp_agent/mcp/mcp_agent_client_session.py +11 -19
  46. mcp_agent/mcp/mcp_agent_server.py +8 -10
  47. mcp_agent/mcp/mcp_aggregator.py +49 -122
  48. mcp_agent/mcp/mcp_connection_manager.py +16 -37
  49. mcp_agent/mcp/prompt_message_multipart.py +12 -18
  50. mcp_agent/mcp/prompt_serialization.py +13 -38
  51. mcp_agent/mcp/prompts/prompt_load.py +99 -0
  52. mcp_agent/mcp/prompts/prompt_server.py +21 -128
  53. mcp_agent/mcp/prompts/prompt_template.py +20 -42
  54. mcp_agent/mcp/resource_utils.py +8 -17
  55. mcp_agent/mcp/sampling.py +62 -64
  56. mcp_agent/mcp/stdio.py +11 -8
  57. mcp_agent/mcp_server/__init__.py +1 -1
  58. mcp_agent/mcp_server/agent_server.py +10 -17
  59. mcp_agent/mcp_server_registry.py +13 -35
  60. mcp_agent/resources/examples/data-analysis/analysis-campaign.py +1 -1
  61. mcp_agent/resources/examples/data-analysis/analysis.py +1 -1
  62. mcp_agent/resources/examples/data-analysis/slides.py +110 -0
  63. mcp_agent/resources/examples/internal/agent.py +2 -1
  64. mcp_agent/resources/examples/internal/job.py +2 -1
  65. mcp_agent/resources/examples/internal/prompt_category.py +1 -1
  66. mcp_agent/resources/examples/internal/prompt_sizing.py +3 -5
  67. mcp_agent/resources/examples/internal/sizer.py +2 -1
  68. mcp_agent/resources/examples/internal/social.py +2 -1
  69. mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +1 -1
  70. mcp_agent/resources/examples/prompting/__init__.py +1 -1
  71. mcp_agent/resources/examples/prompting/agent.py +2 -1
  72. mcp_agent/resources/examples/prompting/image_server.py +5 -11
  73. mcp_agent/resources/examples/researcher/researcher-eval.py +1 -1
  74. mcp_agent/resources/examples/researcher/researcher-imp.py +3 -4
  75. mcp_agent/resources/examples/researcher/researcher.py +2 -1
  76. mcp_agent/resources/examples/workflows/agent_build.py +2 -1
  77. mcp_agent/resources/examples/workflows/chaining.py +2 -1
  78. mcp_agent/resources/examples/workflows/evaluator.py +2 -1
  79. mcp_agent/resources/examples/workflows/human_input.py +2 -1
  80. mcp_agent/resources/examples/workflows/orchestrator.py +2 -1
  81. mcp_agent/resources/examples/workflows/parallel.py +2 -1
  82. mcp_agent/resources/examples/workflows/router.py +2 -1
  83. mcp_agent/resources/examples/workflows/sse.py +1 -1
  84. mcp_agent/telemetry/usage_tracking.py +2 -1
  85. mcp_agent/ui/console_display.py +17 -41
  86. mcp_agent/workflows/embedding/embedding_base.py +1 -4
  87. mcp_agent/workflows/embedding/embedding_cohere.py +2 -2
  88. mcp_agent/workflows/embedding/embedding_openai.py +4 -13
  89. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +23 -57
  90. mcp_agent/workflows/intent_classifier/intent_classifier_base.py +5 -8
  91. mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +7 -11
  92. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +4 -8
  93. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +4 -8
  94. mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +11 -22
  95. mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +3 -3
  96. mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +4 -6
  97. mcp_agent/workflows/llm/anthropic_utils.py +8 -29
  98. mcp_agent/workflows/llm/augmented_llm.py +94 -332
  99. mcp_agent/workflows/llm/augmented_llm_anthropic.py +43 -76
  100. mcp_agent/workflows/llm/augmented_llm_openai.py +46 -100
  101. mcp_agent/workflows/llm/augmented_llm_passthrough.py +42 -20
  102. mcp_agent/workflows/llm/augmented_llm_playback.py +8 -6
  103. mcp_agent/workflows/llm/memory.py +103 -0
  104. mcp_agent/workflows/llm/model_factory.py +9 -21
  105. mcp_agent/workflows/llm/openai_utils.py +1 -1
  106. mcp_agent/workflows/llm/prompt_utils.py +39 -27
  107. mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +246 -184
  108. mcp_agent/workflows/llm/providers/multipart_converter_openai.py +212 -202
  109. mcp_agent/workflows/llm/providers/openai_multipart.py +19 -61
  110. mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +11 -212
  111. mcp_agent/workflows/llm/providers/sampling_converter_openai.py +13 -215
  112. mcp_agent/workflows/llm/sampling_converter.py +117 -0
  113. mcp_agent/workflows/llm/sampling_format_converter.py +12 -29
  114. mcp_agent/workflows/orchestrator/orchestrator.py +24 -67
  115. mcp_agent/workflows/orchestrator/orchestrator_models.py +14 -40
  116. mcp_agent/workflows/parallel/fan_in.py +17 -47
  117. mcp_agent/workflows/parallel/fan_out.py +6 -12
  118. mcp_agent/workflows/parallel/parallel_llm.py +9 -26
  119. mcp_agent/workflows/router/router_base.py +29 -59
  120. mcp_agent/workflows/router/router_embedding.py +11 -25
  121. mcp_agent/workflows/router/router_embedding_cohere.py +2 -2
  122. mcp_agent/workflows/router/router_embedding_openai.py +2 -2
  123. mcp_agent/workflows/router/router_llm.py +12 -28
  124. mcp_agent/workflows/swarm/swarm.py +20 -48
  125. mcp_agent/workflows/swarm/swarm_anthropic.py +2 -2
  126. mcp_agent/workflows/swarm/swarm_openai.py +2 -2
  127. fast_agent_mcp-0.1.11.dist-info/RECORD +0 -160
  128. mcp_agent/workflows/llm/llm_selector.py +0 -345
  129. {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.13.dist-info}/WHEEL +0 -0
  130. {fast_agent_mcp-0.1.11.dist-info → fast_agent_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
  131. {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 List, Union, Optional, Dict, Any, Tuple
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
- from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
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 the types for OpenAI API
22
- OpenAIContentBlock = Dict[str, Any]
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 convert_to_openai(
31
- multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False
32
- ) -> OpenAIMessage:
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(OpenAIConverter._convert_text_content(item))
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
- # 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})
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
- 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)
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
- # Don't forget any remaining text
141
- if current_text:
142
- combined_blocks.append({"type": "text", "text": current_text})
131
+ @staticmethod
132
+ def _concatenate_text_blocks(blocks: List[ContentBlock]) -> List[ContentBlock]:
133
+ """
134
+ Combine adjacent text blocks into single blocks.
143
135
 
144
- content_blocks = combined_blocks
136
+ Args:
137
+ blocks: List of content blocks
145
138
 
146
- return {"role": role, "content": content_blocks}
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 _convert_text_content(content: TextContent) -> OpenAIContentBlock:
150
- """Convert TextContent to OpenAI text content block."""
151
- return {"type": "text", "text": content.text}
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) -> OpenAIContentBlock:
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[OpenAIContentBlock]:
174
- """Convert EmbeddedResource to appropriate OpenAI content block."""
175
- resource_content = resource.resource
176
- uri = resource_content.uri
228
+ ) -> Optional[ContentBlock]:
229
+ """
230
+ Convert EmbeddedResource to appropriate OpenAI content block.
177
231
 
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"
232
+ Args:
233
+ resource: The embedded resource to convert
184
234
 
185
- is_url: bool = str(uri).startswith(("http://", "https://"))
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 image resources
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["url"] = str(uri)
249
+ return {"type": "image_url", "image_url": {"url": str(uri)}}
194
250
  elif hasattr(resource_content, "blob"):
195
- image_url["url"] = f"data:{mime_type};base64,{resource_content.blob}"
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
- # 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
258
+ # Handle PDFs
210
259
  elif mime_type == "application/pdf":
211
260
  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}
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 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
- }
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
- # 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
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
- _logger.warning(f"Unable to convert resource with MIME type: {mime_type}")
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[OpenAIMessage, Tuple[OpenAIMessage, List[OpenAIMessage]]]:
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
- If the result contains non-text elements, those are converted to separate messages
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
- # First, separate text and non-text content
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
- # 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
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
- 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)
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
- 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
- )
393
+ non_text_multipart = PromptMessageMultipart(role="user", content=non_text_content)
381
394
 
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
395
+ # Convert to OpenAI format
396
+ user_message = OpenAIConverter.convert_to_openai(non_text_multipart)
386
397
 
387
- return (tool_message, [user_message])
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[OpenAIMessage]:
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