fast-agent-mcp 0.2.16__py3-none-any.whl → 0.2.17__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 (44) hide show
  1. {fast_agent_mcp-0.2.16.dist-info → fast_agent_mcp-0.2.17.dist-info}/METADATA +4 -6
  2. {fast_agent_mcp-0.2.16.dist-info → fast_agent_mcp-0.2.17.dist-info}/RECORD +43 -43
  3. mcp_agent/agents/base_agent.py +50 -6
  4. mcp_agent/agents/workflow/orchestrator_agent.py +6 -7
  5. mcp_agent/agents/workflow/router_agent.py +70 -136
  6. mcp_agent/app.py +1 -124
  7. mcp_agent/cli/commands/setup.py +1 -1
  8. mcp_agent/config.py +16 -13
  9. mcp_agent/context.py +4 -22
  10. mcp_agent/core/agent_types.py +2 -2
  11. mcp_agent/core/direct_decorators.py +2 -2
  12. mcp_agent/core/direct_factory.py +2 -1
  13. mcp_agent/core/fastagent.py +1 -1
  14. mcp_agent/core/request_params.py +5 -1
  15. mcp_agent/executor/workflow_signal.py +0 -2
  16. mcp_agent/llm/augmented_llm.py +183 -57
  17. mcp_agent/llm/augmented_llm_passthrough.py +1 -1
  18. mcp_agent/llm/augmented_llm_playback.py +21 -1
  19. mcp_agent/llm/memory.py +3 -3
  20. mcp_agent/llm/model_factory.py +3 -1
  21. mcp_agent/llm/provider_key_manager.py +1 -0
  22. mcp_agent/llm/provider_types.py +2 -1
  23. mcp_agent/llm/providers/augmented_llm_anthropic.py +49 -10
  24. mcp_agent/llm/providers/augmented_llm_deepseek.py +0 -2
  25. mcp_agent/llm/providers/augmented_llm_google.py +30 -0
  26. mcp_agent/llm/providers/augmented_llm_openai.py +95 -158
  27. mcp_agent/llm/providers/multipart_converter_openai.py +10 -27
  28. mcp_agent/llm/providers/sampling_converter_openai.py +5 -6
  29. mcp_agent/mcp/interfaces.py +6 -1
  30. mcp_agent/mcp/mcp_aggregator.py +2 -8
  31. mcp_agent/mcp/prompt_message_multipart.py +25 -2
  32. mcp_agent/resources/examples/data-analysis/analysis-campaign.py +2 -2
  33. mcp_agent/resources/examples/in_dev/agent_build.py +1 -1
  34. mcp_agent/resources/examples/internal/job.py +1 -1
  35. mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +1 -1
  36. mcp_agent/resources/examples/prompting/agent.py +0 -2
  37. mcp_agent/resources/examples/prompting/fastagent.config.yaml +2 -3
  38. mcp_agent/resources/examples/researcher/fastagent.config.yaml +1 -6
  39. mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -1
  40. mcp_agent/resources/examples/workflows/parallel.py +1 -1
  41. mcp_agent/executor/decorator_registry.py +0 -112
  42. {fast_agent_mcp-0.2.16.dist-info → fast_agent_mcp-0.2.17.dist-info}/WHEEL +0 -0
  43. {fast_agent_mcp-0.2.16.dist-info → fast_agent_mcp-0.2.17.dist-info}/entry_points.txt +0 -0
  44. {fast_agent_mcp-0.2.16.dist-info → fast_agent_mcp-0.2.17.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
- from typing import List, Tuple, Type
1
+ from typing import Dict, List
2
2
 
3
3
  from mcp.types import (
4
4
  CallToolRequest,
@@ -16,7 +16,6 @@ from openai.types.chat import (
16
16
  ChatCompletionMessageParam,
17
17
  ChatCompletionSystemMessageParam,
18
18
  ChatCompletionToolParam,
19
- ChatCompletionUserMessageParam,
20
19
  )
21
20
  from pydantic_core import from_json
22
21
  from rich.text import Text
@@ -25,11 +24,10 @@ from mcp_agent.core.exceptions import ProviderKeyError
25
24
  from mcp_agent.core.prompt import Prompt
26
25
  from mcp_agent.llm.augmented_llm import (
27
26
  AugmentedLLM,
28
- ModelT,
29
27
  RequestParams,
30
28
  )
31
29
  from mcp_agent.llm.provider_types import Provider
32
- from mcp_agent.llm.providers.multipart_converter_openai import OpenAIConverter
30
+ from mcp_agent.llm.providers.multipart_converter_openai import OpenAIConverter, OpenAIMessage
33
31
  from mcp_agent.llm.providers.sampling_converter_openai import (
34
32
  OpenAISamplingConverter,
35
33
  )
@@ -38,7 +36,7 @@ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
38
36
 
39
37
  _logger = get_logger(__name__)
40
38
 
41
- DEFAULT_OPENAI_MODEL = "gpt-4o"
39
+ DEFAULT_OPENAI_MODEL = "gpt-4.1-mini"
42
40
  DEFAULT_REASONING_EFFORT = "medium"
43
41
 
44
42
 
@@ -49,6 +47,17 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
49
47
  This implementation uses OpenAI's ChatCompletion as the LLM.
50
48
  """
51
49
 
50
+ # OpenAI-specific parameter exclusions
51
+ OPENAI_EXCLUDE_FIELDS = {
52
+ AugmentedLLM.PARAM_MESSAGES,
53
+ AugmentedLLM.PARAM_MODEL,
54
+ AugmentedLLM.PARAM_MAX_TOKENS,
55
+ AugmentedLLM.PARAM_SYSTEM_PROMPT,
56
+ AugmentedLLM.PARAM_PARALLEL_TOOL_CALLS,
57
+ AugmentedLLM.PARAM_USE_HISTORY,
58
+ AugmentedLLM.PARAM_MAX_ITERATIONS,
59
+ }
60
+
52
61
  def __init__(self, provider: Provider = Provider.OPENAI, *args, **kwargs) -> None:
53
62
  # Set type_converter before calling super().__init__
54
63
  if "type_converter" not in kwargs:
@@ -68,6 +77,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
68
77
  self._reasoning_effort = self.context.config.openai.reasoning_effort
69
78
 
70
79
  # Determine if we're using a reasoning model
80
+ # TODO -- move this to model capabiltities, add o4.
71
81
  chosen_model = self.default_request_params.model if self.default_request_params else None
72
82
  self._reasoning = chosen_model and (
73
83
  chosen_model.startswith("o3") or chosen_model.startswith("o1")
@@ -92,9 +102,19 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
92
102
  def _base_url(self) -> str:
93
103
  return self.context.config.openai.base_url if self.context.config.openai else None
94
104
 
95
- async def generate_internal(
105
+ def _openai_client(self) -> OpenAI:
106
+ try:
107
+ return OpenAI(api_key=self._api_key(), base_url=self._base_url())
108
+ except AuthenticationError as e:
109
+ raise ProviderKeyError(
110
+ "Invalid OpenAI API key",
111
+ "The configured OpenAI API key was rejected.\n"
112
+ "Please check that your API key is valid and not expired.",
113
+ ) from e
114
+
115
+ async def _openai_completion(
96
116
  self,
97
- message,
117
+ message: OpenAIMessage,
98
118
  request_params: RequestParams | None = None,
99
119
  ) -> List[TextContent | ImageContent | EmbeddedResource]:
100
120
  """
@@ -103,31 +123,18 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
103
123
  Override this method to use a different LLM.
104
124
  """
105
125
 
106
- try:
107
- openai_client = OpenAI(api_key=self._api_key(), base_url=self._base_url())
108
- messages: List[ChatCompletionMessageParam] = []
109
- params = self.get_request_params(request_params)
110
- except AuthenticationError as e:
111
- raise ProviderKeyError(
112
- "Invalid OpenAI API key",
113
- "The configured OpenAI API key was rejected.\n"
114
- "Please check that your API key is valid and not expired.",
115
- ) from e
126
+ request_params = self.get_request_params(request_params=request_params)
127
+
128
+ responses: List[TextContent | ImageContent | EmbeddedResource] = []
116
129
 
117
- system_prompt = self.instruction or params.systemPrompt
130
+ # TODO -- move this in to agent context management / agent group handling
131
+ messages: List[ChatCompletionMessageParam] = []
132
+ system_prompt = self.instruction or request_params.systemPrompt
118
133
  if system_prompt:
119
134
  messages.append(ChatCompletionSystemMessageParam(role="system", content=system_prompt))
120
135
 
121
- # Always include prompt messages, but only include conversation history
122
- # if use_history is True
123
- messages.extend(self.history.get(include_history=params.use_history))
124
-
125
- if isinstance(message, str):
126
- messages.append(ChatCompletionUserMessageParam(role="user", content=message))
127
- elif isinstance(message, list):
128
- messages.extend(message)
129
- else:
130
- messages.append(message)
136
+ messages.extend(self.history.get(include_completion_history=request_params.use_history))
137
+ messages.append(message)
131
138
 
132
139
  response = await self.aggregator.list_tools()
133
140
  available_tools: List[ChatCompletionToolParam] | None = [
@@ -135,57 +142,37 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
135
142
  type="function",
136
143
  function={
137
144
  "name": tool.name,
138
- "description": tool.description,
145
+ "description": tool.description if tool.description else "",
139
146
  "parameters": tool.inputSchema,
140
- # TODO: saqadri - determine if we should specify "strict" to True by default
141
147
  },
142
148
  )
143
149
  for tool in response.tools
144
150
  ]
151
+
145
152
  if not available_tools:
146
153
  available_tools = None # deepseek does not allow empty array
147
154
 
148
- responses: List[TextContent | ImageContent | EmbeddedResource] = []
149
- model = self.default_request_params.model
150
-
151
- # we do NOT send stop sequences as this causes errors with mutlimodal processing
152
- for i in range(params.max_iterations):
153
- arguments = {
154
- "model": model or "gpt-4o",
155
- "messages": messages,
156
- "tools": available_tools,
157
- }
158
- if self._reasoning:
159
- arguments = {
160
- **arguments,
161
- "max_completion_tokens": params.maxTokens,
162
- "reasoning_effort": self._reasoning_effort,
163
- }
164
- else:
165
- arguments = {**arguments, "max_tokens": params.maxTokens}
166
- if available_tools:
167
- arguments["parallel_tool_calls"] = params.parallel_tool_calls
168
-
169
- if params.metadata:
170
- arguments = {**arguments, **params.metadata}
155
+ # we do NOT send "stop sequences" as this causes errors with mutlimodal processing
156
+ for i in range(request_params.max_iterations):
157
+ arguments = self._prepare_api_request(messages, available_tools, request_params)
158
+ self.logger.debug(f"OpenAI completion requested for: {arguments}")
171
159
 
172
- self.logger.debug(f"{arguments}")
173
- self._log_chat_progress(self.chat_turn(), model=model)
160
+ self._log_chat_progress(self.chat_turn(), model=self.default_request_params.model)
174
161
 
175
162
  executor_result = await self.executor.execute(
176
- openai_client.chat.completions.create, **arguments
163
+ self._openai_client().chat.completions.create, **arguments
177
164
  )
178
165
 
179
166
  response = executor_result[0]
180
167
 
181
168
  self.logger.debug(
182
- "OpenAI ChatCompletion response:",
169
+ "OpenAI completion response:",
183
170
  data=response,
184
171
  )
185
172
 
186
173
  if isinstance(response, AuthenticationError):
187
174
  raise ProviderKeyError(
188
- "Invalid OpenAI API key",
175
+ "Rejected OpenAI API key",
189
176
  "The configured OpenAI API key was rejected.\n"
190
177
  "Please check that your API key is valid and not expired.",
191
178
  ) from response
@@ -203,8 +190,9 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
203
190
  if message.content:
204
191
  responses.append(TextContent(type="text", text=message.content))
205
192
 
206
- converted_message = self.convert_message_to_message_param(message, name=self.name)
193
+ converted_message = self.convert_message_to_message_param(message)
207
194
  messages.append(converted_message)
195
+
208
196
  message_text = converted_message.content
209
197
  if choice.finish_reason in ["tool_calls", "function_call"] and message.tool_calls:
210
198
  if message_text:
@@ -262,14 +250,12 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
262
250
  )
263
251
 
264
252
  await self.show_assistant_message(message_text)
265
- # TODO: saqadri - would be useful to return the reason for stopping to the caller
266
253
  break
267
254
  elif choice.finish_reason == "content_filter":
268
255
  # The response was filtered by the content filter
269
256
  self.logger.debug(
270
257
  f"Iteration {i}: Stopping because finish_reason is 'content_filter'"
271
258
  )
272
- # TODO: saqadri - would be useful to return the reason for stopping to the caller
273
259
  break
274
260
  elif choice.finish_reason == "stop":
275
261
  self.logger.debug(f"Iteration {i}: Stopping because finish_reason is 'stop'")
@@ -277,11 +263,9 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
277
263
  await self.show_assistant_message(message_text, "")
278
264
  break
279
265
 
280
- # Only save the new conversation messages to history if use_history is true
281
- # Keep the prompt messages separate
282
- if params.use_history:
266
+ if request_params.use_history:
283
267
  # Get current prompt messages
284
- prompt_messages = self.history.get(include_history=False)
268
+ prompt_messages = self.history.get(include_completion_history=False)
285
269
 
286
270
  # Calculate new conversation messages (excluding prompts)
287
271
  new_messages = messages[len(prompt_messages) :]
@@ -289,7 +273,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
289
273
  # Update conversation history
290
274
  self.history.set(new_messages)
291
275
 
292
- self._log_chat_finished(model=model)
276
+ self._log_chat_finished(model=self.default_request_params.model)
293
277
 
294
278
  return responses
295
279
 
@@ -297,111 +281,34 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
297
281
  self,
298
282
  multipart_messages: List["PromptMessageMultipart"],
299
283
  request_params: RequestParams | None = None,
284
+ is_template: bool = False,
300
285
  ) -> PromptMessageMultipart:
301
- # TODO -- this is very similar to Anthropic (just the converter class changes).
302
- # TODO -- potential refactor to base class, standardize Converter interface
303
- # Check the last message role
304
286
  last_message = multipart_messages[-1]
305
287
 
306
288
  # Add all previous messages to history (or all messages if last is from assistant)
289
+ # if the last message is a "user" inference is required
307
290
  messages_to_add = (
308
291
  multipart_messages[:-1] if last_message.role == "user" else multipart_messages
309
292
  )
310
293
  converted = []
311
294
  for msg in messages_to_add:
312
295
  converted.append(OpenAIConverter.convert_to_openai(msg))
313
- self.history.extend(converted, is_prompt=True)
314
-
315
- if last_message.role == "user":
316
- # For user messages: Generate response to the last one
317
- self.logger.debug("Last message in prompt is from user, generating assistant response")
318
- message_param = OpenAIConverter.convert_to_openai(last_message)
319
- responses: List[
320
- TextContent | ImageContent | EmbeddedResource
321
- ] = await self.generate_internal(
322
- message_param,
323
- request_params,
324
- )
325
- return Prompt.assistant(*responses)
326
- else:
327
- # For assistant messages: Return the last message content as text
328
- self.logger.debug("Last message in prompt is from assistant, returning it directly")
329
- return last_message
330
296
 
331
- async def structured(
332
- self,
333
- prompt: List[PromptMessageMultipart],
334
- model: Type[ModelT],
335
- request_params: RequestParams | None = None,
336
- ) -> Tuple[ModelT | None, PromptMessageMultipart]:
337
- """
338
- Apply the prompt and return the result as a Pydantic model.
339
-
340
- Uses OpenAI's beta parse feature when compatible, falling back to standard
341
- JSON parsing if the beta feature fails or is unavailable.
342
-
343
- Args:
344
- prompt: List of messages to process
345
- model: Pydantic model to parse the response into
346
- request_params: Optional request parameters
347
-
348
- Returns:
349
- The parsed response as a Pydantic model, or None if parsing fails
350
- """
351
-
352
- if not Provider.OPENAI == self.provider:
353
- return await super().structured(prompt, model, request_params)
354
-
355
- logger = get_logger(__name__)
356
-
357
- # First try to use OpenAI's beta.chat.completions.parse feature
358
- try:
359
- # Convert the multipart messages to OpenAI format
360
- messages = []
361
- for msg in prompt:
362
- messages.append(OpenAIConverter.convert_to_openai(msg))
363
-
364
- # Add system prompt if available and not already present
365
- if self.instruction and not any(m.get("role") == "system" for m in messages):
366
- system_msg = ChatCompletionSystemMessageParam(
367
- role="system", content=self.instruction
368
- )
369
- messages.insert(0, system_msg)
370
- model_name = self.default_request_params.model
371
- self.show_user_message(prompt[-1].first_text(), model_name, self.chat_turn())
372
- # Use the beta parse feature
373
- try:
374
- openai_client = OpenAI(api_key=self._api_key(), base_url=self._base_url())
375
- model_name = self.default_request_params.model
376
-
377
- logger.debug(
378
- f"Using OpenAI beta parse with model {model_name} for structured output"
379
- )
380
- response = await self.executor.execute(
381
- openai_client.beta.chat.completions.parse,
382
- model=model_name,
383
- messages=messages,
384
- response_format=model,
385
- )
386
-
387
- if response and isinstance(response[0], BaseException):
388
- raise response[0]
389
- parsed_result = response[0].choices[0].message
390
- await self.show_assistant_message(parsed_result.content)
391
- logger.debug("Successfully used OpenAI beta parse feature for structured output")
392
- return parsed_result.parsed, Prompt.assistant(parsed_result.content)
297
+ # TODO -- this looks like a defect from previous apply_prompt implementation.
298
+ self.history.extend(converted, is_prompt=is_template)
393
299
 
394
- except (ImportError, AttributeError, NotImplementedError) as e:
395
- # Beta feature not available, log and continue to fallback
396
- logger.debug(f"OpenAI beta parse feature not available: {str(e)}")
397
- # Continue to fallback
398
-
399
- except Exception as e:
400
- logger.debug(f"OpenAI beta parse failed: {str(e)}, falling back to standard method")
401
- # Continue to standard method as fallback
300
+ if "assistant" == last_message.role:
301
+ return last_message
402
302
 
403
- # Fallback to standard method (inheriting from base class)
404
- return await super().structured(prompt, model, request_params)
303
+ # For assistant messages: Return the last message (no completion needed)
304
+ message_param: OpenAIMessage = OpenAIConverter.convert_to_openai(last_message)
305
+ responses: List[
306
+ TextContent | ImageContent | EmbeddedResource
307
+ ] = await self._openai_completion(
308
+ message_param,
309
+ request_params,
310
+ )
311
+ return Prompt.assistant(*responses)
405
312
 
406
313
  async def pre_tool_call(self, tool_call_id: str | None, request: CallToolRequest):
407
314
  return request
@@ -410,3 +317,33 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
410
317
  self, tool_call_id: str | None, request: CallToolRequest, result: CallToolResult
411
318
  ):
412
319
  return result
320
+
321
+ def _prepare_api_request(
322
+ self, messages, tools, request_params: RequestParams
323
+ ) -> dict[str, str]:
324
+ # Create base arguments dictionary
325
+
326
+ # overriding model via request params not supported (intentional)
327
+ base_args = {
328
+ "model": self.default_request_params.model,
329
+ "messages": messages,
330
+ "tools": tools,
331
+ }
332
+
333
+ if self._reasoning:
334
+ base_args.update(
335
+ {
336
+ "max_completion_tokens": request_params.maxTokens,
337
+ "reasoning_effort": self._reasoning_effort,
338
+ }
339
+ )
340
+ else:
341
+ base_args["max_tokens"] = request_params.maxTokens
342
+
343
+ if tools:
344
+ base_args["parallel_tool_calls"] = request_params.parallel_tool_calls
345
+
346
+ arguments: Dict[str, str] = self.prepare_provider_arguments(
347
+ base_args, request_params, self.OPENAI_EXCLUDE_FIELDS.union(self.BASE_EXCLUDE_FIELDS)
348
+ )
349
+ return arguments
@@ -7,6 +7,7 @@ from mcp.types import (
7
7
  PromptMessage,
8
8
  TextContent,
9
9
  )
10
+ from openai.types.chat import ChatCompletionMessageParam
10
11
 
11
12
  from mcp_agent.logging.logger import get_logger
12
13
  from mcp_agent.mcp.helpers.content_helpers import (
@@ -23,7 +24,6 @@ from mcp_agent.mcp.mime_utils import (
23
24
  is_text_mime_type,
24
25
  )
25
26
  from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
26
- from mcp_agent.mcp.prompts.prompt_helpers import MessageContent
27
27
  from mcp_agent.mcp.resource_utils import extract_title_from_uri
28
28
 
29
29
  _logger = get_logger("multipart_converter_openai")
@@ -54,7 +54,7 @@ class OpenAIConverter:
54
54
  @staticmethod
55
55
  def convert_to_openai(
56
56
  multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False
57
- ) -> OpenAIMessage:
57
+ ) -> Dict[str, str | ContentBlock | List[ContentBlock]]:
58
58
  """
59
59
  Convert a PromptMessageMultipart message to OpenAI API format.
60
60
 
@@ -71,11 +71,9 @@ class OpenAIConverter:
71
71
  if not multipart_msg.content:
72
72
  return {"role": role, "content": ""}
73
73
 
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="")
78
- return {"role": role, "content": content_text}
74
+ # single text block
75
+ if 1 == len(multipart_msg.content) and is_text_content(multipart_msg.content[0]):
76
+ return {"role": role, "content": get_text(multipart_msg.content[0])}
79
77
 
80
78
  # For user messages, convert each content block
81
79
  content_blocks: List[ContentBlock] = []
@@ -94,12 +92,6 @@ class OpenAIConverter:
94
92
  if block:
95
93
  content_blocks.append(block)
96
94
 
97
- # Handle input_audio if implemented
98
- elif hasattr(item, "type") and getattr(item, "type") == "input_audio":
99
- _logger.warning("Input audio content not supported in standard OpenAI types")
100
- fallback_text = "[Audio content not directly supported]"
101
- content_blocks.append({"type": "text", "text": fallback_text})
102
-
103
95
  else:
104
96
  _logger.warning(f"Unsupported content type: {type(item)}")
105
97
  # Create a text block with information about the skipped content
@@ -112,18 +104,9 @@ class OpenAIConverter:
112
104
  fallback_text = f"[Content conversion error: {str(e)}]"
113
105
  content_blocks.append({"type": "text", "text": fallback_text})
114
106
 
115
- # Special case: empty content list or only empty text blocks
116
107
  if not content_blocks:
117
108
  return {"role": role, "content": ""}
118
109
 
119
- # If we only have one text content and it's empty, return an empty string for content
120
- if (
121
- len(content_blocks) == 1
122
- and content_blocks[0]["type"] == "text"
123
- and not content_blocks[0]["text"]
124
- ):
125
- return {"role": role, "content": ""}
126
-
127
110
  # If concatenate_text_blocks is True, combine adjacent text blocks
128
111
  if concatenate_text_blocks:
129
112
  content_blocks = OpenAIConverter._concatenate_text_blocks(content_blocks)
@@ -172,7 +155,7 @@ class OpenAIConverter:
172
155
  @staticmethod
173
156
  def convert_prompt_message_to_openai(
174
157
  message: PromptMessage, concatenate_text_blocks: bool = False
175
- ) -> OpenAIMessage:
158
+ ) -> ChatCompletionMessageParam:
176
159
  """
177
160
  Convert a standard PromptMessage to OpenAI API format.
178
161
 
@@ -194,7 +177,7 @@ class OpenAIConverter:
194
177
  """Convert ImageContent to OpenAI image_url content block."""
195
178
  # Get image data using helper
196
179
  image_data = get_image_data(content)
197
-
180
+
198
181
  # OpenAI requires image URLs or data URIs for images
199
182
  image_url = {"url": f"data:{content.mimeType};base64,{image_data}"}
200
183
 
@@ -256,8 +239,8 @@ class OpenAIConverter:
256
239
  if OpenAIConverter._is_supported_image_type(mime_type):
257
240
  if is_url and uri_str:
258
241
  return {"type": "image_url", "image_url": {"url": uri_str}}
259
-
260
- # Try to get image data
242
+
243
+ # Try to get image data
261
244
  image_data = get_image_data(resource)
262
245
  if image_data:
263
246
  return {
@@ -462,4 +445,4 @@ class OpenAIConverter:
462
445
  # Single message case (text-only)
463
446
  messages.append(converted)
464
447
 
465
- return messages
448
+ return messages
@@ -1,11 +1,8 @@
1
- from typing import Any, Dict
2
1
 
3
2
  from mcp.types import (
4
3
  PromptMessage,
5
4
  )
6
- from openai.types.chat import (
7
- ChatCompletionMessage,
8
- )
5
+ from openai.types.chat import ChatCompletionMessage, ChatCompletionMessageParam
9
6
 
10
7
  from mcp_agent.llm.sampling_format_converter import (
11
8
  ProviderFormatConverter,
@@ -15,9 +12,11 @@ from mcp_agent.logging.logger import get_logger
15
12
  _logger = get_logger(__name__)
16
13
 
17
14
 
18
- class OpenAISamplingConverter(ProviderFormatConverter[Dict[str, Any], ChatCompletionMessage]):
15
+ class OpenAISamplingConverter(
16
+ ProviderFormatConverter[ChatCompletionMessageParam, ChatCompletionMessage]
17
+ ):
19
18
  @classmethod
20
- def from_prompt_message(cls, message: PromptMessage) -> Dict[str, Any]:
19
+ def from_prompt_message(cls, message: PromptMessage) -> ChatCompletionMessageParam:
21
20
  """Convert an MCP PromptMessage to an OpenAI message dict."""
22
21
  from mcp_agent.llm.providers.multipart_converter_openai import (
23
22
  OpenAIConverter,
@@ -20,6 +20,7 @@ from typing import (
20
20
  runtime_checkable,
21
21
  )
22
22
 
23
+ from a2a_types.types import AgentCard
23
24
  from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
24
25
  from deprecated import deprecated
25
26
  from mcp import ClientSession
@@ -96,7 +97,7 @@ class AugmentedLLMProtocol(Protocol):
96
97
 
97
98
  async def structured(
98
99
  self,
99
- prompt: List[PromptMessageMultipart],
100
+ multipart_messages: List[PromptMessageMultipart],
100
101
  model: Type[ModelT],
101
102
  request_params: RequestParams | None = None,
102
103
  ) -> Tuple[ModelT | None, PromptMessageMultipart]:
@@ -190,6 +191,10 @@ class AgentProtocol(AugmentedLLMProtocol, Protocol):
190
191
  """Send a message with an attached MCP resource"""
191
192
  ...
192
193
 
194
+ async def agent_card(self) -> AgentCard:
195
+ """Return an A2A Agent Card for this Agent"""
196
+ ...
197
+
193
198
  async def initialize(self) -> None:
194
199
  """Initialize the agent and connect to MCP servers"""
195
200
  ...
@@ -482,7 +482,7 @@ class MCPAggregator(ContextDependent):
482
482
 
483
483
  async def get_prompt(
484
484
  self,
485
- prompt_name: str | None,
485
+ prompt_name: str,
486
486
  arguments: dict[str, str] | None = None,
487
487
  server_name: str | None = None,
488
488
  ) -> GetPromptResult:
@@ -502,13 +502,7 @@ class MCPAggregator(ContextDependent):
502
502
  await self.load_servers()
503
503
 
504
504
  # Handle the case where prompt_name is None
505
- if not prompt_name:
506
- if server_name is None:
507
- server_name = self.server_names[0] if self.server_names else None
508
- local_prompt_name = None
509
- namespaced_name = None
510
- # Handle namespaced prompt name
511
- elif SEP in prompt_name and server_name is None:
505
+ if SEP in prompt_name and server_name is None:
512
506
  server_name, local_prompt_name = prompt_name.split(SEP, 1)
513
507
  namespaced_name = prompt_name # Already namespaced
514
508
  # Plain prompt name - use provided server or search
@@ -58,7 +58,7 @@ class PromptMessageMultipart(BaseModel):
58
58
 
59
59
  def first_text(self) -> str:
60
60
  """
