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
@@ -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,44 +63,22 @@ 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
- if (
83
- text
84
- and (
85
- text.startswith("[Resource:")
86
- or text.startswith("[Binary Resource:")
87
- )
88
- and "\n" in text
89
- ):
73
+ if text and (text.startswith("[Resource:") or text.startswith("[Binary Resource:")) and "\n" in text:
90
74
  header, content_text = text.split("\n", 1)
91
75
  if "MIME:" in header:
92
76
  mime_match = header.split("MIME:", 1)[1].split("]")[0].strip()
93
77
 
94
78
  # If not text/plain, create an embedded resource
95
79
  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
- )
80
+ if "Resource:" in header and "Binary Resource:" not in header:
81
+ uri = header.split("Resource:", 1)[1].split(",")[0].strip()
105
82
  mcp_contents.append(
106
83
  EmbeddedResource(
107
84
  type="resource",
@@ -119,31 +96,17 @@ def _openai_message_to_multipart(
119
96
 
120
97
  # Handle image content
121
98
  elif part_type == "image_url":
122
- image_url = (
123
- part.get("image_url", {})
124
- if isinstance(part, dict)
125
- else getattr(part, "image_url", None)
126
- )
99
+ image_url = part.get("image_url", {}) if isinstance(part, dict) else getattr(part, "image_url", None)
127
100
  if image_url:
128
- url = (
129
- image_url.get("url")
130
- if isinstance(image_url, dict)
131
- else getattr(image_url, "url", "")
132
- )
101
+ url = image_url.get("url") if isinstance(image_url, dict) else getattr(image_url, "url", "")
133
102
  if url and url.startswith("data:image/"):
134
103
  # Handle base64 data URLs
135
104
  mime_type = url.split(";")[0].replace("data:", "")
136
105
  data = url.split(",")[1]
137
- mcp_contents.append(
138
- ImageContent(type="image", data=data, mimeType=mime_type)
139
- )
106
+ mcp_contents.append(ImageContent(type="image", data=data, mimeType=mime_type))
140
107
 
141
108
  # Handle explicit resource types
142
- elif (
143
- part_type == "resource"
144
- and isinstance(part, dict)
145
- and "resource" in part
146
- ):
109
+ elif part_type == "resource" and isinstance(part, dict) and "resource" in part:
147
110
  resource = part["resource"]
148
111
  if isinstance(resource, dict):
149
112
  # Text resource
@@ -152,9 +115,7 @@ def _openai_message_to_multipart(
152
115
  uri = resource.get("uri", "resource://unknown")
153
116
 
154
117
  if mime_type == "text/plain":
155
- mcp_contents.append(
156
- TextContent(type="text", text=resource["text"])
157
- )
118
+ mcp_contents.append(TextContent(type="text", text=resource["text"]))
158
119
  else:
159
120
  mcp_contents.append(
160
121
  EmbeddedResource(
@@ -171,10 +132,7 @@ def _openai_message_to_multipart(
171
132
  mime_type = resource["mimeType"]
172
133
  uri = resource.get("uri", "resource://unknown")
173
134
 
174
- if (
175
- mime_type.startswith("image/")
176
- and mime_type != "image/svg+xml"
177
- ):
135
+ if mime_type.startswith("image/") and mime_type != "image/svg+xml":
178
136
  mcp_contents.append(
179
137
  ImageContent(
180
138
  type="image",
@@ -1,204 +1,30 @@
1
- import json
2
- from typing import Iterable, List
3
- from mcp import CreateMessageResult, SamplingMessage, StopReason
4
- from pydantic import BaseModel
5
- from mcp_agent.workflows.llm.sampling_format_converter import SamplingFormatConverter
6
-
7
- from mcp.types import (
8
- PromptMessage,
9
- EmbeddedResource,
10
- ImageContent,
11
- TextContent,
12
- TextResourceContents,
13
- )
14
-
15
1
  from anthropic.types import (
16
- ContentBlock,
17
- DocumentBlockParam,
18
2
  Message,
19
3
  MessageParam,
20
- ImageBlockParam,
21
- TextBlock,
22
- TextBlockParam,
23
- ToolResultBlockParam,
24
- ToolUseBlockParam,
4
+ )
5
+ from mcp import StopReason
6
+ from mcp.types import (
7
+ PromptMessage,
25
8
  )
26
9
 
27
10
  from mcp_agent.logging.logger import get_logger
11
+ from mcp_agent.workflows.llm.providers.multipart_converter_anthropic import (
12
+ AnthropicConverter,
13
+ )
14
+ from mcp_agent.workflows.llm.sampling_format_converter import ProviderFormatConverter
28
15
 
29
16
  _logger = get_logger(__name__)
30
17
 
31
18
 
32
- class AnthropicSamplingConverter(SamplingFormatConverter[MessageParam, Message]):
19
+ class AnthropicSamplingConverter(ProviderFormatConverter[MessageParam, Message]):
33
20
  """
34
21
  Convert between Anthropic and MCP types.
35
22
  """
36
23
 
37
- @classmethod
38
- def from_sampling_result(cls, result: CreateMessageResult) -> Message:
39
- # -> Message
40
- if result.role != "assistant":
41
- raise ValueError(
42
- f"Expected role to be 'assistant' but got '{result.role}' instead."
43
- )
44
-
45
- return Message(
46
- role="assistant",
47
- type="message",
48
- content=[mcp_content_to_anthropic_content(result.content)],
49
- stop_reason=mcp_stop_reason_to_anthropic_stop_reason(result.stopReason),
50
- model=result.model,
51
- usage={"input_tokens": 0, "output_tokens": 0},
52
- id="sampling_id",
53
- # TODO -- incorporate usage info and message identity
54
- )
55
-
56
- @classmethod
57
- def to_sampling_result(cls, result: Message) -> CreateMessageResult:
58
- contents = anthropic_content_to_mcp_content(result.content)
59
- if len(contents) > 1:
60
- raise NotImplementedError(
61
- "Multiple content elements in a single message are not supported in MCP yet"
62
- )
63
- mcp_content = contents[0]
64
-
65
- # Create a dictionary with required fields
66
- result_dict = {
67
- "role": result.role,
68
- "content": mcp_content,
69
- "model": result.model,
70
- "stopReason": anthropic_stop_reason_to_mcp_stop_reason(result.stop_reason),
71
- }
72
-
73
- # Add any other fields from the original message that might be needed
74
- extras = result.model_dump(exclude={"role", "content", "model", "stop_reason"})
75
- if extras:
76
- # Only include compatible fields to avoid validation errors
77
- # Skip fields that would cause validation issues with CreateMessageResult
78
- safe_extras = {
79
- k: v for k, v in extras.items() if k in CreateMessageResult.model_fields
80
- }
81
- result_dict.update(safe_extras)
82
-
83
- return CreateMessageResult(**result_dict)
84
-
85
- @classmethod
86
- def from_sampling_message(cls, param: SamplingMessage) -> MessageParam:
87
- extras = param.model_dump(exclude={"role", "content"})
88
- return MessageParam(
89
- role=param.role,
90
- content=[mcp_content_to_anthropic_content(param.content)],
91
- **extras,
92
- )
93
-
94
- @classmethod
95
- def to_sampling_message(cls, param: MessageParam) -> SamplingMessage:
96
- # Implement the conversion from ChatCompletionMessage to MCP message param
97
-
98
- contents = anthropic_content_to_mcp_content(param["content"])
99
-
100
- # TODO: saqadri - the mcp_content can have multiple elements
101
- # while sampling message content has a single content element
102
- # Right now we error out if there are > 1 elements in mcp_content
103
- # We need to handle this case properly going forward
104
- if len(contents) > 1:
105
- raise NotImplementedError(
106
- "Multiple content elements in a single message are not supported"
107
- )
108
- mcp_content = contents[0]
109
-
110
- # Only include fields that are valid for SamplingMessage
111
- extras = {
112
- k: v
113
- for k, v in param.items()
114
- if k not in ["role", "content"] and k in SamplingMessage.model_fields
115
- }
116
-
117
- return SamplingMessage(
118
- role=param["role"],
119
- content=mcp_content,
120
- **extras,
121
- )
122
-
123
24
  @classmethod
124
25
  def from_prompt_message(cls, message: PromptMessage) -> MessageParam:
125
26
  """Convert an MCP PromptMessage to an Anthropic MessageParam."""
126
-
127
- # Extract content text
128
- content_text = (
129
- message.content.text
130
- if hasattr(message.content, "text")
131
- else str(message.content)
132
- )
133
-
134
- # Extract extras for flexibility
135
- extras = message.model_dump(exclude={"role", "content"})
136
-
137
- # Handle based on role
138
- if message.role == "user":
139
- return {"role": "user", "content": content_text, **extras}
140
- elif message.role == "assistant":
141
- return {
142
- "role": "assistant",
143
- "content": [{"type": "text", "text": content_text}],
144
- **extras,
145
- }
146
- else:
147
- # Fall back to user for any unrecognized role, including "system"
148
- _logger.warning(
149
- f"Unsupported role '{message.role}' in PromptMessage. Falling back to 'user' role."
150
- )
151
- return {
152
- "role": "user",
153
- "content": f"[{message.role.upper()}] {content_text}",
154
- **extras,
155
- }
156
-
157
-
158
- def anthropic_content_to_mcp_content(
159
- content: str
160
- | Iterable[
161
- TextBlockParam
162
- | ImageBlockParam
163
- | ToolUseBlockParam
164
- | ToolResultBlockParam
165
- | DocumentBlockParam
166
- | ContentBlock
167
- ],
168
- ) -> List[TextContent | ImageContent | EmbeddedResource]:
169
- mcp_content = []
170
-
171
- if isinstance(content, str):
172
- mcp_content.append(TextContent(type="text", text=content))
173
- else:
174
- for block in content:
175
- if block.type == "text":
176
- mcp_content.append(TextContent(type="text", text=block.text))
177
- elif block.type == "image":
178
- raise NotImplementedError("Image content conversion not implemented")
179
- elif block.type == "tool_use":
180
- # Best effort to convert a tool use to text (since there's no ToolUseContent)
181
- mcp_content.append(
182
- TextContent(
183
- type="text",
184
- text=to_string(block),
185
- )
186
- )
187
- elif block.type == "tool_result":
188
- # Best effort to convert a tool result to text (since there's no ToolResultContent)
189
- mcp_content.append(
190
- TextContent(
191
- type="text",
192
- text=to_string(block),
193
- )
194
- )
195
- elif block.type == "document":
196
- raise NotImplementedError("Document content conversion not implemented")
197
- else:
198
- # Last effort to convert the content to a string
199
- mcp_content.append(TextContent(type="text", text=str(block)))
200
-
201
- return mcp_content
27
+ return AnthropicConverter.convert_prompt_message_to_anthropic(message)
202
28
 
203
29
 
204
30
  def mcp_stop_reason_to_anthropic_stop_reason(stop_reason: StopReason):
@@ -218,7 +44,7 @@ def mcp_stop_reason_to_anthropic_stop_reason(stop_reason: StopReason):
218
44
 
219
45
  def anthropic_stop_reason_to_mcp_stop_reason(stop_reason: str) -> StopReason:
220
46
  if not stop_reason:
221
- return None
47
+ return "end_turn"
222
48
  elif stop_reason == "end_turn":
223
49
  return "endTurn"
224
50
  elif stop_reason == "max_tokens":
@@ -229,30 +55,3 @@ def anthropic_stop_reason_to_mcp_stop_reason(stop_reason: str) -> StopReason:
229
55
  return "toolUse"
230
56
  else:
231
57
  return stop_reason
232
-
233
-
234
- def mcp_content_to_anthropic_content(
235
- content: TextContent | ImageContent | EmbeddedResource,
236
- ) -> ContentBlock:
237
- if isinstance(content, TextContent):
238
- return TextBlock(type=content.type, text=content.text)
239
- elif isinstance(content, ImageContent):
240
- # Best effort to convert an image to text (since there's no ImageBlock)
241
- return TextBlock(type="text", text=f"{content.mimeType}:{content.data}")
242
- elif isinstance(content, EmbeddedResource):
243
- if isinstance(content.resource, TextResourceContents):
244
- return TextBlock(type="text", text=content.resource.text)
245
- else: # BlobResourceContents
246
- return TextBlock(
247
- type="text", text=f"{content.resource.mimeType}:{content.resource.blob}"
248
- )
249
- else:
250
- # Last effort to convert the content to a string
251
- return TextBlock(type="text", text=str(content))
252
-
253
-
254
- def to_string(obj: BaseModel | dict) -> str:
255
- if isinstance(obj, BaseModel):
256
- return obj.model_dump_json()
257
- else:
258
- return json.dumps(obj)
@@ -1,229 +1,27 @@
1
- from typing import Iterable
2
- from mcp import CreateMessageResult, SamplingMessage
3
- from openai.types.chat import (
4
- ChatCompletionMessage,
5
- ChatCompletionUserMessageParam,
6
- ChatCompletionAssistantMessageParam,
7
- ChatCompletionMessageParam,
8
- ChatCompletionContentPartTextParam,
9
- ChatCompletionContentPartParam,
10
- ChatCompletionContentPartRefusalParam,
11
- )
1
+ from typing import Any, Dict
12
2
 
13
3
  from mcp.types import (
14
4
  PromptMessage,
15
- TextContent,
16
- ImageContent,
17
- EmbeddedResource,
18
- TextResourceContents,
19
5
  )
20
-
21
- from mcp_agent.workflows.llm.sampling_format_converter import (
22
- SamplingFormatConverter,
23
- typed_dict_extras,
6
+ from openai.types.chat import (
7
+ ChatCompletionMessage,
24
8
  )
25
9
 
26
10
  from mcp_agent.logging.logger import get_logger
11
+ from mcp_agent.workflows.llm.sampling_format_converter import (
12
+ ProviderFormatConverter,
13
+ )
27
14
 
28
15
  _logger = get_logger(__name__)
29
16
 
30
17
 
31
- class OpenAISamplingConverter(
32
- SamplingFormatConverter[ChatCompletionMessageParam, ChatCompletionMessage]
33
- ):
34
- """
35
- Convert between OpenAI and MCP types.
36
- """
37
-
38
- @classmethod
39
- def from_sampling_result(cls, result: CreateMessageResult) -> ChatCompletionMessage:
40
- """Convert an MCP message result to an OpenAI ChatCompletionMessage."""
41
- # Basic implementation - would need to be expanded
42
-
43
- if result.role != "assistant":
44
- raise ValueError(
45
- f"Expected role to be 'assistant' but got '{result.role}' instead."
46
- )
47
- # TODO -- add image support for sampling
48
- return ChatCompletionMessage(
49
- role=result.role,
50
- content=result.content.text or "image",
51
- )
52
-
53
- @classmethod
54
- def to_sampling_result(cls, result: ChatCompletionMessage) -> CreateMessageResult:
55
- """Convert an OpenAI ChatCompletionMessage to an MCP message result."""
56
- content = result.content
57
- if content is None:
58
- content = ""
59
-
60
- return CreateMessageResult(
61
- role=result.role,
62
- content=TextContent(type="text", text=content),
63
- model="unknown", # Model is required by CreateMessageResult
64
- )
65
-
66
- @classmethod
67
- def from_sampling_message(
68
- cls, param: SamplingMessage
69
- ) -> ChatCompletionMessageParam:
70
- if param.role == "assistant":
71
- return ChatCompletionAssistantMessageParam(
72
- role="assistant",
73
- content=mcp_to_openai_blocks(param.content),
74
- )
75
- elif param.role == "user":
76
- return ChatCompletionUserMessageParam(
77
- role="user",
78
- content=mcp_to_openai_blocks(param.content),
79
- )
80
- else:
81
- raise ValueError(
82
- f"Unexpected role: {param.role}, MCP only supports 'assistant' and 'user'"
83
- )
84
-
18
+ class OpenAISamplingConverter(ProviderFormatConverter[Dict[str, Any], ChatCompletionMessage]):
85
19
  @classmethod
86
- def to_sampling_message(cls, param: ChatCompletionMessageParam) -> SamplingMessage:
87
- contents = openai_to_mcp_blocks(param)
88
-
89
- # TODO: saqadri - the mcp_content can have multiple elements
90
- # while sampling message content has a single content element
91
- # Right now we error out if there are > 1 elements in mcp_content
92
- # We need to handle this case properly going forward
93
- if len(contents) > 1:
94
- raise NotImplementedError(
95
- "Multiple content elements in a single message are not supported"
96
- )
97
- mcp_content: TextContent | ImageContent | EmbeddedResource = contents[0]
98
-
99
- if param["role"] == "assistant":
100
- return SamplingMessage(
101
- role="assistant",
102
- content=mcp_content,
103
- **typed_dict_extras(param, ["role", "content"]),
104
- )
105
- elif param["role"] == "user":
106
- return SamplingMessage(
107
- role="user",
108
- content=mcp_content,
109
- **typed_dict_extras(param, ["role", "content"]),
110
- )
111
- elif param.role == "tool":
112
- raise NotImplementedError(
113
- "Tool messages are not supported in SamplingMessage yet"
114
- )
115
- elif param.role == "system":
116
- raise NotImplementedError(
117
- "System messages are not supported in SamplingMessage yet"
118
- )
119
- elif param.role == "developer":
120
- raise NotImplementedError(
121
- "Developer messages are not supported in SamplingMessage yet"
122
- )
123
- elif param.role == "function":
124
- raise NotImplementedError(
125
- "Function messages are not supported in SamplingMessage yet"
126
- )
127
- else:
128
- raise ValueError(
129
- f"Unexpected role: {param.role}, MCP only supports 'assistant', 'user', 'tool', 'system', 'developer', and 'function'"
130
- )
131
-
132
- @classmethod
133
- def from_prompt_message(cls, message: PromptMessage) -> ChatCompletionMessageParam:
134
- """Convert an MCP PromptMessage to an OpenAI ChatCompletionMessageParam."""
135
- content_text = (
136
- message.content.text
137
- if hasattr(message.content, "text")
138
- else str(message.content)
139
- )
140
-
141
- return {
142
- "role": message.role,
143
- "content": content_text,
144
- }
145
-
146
-
147
- def mcp_to_openai_blocks(
148
- content: TextContent | ImageContent | EmbeddedResource,
149
- ) -> ChatCompletionContentPartTextParam:
150
- if isinstance(content, list):
151
- # Handle list of content items
152
- return ChatCompletionContentPartTextParam(
153
- type="text",
154
- text="\n".join(mcp_to_openai_blocks(c) for c in content),
155
- )
156
-
157
- if isinstance(content, TextContent):
158
- return ChatCompletionContentPartTextParam(type="text", text=content.text)
159
- elif isinstance(content, ImageContent):
160
- # Best effort to convert an image to text
161
- return ChatCompletionContentPartTextParam(
162
- type="text", text=f"{content.mimeType}:{content.data}"
20
+ def from_prompt_message(cls, message: PromptMessage) -> Dict[str, Any]:
21
+ """Convert an MCP PromptMessage to an OpenAI message dict."""
22
+ from mcp_agent.workflows.llm.providers.multipart_converter_openai import (
23
+ OpenAIConverter,
163
24
  )
164
- elif isinstance(content, EmbeddedResource):
165
- if isinstance(content.resource, TextResourceContents):
166
- return ChatCompletionContentPartTextParam(
167
- type="text", text=content.resource.text
168
- )
169
- else: # BlobResourceContents
170
- return ChatCompletionContentPartTextParam(
171
- type="text", text=f"{content.resource.mimeType}:{content.resource.blob}"
172
- )
173
- else:
174
- # Last effort to convert the content to a string
175
- return ChatCompletionContentPartTextParam(type="text", text=str(content))
176
-
177
-
178
- def openai_to_mcp_blocks(
179
- content: str
180
- | Iterable[ChatCompletionContentPartParam | ChatCompletionContentPartRefusalParam],
181
- ) -> Iterable[TextContent | ImageContent | EmbeddedResource]:
182
- mcp_content = []
183
-
184
- if isinstance(content, str):
185
- mcp_content = [TextContent(type="text", text=content)]
186
-
187
- else:
188
- mcp_content = [TextContent(type="text", text=content["content"])]
189
-
190
- return mcp_content
191
-
192
- # # TODO: saqadri - this is a best effort conversion, we should handle all possible content types
193
- # for c in content["content"]:
194
- # # TODO: evalstate, need to go through all scenarios here
195
- # if isinstance(c, str):
196
- # mcp_content.append(TextContent(type="text", text=c))
197
- # break
198
25
 
199
- # if c.type == "text": # isinstance(c, ChatCompletionContentPartTextParam):
200
- # mcp_content.append(
201
- # TextContent(
202
- # type="text", text=c.text, **typed_dict_extras(c, ["text"])
203
- # )
204
- # )
205
- # elif (
206
- # c.type == "image_url"
207
- # ): # isinstance(c, ChatCompletionContentPartImageParam):
208
- # raise NotImplementedError("Image content conversion not implemented")
209
- # # TODO: saqadri - need to download the image into a base64-encoded string
210
- # # Download image from c.image_url
211
- # # return ImageContent(
212
- # # type="image",
213
- # # data=downloaded_image,
214
- # # **c
215
- # # )
216
- # elif (
217
- # c.type == "input_audio"
218
- # ): # isinstance(c, ChatCompletionContentPartInputAudioParam):
219
- # raise NotImplementedError("Audio content conversion not implemented")
220
- # elif (
221
- # c.type == "refusal"
222
- # ): # isinstance(c, ChatCompletionContentPartRefusalParam):
223
- # mcp_content.append(
224
- # TextContent(
225
- # type="text", text=c.refusal, **typed_dict_extras(c, ["refusal"])
226
- # )
227
- # )
228
- # else:
229
- # raise ValueError(f"Unexpected content type: {c.type}")
26
+ # Use the full-featured OpenAI converter for consistent handling
27
+ return OpenAIConverter.convert_prompt_message_to_openai(message)