fast-agent-mcp 0.1.12__py3-none-any.whl → 0.2.0__py3-none-any.whl

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