61
- Get the first available text content from a message.
61
+ Get the first available text content from a message. Note this could be tool content etc.
62
62
 
63
63
  Args:
64
64
  message: A PromptMessage or PromptMessageMultipart
@@ -73,6 +73,24 @@ class PromptMessageMultipart(BaseModel):
73
73
 
74
74
  return "<no text>"
75
75
 
76
+ def last_text(self) -> str:
77
+ """
78
+ Get the last available text content from a message. This will usually be the final
79
+ generation from the Assistant.
80
+
81
+ Args:
82
+ message: A PromptMessage or PromptMessageMultipart
83
+
84
+ Returns:
85
+ First text content or None if no text content exists
86
+ """
87
+ for content in reversed(self.content):
88
+ text = get_text(content)
89
+ if text is not None:
90
+ return text
91
+
92
+ return "<no text>"
93
+
76
94
  def all_text(self) -> str:
77
95
  """
78
96
  Get all the text available.
@@ -91,6 +109,11 @@ class PromptMessageMultipart(BaseModel):
91
109
 
92
110
  return "\n".join(result)
93
111
 
112
+ def add_text(self, to_add: str) -> TextContent:
113
+ text = TextContent(type="text", text=to_add)
114
+ self.content.append(text)
115
+ return text
116
+
94
117
  @classmethod
95
118
  def parse_get_prompt_result(cls, result: GetPromptResult) -> List["PromptMessageMultipart"]:
96
119
  """
