fast-agent-mcp 0.1.13__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 (147) hide show
  1. {fast_agent_mcp-0.1.13.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 +59 -371
  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 +27 -11
  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 +3 -1
  14. mcp_agent/cli/commands/bootstrap.py +18 -7
  15. mcp_agent/cli/commands/setup.py +12 -4
  16. mcp_agent/cli/main.py +1 -1
  17. mcp_agent/cli/terminal.py +1 -1
  18. mcp_agent/config.py +24 -35
  19. mcp_agent/context.py +3 -1
  20. mcp_agent/context_dependent.py +3 -1
  21. mcp_agent/core/agent_types.py +10 -7
  22. mcp_agent/core/direct_agent_app.py +179 -0
  23. mcp_agent/core/direct_decorators.py +443 -0
  24. mcp_agent/core/direct_factory.py +476 -0
  25. mcp_agent/core/enhanced_prompt.py +15 -20
  26. mcp_agent/core/fastagent.py +151 -337
  27. mcp_agent/core/interactive_prompt.py +424 -0
  28. mcp_agent/core/mcp_content.py +19 -11
  29. mcp_agent/core/prompt.py +6 -2
  30. mcp_agent/core/validation.py +89 -16
  31. mcp_agent/executor/decorator_registry.py +6 -2
  32. mcp_agent/executor/temporal.py +35 -11
  33. mcp_agent/executor/workflow_signal.py +8 -2
  34. mcp_agent/human_input/handler.py +3 -1
  35. mcp_agent/llm/__init__.py +2 -0
  36. mcp_agent/{workflows/llm → llm}/augmented_llm.py +131 -256
  37. mcp_agent/{workflows/llm → llm}/augmented_llm_passthrough.py +35 -107
  38. mcp_agent/llm/augmented_llm_playback.py +83 -0
  39. mcp_agent/{workflows/llm → llm}/model_factory.py +26 -8
  40. mcp_agent/llm/providers/__init__.py +8 -0
  41. mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +5 -1
  42. mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +37 -141
  43. mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
  44. mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +112 -148
  45. mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +78 -35
  46. mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +73 -44
  47. mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +18 -4
  48. mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +3 -3
  49. mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +3 -3
  50. mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +3 -3
  51. mcp_agent/{workflows/llm → llm}/sampling_converter.py +0 -21
  52. mcp_agent/{workflows/llm → llm}/sampling_format_converter.py +16 -1
  53. mcp_agent/logging/logger.py +2 -2
  54. mcp_agent/mcp/gen_client.py +9 -3
  55. mcp_agent/mcp/interfaces.py +67 -45
  56. mcp_agent/mcp/logger_textio.py +97 -0
  57. mcp_agent/mcp/mcp_agent_client_session.py +12 -4
  58. mcp_agent/mcp/mcp_agent_server.py +3 -1
  59. mcp_agent/mcp/mcp_aggregator.py +124 -93
  60. mcp_agent/mcp/mcp_connection_manager.py +21 -7
  61. mcp_agent/mcp/prompt_message_multipart.py +59 -1
  62. mcp_agent/mcp/prompt_render.py +77 -0
  63. mcp_agent/mcp/prompt_serialization.py +20 -13
  64. mcp_agent/mcp/prompts/prompt_constants.py +18 -0
  65. mcp_agent/mcp/prompts/prompt_helpers.py +327 -0
  66. mcp_agent/mcp/prompts/prompt_load.py +15 -5
  67. mcp_agent/mcp/prompts/prompt_server.py +154 -87
  68. mcp_agent/mcp/prompts/prompt_template.py +26 -35
  69. mcp_agent/mcp/resource_utils.py +3 -1
  70. mcp_agent/mcp/sampling.py +24 -15
  71. mcp_agent/mcp_server/agent_server.py +8 -5
  72. mcp_agent/mcp_server_registry.py +22 -9
  73. mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +1 -1
  74. mcp_agent/resources/examples/{data-analysis → in_dev}/slides.py +1 -1
  75. mcp_agent/resources/examples/internal/agent.py +4 -2
  76. mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
  77. mcp_agent/resources/examples/prompting/image_server.py +3 -1
  78. mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
  79. mcp_agent/ui/console_display.py +27 -7
  80. fast_agent_mcp-0.1.13.dist-info/RECORD +0 -164
  81. mcp_agent/core/agent_app.py +0 -570
  82. mcp_agent/core/agent_utils.py +0 -69
  83. mcp_agent/core/decorators.py +0 -448
  84. mcp_agent/core/factory.py +0 -422
  85. mcp_agent/core/proxies.py +0 -278
  86. mcp_agent/core/types.py +0 -22
  87. mcp_agent/eval/__init__.py +0 -0
  88. mcp_agent/mcp/stdio.py +0 -114
  89. mcp_agent/resources/examples/data-analysis/analysis-campaign.py +0 -188
  90. mcp_agent/resources/examples/data-analysis/analysis.py +0 -65
  91. mcp_agent/resources/examples/data-analysis/fastagent.config.yaml +0 -41
  92. mcp_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -1471
  93. mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +0 -53
  94. mcp_agent/resources/examples/researcher/fastagent.config.yaml +0 -66
  95. mcp_agent/resources/examples/researcher/researcher-eval.py +0 -53
  96. mcp_agent/resources/examples/researcher/researcher-imp.py +0 -189
  97. mcp_agent/resources/examples/researcher/researcher.py +0 -39
  98. mcp_agent/resources/examples/workflows/chaining.py +0 -45
  99. mcp_agent/resources/examples/workflows/evaluator.py +0 -79
  100. mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
  101. mcp_agent/resources/examples/workflows/human_input.py +0 -26
  102. mcp_agent/resources/examples/workflows/orchestrator.py +0 -74
  103. mcp_agent/resources/examples/workflows/parallel.py +0 -79
  104. mcp_agent/resources/examples/workflows/router.py +0 -54
  105. mcp_agent/resources/examples/workflows/sse.py +0 -23
  106. mcp_agent/telemetry/__init__.py +0 -0
  107. mcp_agent/telemetry/usage_tracking.py +0 -19
  108. mcp_agent/workflows/__init__.py +0 -0
  109. mcp_agent/workflows/embedding/__init__.py +0 -0
  110. mcp_agent/workflows/embedding/embedding_base.py +0 -58
  111. mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
  112. mcp_agent/workflows/embedding/embedding_openai.py +0 -37
  113. mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
  114. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -447
  115. mcp_agent/workflows/intent_classifier/__init__.py +0 -0
  116. mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -117
  117. mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -130
  118. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -41
  119. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -41
  120. mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -150
  121. mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
  122. mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -58
  123. mcp_agent/workflows/llm/__init__.py +0 -0
  124. mcp_agent/workflows/llm/augmented_llm_playback.py +0 -111
  125. mcp_agent/workflows/llm/providers/__init__.py +0 -8
  126. mcp_agent/workflows/orchestrator/__init__.py +0 -0
  127. mcp_agent/workflows/orchestrator/orchestrator.py +0 -535
  128. mcp_agent/workflows/parallel/__init__.py +0 -0
  129. mcp_agent/workflows/parallel/fan_in.py +0 -320
  130. mcp_agent/workflows/parallel/fan_out.py +0 -181
  131. mcp_agent/workflows/parallel/parallel_llm.py +0 -149
  132. mcp_agent/workflows/router/__init__.py +0 -0
  133. mcp_agent/workflows/router/router_base.py +0 -338
  134. mcp_agent/workflows/router/router_embedding.py +0 -226
  135. mcp_agent/workflows/router/router_embedding_cohere.py +0 -59
  136. mcp_agent/workflows/router/router_embedding_openai.py +0 -59
  137. mcp_agent/workflows/router/router_llm.py +0 -304
  138. mcp_agent/workflows/swarm/__init__.py +0 -0
  139. mcp_agent/workflows/swarm/swarm.py +0 -292
  140. mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
  141. mcp_agent/workflows/swarm/swarm_openai.py +0 -41
  142. {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
  143. {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
  144. {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  145. /mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_prompts.py +0 -0
  146. /mcp_agent/{workflows/llm → llm}/memory.py +0 -0
  147. /mcp_agent/{workflows/llm → llm}/prompt_utils.py +0 -0
@@ -0,0 +1,53 @@
1
+ import os
2
+
3
+ from mcp_agent.core.exceptions import ProviderKeyError
4
+ from mcp_agent.core.request_params import RequestParams
5
+ from mcp_agent.llm.providers.augmented_llm_openai import OpenAIAugmentedLLM
6
+
7
+ DEEPSEEK_BASE_URL = "https://api.deepseek.com"
8
+ DEFAULT_DEEPSEEK_MODEL = "deepseekchat" # current Deepseek only has two type models
9
+
10
+
11
+ class DeepSeekAugmentedLLM(OpenAIAugmentedLLM):
12
+ def __init__(self, *args, **kwargs) -> None:
13
+ kwargs["provider_name"] = "Deepseek" # Set provider name in kwargs
14
+ super().__init__(*args, **kwargs) # Properly pass args and kwargs to parent
15
+
16
+ def _initialize_default_params(self, kwargs: dict) -> RequestParams:
17
+ """Initialize Deepseek-specific default parameters"""
18
+ chosen_model = kwargs.get("model", DEFAULT_DEEPSEEK_MODEL)
19
+
20
+ return RequestParams(
21
+ model=chosen_model,
22
+ systemPrompt=self.instruction,
23
+ parallel_tool_calls=True,
24
+ max_iterations=10,
25
+ use_history=True,
26
+ )
27
+
28
+ def _api_key(self) -> str:
29
+ config = self.context.config
30
+ api_key = None
31
+
32
+ if config and config.deepseek:
33
+ api_key = config.deepseek.api_key
34
+ if api_key == "<your-api-key-here>":
35
+ api_key = None
36
+
37
+ if api_key is None:
38
+ api_key = os.getenv("DEEPSEEK_API_KEY")
39
+
40
+ if not api_key:
41
+ raise ProviderKeyError(
42
+ "DEEPSEEK API key not configured",
43
+ "The DEEKSEEK API key is required but not set.\n"
44
+ "Add it to your configuration file under deepseek.api_key\n"
45
+ "Or set the DEEPSEEK_API_KEY environment variable",
46
+ )
47
+ return api_key
48
+
49
+ def _base_url(self) -> str:
50
+ if self.context.config and self.context.config.deepseek:
51
+ base_url = self.context.config.deepseek.base_url
52
+
53
+ return base_url if base_url else DEEPSEEK_BASE_URL
@@ -1,15 +1,6 @@
1
1
  import os
2
- from typing import TYPE_CHECKING, List, Type
2
+ from typing import List, Type
3
3
 
4
- from pydantic_core import from_json
5
-
6
- from mcp_agent.workflows.llm.providers.multipart_converter_openai import OpenAIConverter
7
- from mcp_agent.workflows.llm.providers.sampling_converter_openai import (
8
- OpenAISamplingConverter,
9
- )
10
-
11
- if TYPE_CHECKING:
12
- from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
13
4
  from mcp.types import (
14
5
  CallToolRequest,
15
6
  CallToolRequestParams,
@@ -25,15 +16,22 @@ from openai.types.chat import (
25
16
  ChatCompletionToolParam,
26
17
  ChatCompletionUserMessageParam,
27
18
  )
19
+ from pydantic_core import from_json
28
20
  from rich.text import Text
29
21
 
30
22
  from mcp_agent.core.exceptions import ProviderKeyError
31
- from mcp_agent.logging.logger import get_logger
32
- from mcp_agent.workflows.llm.augmented_llm import (
23
+ from mcp_agent.core.prompt import Prompt
24
+ from mcp_agent.llm.augmented_llm import (
33
25
  AugmentedLLM,
34
26
  ModelT,
35
27
  RequestParams,
36
28
  )
29
+ from mcp_agent.llm.providers.multipart_converter_openai import OpenAIConverter
30
+ from mcp_agent.llm.providers.sampling_converter_openai import (
31
+ OpenAISamplingConverter,
32
+ )
33
+ from mcp_agent.logging.logger import get_logger
34
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
37
35
 
38
36
  _logger = get_logger(__name__)
39
37
 
@@ -48,38 +46,39 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
48
46
  This implementation uses OpenAI's ChatCompletion as the LLM.
49
47
  """
50
48
 
51
- def __init__(self, *args, **kwargs) -> None:
49
+ def __init__(self, provider_name: str = "OpenAI", *args, **kwargs) -> None:
52
50
  # Set type_converter before calling super().__init__
53
51
  if "type_converter" not in kwargs:
54
52
  kwargs["type_converter"] = OpenAISamplingConverter
55
53
 
56
54
  super().__init__(*args, **kwargs)
57
55
 
58
- self.provider = "OpenAI"
56
+ self.provider = provider_name
59
57
  # Initialize logger with name if available
60
58
  self.logger = get_logger(f"{__name__}.{self.name}" if self.name else __name__)
61
59
 
62
60
  # Set up reasoning-related attributes
63
61
  self._reasoning_effort = kwargs.get("reasoning_effort", None)
64
62
  if self.context and self.context.config and self.context.config.openai:
65
- if self._reasoning_effort is None and hasattr(self.context.config.openai, "reasoning_effort"):
63
+ if self._reasoning_effort is None and hasattr(
64
+ self.context.config.openai, "reasoning_effort"
65
+ ):
66
66
  self._reasoning_effort = self.context.config.openai.reasoning_effort
67
67
 
68
68
  # Determine if we're using a reasoning model
69
69
  chosen_model = self.default_request_params.model if self.default_request_params else None
70
- self._reasoning = chosen_model and (chosen_model.startswith("o3") or chosen_model.startswith("o1"))
70
+ self._reasoning = chosen_model and (
71
+ chosen_model.startswith("o3") or chosen_model.startswith("o1")
72
+ )
71
73
  if self._reasoning:
72
- self.logger.info(f"Using reasoning model '{chosen_model}' with '{self._reasoning_effort}' reasoning effort")
74
+ self.logger.info(
75
+ f"Using reasoning model '{chosen_model}' with '{self._reasoning_effort}' reasoning effort"
76
+ )
73
77
 
74
78
  def _initialize_default_params(self, kwargs: dict) -> RequestParams:
75
79
  """Initialize OpenAI-specific default parameters"""
76
80
  chosen_model = kwargs.get("model", DEFAULT_OPENAI_MODEL)
77
81
 
78
- # Get default model from config if available
79
- if self.context and self.context.config and self.context.config.openai:
80
- if hasattr(self.context.config.openai, "default_model"):
81
- chosen_model = self.context.config.openai.default_model
82
-
83
82
  return RequestParams(
84
83
  model=chosen_model,
85
84
  systemPrompt=self.instruction,
@@ -112,7 +111,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
112
111
  def _base_url(self) -> str:
113
112
  return self.context.config.openai.base_url if self.context.config.openai else None
114
113
 
115
- async def generate(
114
+ async def generate_internal(
116
115
  self,
117
116
  message,
118
117
  request_params: RequestParams | None = None,
@@ -131,7 +130,8 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
131
130
  except AuthenticationError as e:
132
131
  raise ProviderKeyError(
133
132
  "Invalid OpenAI API key",
134
- "The configured OpenAI API key was rejected.\n" "Please check that your API key is valid and not expired.",
133
+ "The configured OpenAI API key was rejected.\n"
134
+ "Please check that your API key is valid and not expired.",
135
135
  ) from e
136
136
 
137
137
  system_prompt = self.instruction or params.systemPrompt
@@ -150,7 +150,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
150
150
  messages.append(message)
151
151
 
152
152
  response = await self.aggregator.list_tools()
153
- available_tools: List[ChatCompletionToolParam] = [
153
+ available_tools: List[ChatCompletionToolParam] | None = [
154
154
  ChatCompletionToolParam(
155
155
  type="function",
156
156
  function={
@@ -163,15 +163,10 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
163
163
  for tool in response.tools
164
164
  ]
165
165
  if not available_tools:
166
- available_tools = []
166
+ available_tools = None # deepseek does not allow empty array
167
167
 
168
168
  responses: List[ChatCompletionMessage] = []
169
- model = await self.select_model(params)
170
- chat_turn = len(messages) // 2
171
- if self._reasoning:
172
- self.show_user_message(str(message), f"{model} ({self._reasoning_effort})", chat_turn)
173
- else:
174
- self.show_user_message(str(message), model, chat_turn)
169
+ model = self.default_request_params.model
175
170
 
176
171
  # we do NOT send stop sequences as this causes errors with mutlimodal processing
177
172
  for i in range(params.max_iterations):
@@ -195,10 +190,12 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
195
190
  arguments = {**arguments, **params.metadata}
196
191
 
197
192
  self.logger.debug(f"{arguments}")
198
- self._log_chat_progress(chat_turn, model=model)
193
+ self._log_chat_progress(self.chat_turn(), model=model)
199
194
 
200
195
  if response_model is None:
201
- executor_result = await self.executor.execute(openai_client.chat.completions.create, **arguments)
196
+ executor_result = await self.executor.execute(
197
+ openai_client.chat.completions.create, **arguments
198
+ )
202
199
  else:
203
200
  executor_result = await self.executor.execute(
204
201
  openai_client.beta.chat.completions.parse,
@@ -216,7 +213,8 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
216
213
  if isinstance(response, AuthenticationError):
217
214
  raise ProviderKeyError(
218
215
  "Invalid OpenAI API key",
219
- "The configured OpenAI API key was rejected.\n" "Please check that your API key is valid and not expired.",
216
+ "The configured OpenAI API key was rejected.\n"
217
+ "Please check that your API key is valid and not expired.",
220
218
  ) from response
221
219
  elif isinstance(response, BaseException):
222
220
  self.logger.error(f"Error: {response}")
@@ -226,8 +224,6 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
226
224
  # No response from the model, we're done
227
225
  break
228
226
 
229
- # TODO: saqadri - handle multiple choices for more complex interactions.
230
- # Keeping it simple for now because multiple choices will also complicate memory management
231
227
  choice = response.choices[0]
232
228
  message = choice.message
233
229
  responses.append(message)
@@ -239,7 +235,9 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
239
235
  if message_text:
240
236
  await self.show_assistant_message(
241
237
  message_text,
242
- message.tool_calls[0].function.name, # TODO support displaying multiple tool calls
238
+ message.tool_calls[
239
+ 0
240
+ ].function.name, # TODO support displaying multiple tool calls
243
241
  )
244
242
  else:
245
243
  await self.show_assistant_message(
@@ -271,7 +269,9 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
271
269
 
272
270
  messages.extend(OpenAIConverter.convert_function_results_to_openai(tool_results))
273
271
 
274
- self.logger.debug(f"Iteration {i}: Tool call results: {str(tool_results) if tool_results else 'None'}")
272
+ self.logger.debug(
273
+ f"Iteration {i}: Tool call results: {str(tool_results) if tool_results else 'None'}"
274
+ )
275
275
  elif choice.finish_reason == "length":
276
276
  # We have reached the max tokens limit
277
277
  self.logger.debug(f"Iteration {i}: Stopping because finish_reason is 'length'")
@@ -291,7 +291,9 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
291
291
  break
292
292
  elif choice.finish_reason == "content_filter":
293
293
  # The response was filtered by the content filter
294
- self.logger.debug(f"Iteration {i}: Stopping because finish_reason is 'content_filter'")
294
+ self.logger.debug(
295
+ f"Iteration {i}: Stopping because finish_reason is 'content_filter'"
296
+ )
295
297
  # TODO: saqadri - would be useful to return the reason for stopping to the caller
296
298
  break
297
299
  elif choice.finish_reason == "stop":
@@ -320,7 +322,7 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
320
322
  self,
321
323
  message,
322
324
  request_params: RequestParams | None = None,
323
- ):
325
+ ) -> str:
324
326
  """
325
327
  Process a query using an LLM and available tools.
326
328
  The default implementation uses OpenAI's ChatCompletion as the LLM.
@@ -330,11 +332,8 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
330
332
  - "***SAVE_HISTORY <filename.md>" - Saves the conversation history to the specified file
331
333
  in MCP prompt format with user/assistant delimiters.
332
334
  """
333
- # Check if this is a special command to save history
334
- if isinstance(message, str) and message.startswith("***SAVE_HISTORY "):
335
- return await self._save_history_to_file(message)
336
335
 
337
- responses = await self.generate(
336
+ responses = await self.generate_internal(
338
337
  message=message,
339
338
  request_params=request_params,
340
339
  )
@@ -352,30 +351,20 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
352
351
 
353
352
  return "\n".join(final_text)
354
353
 
355
- async def _apply_prompt_template_provider_specific(
354
+ async def _apply_prompt_provider_specific(
356
355
  self,
357
356
  multipart_messages: List["PromptMessageMultipart"],
358
357
  request_params: RequestParams | None = None,
359
- ) -> str:
360
- """
361
- OpenAI-specific implementation of apply_prompt_template that handles
362
- multimodal content natively.
363
-
364
- Args:
365
- multipart_messages: List of PromptMessageMultipart objects parsed from the prompt template
366
-
367
- Returns:
368
- String representation of the assistant's response if generated,
369
- or the last assistant message in the prompt
370
- """
371
-
358
+ ) -> PromptMessageMultipart:
372
359
  # TODO -- this is very similar to Anthropic (just the converter class changes).
373
360
  # TODO -- potential refactor to base class, standardize Converter interface
374
361
  # Check the last message role
375
362
  last_message = multipart_messages[-1]
376
363
 
377
364
  # Add all previous messages to history (or all messages if last is from assistant)
378
- messages_to_add = multipart_messages[:-1] if last_message.role == "user" else multipart_messages
365
+ messages_to_add = (
366
+ multipart_messages[:-1] if last_message.role == "user" else multipart_messages
367
+ )
379
368
  converted = []
380
369
  for msg in messages_to_add:
381
370
  converted.append(OpenAIConverter.convert_to_openai(msg))
@@ -385,115 +374,90 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
385
374
  # For user messages: Generate response to the last one
386
375
  self.logger.debug("Last message in prompt is from user, generating assistant response")
387
376
  message_param = OpenAIConverter.convert_to_openai(last_message)
388
- return await self.generate_str(message_param, request_params)
377
+ return Prompt.assistant(await self.generate_str(message_param, request_params))
389
378
  else:
390
379
  # For assistant messages: Return the last message content as text
391
380
  self.logger.debug("Last message in prompt is from assistant, returning it directly")
392
- return str(last_message)
381
+ return last_message
393
382
 
394
- async def _save_history_to_file(self, command: str) -> str:
383
+ async def structured(
384
+ self,
385
+ prompt: List[PromptMessageMultipart],
386
+ model: Type[ModelT],
387
+ request_params: RequestParams | None = None,
388
+ ) -> ModelT | None:
395
389
  """
396
- Save the conversation history to a file in MCP prompt format.
390
+ Apply the prompt and return the result as a Pydantic model.
391
+
392
+ Uses OpenAI's beta parse feature when compatible, falling back to standard
393
+ JSON parsing if the beta feature fails or is unavailable.
397
394
 
398
395
  Args:
399
- command: The command string, expected format: "***SAVE_HISTORY <filename.md>"
396
+ prompt: List of messages to process
397
+ model: Pydantic model to parse the response into
398
+ request_params: Optional request parameters
400
399
 
401
400
  Returns:
402
- Success or error message
401
+ The parsed response as a Pydantic model, or None if parsing fails
403
402
  """
404
- try:
405
- # Extract the filename from the command
406
- parts = command.split(" ", 1)
407
- if len(parts) != 2 or not parts[1].strip():
408
- return "Error: Invalid format. Expected '***SAVE_HISTORY <filename.md>'"
409
403
 
410
- filename = parts[1].strip()
404
+ if not "OpenAI" == self.provider:
405
+ return await super().structured(prompt, model, request_params)
411
406
 
412
- # Get all messages from history
413
- messages = self.history.get(include_history=True)
407
+ logger = get_logger(__name__)
414
408
 
415
- # Import required utilities
416
- from mcp_agent.mcp.prompt_serialization import (
417
- multipart_messages_to_delimited_format,
418
- )
419
- from mcp_agent.workflows.llm.openai_utils import (
420
- openai_message_param_to_prompt_message_multipart,
421
- )
409
+ # First try to use OpenAI's beta.chat.completions.parse feature
410
+ try:
411
+ # Convert the multipart messages to OpenAI format
412
+ messages = []
413
+ for msg in prompt:
414
+ messages.append(OpenAIConverter.convert_to_openai(msg))
415
+
416
+ # Add system prompt if available and not already present
417
+ if self.instruction and not any(m.get("role") == "system" for m in messages):
418
+ system_msg = ChatCompletionSystemMessageParam(
419
+ role="system", content=self.instruction
420
+ )
421
+ messages.insert(0, system_msg)
422
422
 
423
- # Convert message params to PromptMessageMultipart objects
424
- multipart_messages = []
425
- for msg in messages:
426
- # Skip system messages - PromptMessageMultipart only supports user and assistant roles
427
- if isinstance(msg, dict) and msg.get("role") == "system":
428
- continue
429
-
430
- # Convert the message to a multipart message
431
- multipart_messages.append(openai_message_param_to_prompt_message_multipart(msg))
432
-
433
- # Convert to delimited format
434
- delimited_content = multipart_messages_to_delimited_format(
435
- multipart_messages,
436
- user_delimiter="---USER",
437
- assistant_delimiter="---ASSISTANT",
438
- )
423
+ # Use the beta parse feature
424
+ try:
425
+ openai_client = OpenAI(api_key=self._api_key(), base_url=self._base_url())
426
+ model_name = self.default_request_params.model
439
427
 
440
- # Write to file
441
- with open(filename, "w", encoding="utf-8") as f:
442
- f.write("\n\n".join(delimited_content))
428
+ logger.debug(
429
+ f"Using OpenAI beta parse with model {model_name} for structured output"
430
+ )
431
+ response = await self.executor.execute(
432
+ openai_client.beta.chat.completions.parse,
433
+ model=model_name,
434
+ messages=messages,
435
+ response_format=model,
436
+ )
443
437
 
444
- self.logger.info(f"Saved conversation history to {filename}")
445
- return f"Done. Saved conversation history to {filename}"
438
+ if response and isinstance(response[0], BaseException):
439
+ raise response[0]
446
440
 
447
- except Exception as e:
448
- self.logger.error(f"Error saving history: {str(e)}")
449
- return f"Error saving history: {str(e)}"
441
+ parsed_result = response[0].choices[0].message
442
+ logger.debug("Successfully used OpenAI beta parse feature for structured output")
443
+ return parsed_result.parsed
450
444
 
451
- async def generate_structured(
452
- self,
453
- message,
454
- response_model: Type[ModelT],
455
- request_params: RequestParams | None = None,
456
- ) -> ModelT:
457
- responses = await self.generate(
458
- message=message,
459
- request_params=request_params,
460
- response_model=response_model,
461
- )
462
- return responses[0].parsed
445
+ except (ImportError, AttributeError, NotImplementedError) as e:
446
+ # Beta feature not available, log and continue to fallback
447
+ logger.debug(f"OpenAI beta parse feature not available: {str(e)}")
448
+ # Continue to fallback
449
+
450
+ except Exception as e:
451
+ logger.debug(f"OpenAI beta parse failed: {str(e)}, falling back to standard method")
452
+ # Continue to standard method as fallback
463
453
 
464
- async def generate_prompt(self, prompt: "PromptMessageMultipart", request_params: RequestParams | None) -> str:
465
- converted_prompt = OpenAIConverter.convert_to_openai(prompt)
466
- return await self.generate_str(converted_prompt, request_params)
454
+ # Fallback to standard method (inheriting from base class)
455
+ return await super().structured(prompt, model, request_params)
467
456
 
468
457
  async def pre_tool_call(self, tool_call_id: str | None, request: CallToolRequest):
469
458
  return request
470
459
 
471
- async def post_tool_call(self, tool_call_id: str | None, request: CallToolRequest, result: CallToolResult):
460
+ async def post_tool_call(
461
+ self, tool_call_id: str | None, request: CallToolRequest, result: CallToolResult
462
+ ):
472
463
  return result
473
-
474
- def message_param_str(self, message: ChatCompletionMessageParam) -> str:
475
- """Convert an input message to a string representation."""
476
- if message.get("content"):
477
- content = message["content"]
478
- if isinstance(content, str):
479
- return content
480
- else: # content is a list
481
- final_text: List[str] = []
482
- for part in content:
483
- text_part = part.get("text")
484
- if text_part:
485
- final_text.append(str(text_part))
486
- else:
487
- final_text.append(str(part))
488
-
489
- return "\n".join(final_text)
490
-
491
- return str(message)
492
-
493
- def message_str(self, message: ChatCompletionMessage) -> str:
494
- """Convert an output message to a string representation."""
495
- content = message.content
496
- if content:
497
- return content
498
-
499
- return str(message)