@@ -120,4 +143,4 @@ class PromptMessageMultipart(BaseModel):
120
143
  """
121
144
  if not result or not result.messages:
122
145
  return []
123
- return cls.to_multipart(result.messages)
146
+ return cls.to_multipart(result.messages)
@@ -33,7 +33,7 @@ Extract key insights that would be compelling for a social media campaign.
33
33
  - Extracted compelling insights suitable for social media promotion
34
34
  """,
35
35
  request_params=RequestParams(maxTokens=8192),
36
- model="gpt-4o",
36
+ model="gpt-4.1",
37
37
  )
38
38
  @fast.evaluator_optimizer(
39
39
  "analysis_tool",
@@ -55,7 +55,7 @@ Extract key insights that would be compelling for a social media campaign.
55
55
  """,
56
56
  servers=["fetch", "brave"], # Using the fetch MCP server for Brave search
57
57
  request_params=RequestParams(temperature=0.3),
58
- model="gpt-4o",
58
+ model="gpt-4.1",
59
59
  )
60
60
  # Social media content generator
61
61
  @fast.agent(
@@ -61,7 +61,7 @@ if needed. Remind the Human of this.
61
61
  model="sonnet",
62
62
  plan_type="iterative",
63
63
  request_params=RequestParams(maxTokens=8192),
64
- max_iterations=5,
64
+ plan_iterations=5,
65
65
  )
66
66
  async def main() -> None:
67
67
  async with fast.run() as agent:
@@ -42,7 +42,7 @@ fast = FastAgent("PMO Job Description Generator")
42
42
  6. Emphasis on practical experience over formal requirements
43
43
 
44
44
  Provide specific feedback for improvements.""",
45
- model="gpt-4o",
45
+ model="gpt-4.1",
46
46
  )
47
47
  @fast.agent(
48
48
  name="file_handler",