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,39 +1,37 @@
1
1
  import os
2
- from typing import List, Type, TYPE_CHECKING
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,
4
+ from mcp.types import (
5
+ CallToolRequest,
6
+ CallToolRequestParams,
7
+ CallToolResult,
9
8
  )
10
-
11
- if TYPE_CHECKING:
12
- from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
13
- from openai import OpenAI, AuthenticationError
9
+ from openai import AuthenticationError, OpenAI
14
10
 
15
11
  # from openai.types.beta.chat import
16
12
  from openai.types.chat import (
17
- ChatCompletionMessageParam,
18
13
  ChatCompletionMessage,
14
+ ChatCompletionMessageParam,
19
15
  ChatCompletionSystemMessageParam,
20
16
  ChatCompletionToolParam,
21
17
  ChatCompletionUserMessageParam,
22
18
  )
23
- from mcp.types import (
24
- CallToolRequestParams,
25
- CallToolRequest,
26
- CallToolResult,
27
- )
19
+ from pydantic_core import from_json
20
+ from rich.text import Text
28
21
 
29
- from mcp_agent.workflows.llm.augmented_llm import (
22
+ from mcp_agent.core.exceptions import ProviderKeyError
23
+ from mcp_agent.core.prompt import Prompt
24
+ from mcp_agent.llm.augmented_llm import (
30
25
  AugmentedLLM,
31
26
  ModelT,
32
27
  RequestParams,
33
28
  )
34
- from mcp_agent.core.exceptions import ProviderKeyError
29
+ from mcp_agent.llm.providers.multipart_converter_openai import OpenAIConverter
30
+ from mcp_agent.llm.providers.sampling_converter_openai import (
31
+ OpenAISamplingConverter,
32
+ )
35
33
  from mcp_agent.logging.logger import get_logger
36
- from rich.text import Text
34
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
37
35
 
38
36
  _logger = get_logger(__name__)
39
37
 
@@ -41,23 +39,21 @@ DEFAULT_OPENAI_MODEL = "gpt-4o"
41
39
  DEFAULT_REASONING_EFFORT = "medium"
42
40
 
43
41
 
44
- class OpenAIAugmentedLLM(
45
- AugmentedLLM[ChatCompletionMessageParam, ChatCompletionMessage]
46
- ):
42
+ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletionMessage]):
47
43
  """
48
44
  The basic building block of agentic systems is an LLM enhanced with augmentations
49
45
  such as retrieval, tools, and memory provided from a collection of MCP servers.
50
46
  This implementation uses OpenAI's ChatCompletion as the LLM.
51
47
  """
52
48
 
53
- def __init__(self, *args, **kwargs):
49
+ def __init__(self, provider_name: str = "OpenAI", *args, **kwargs) -> None:
54
50
  # Set type_converter before calling super().__init__
55
51
  if "type_converter" not in kwargs:
56
52
  kwargs["type_converter"] = OpenAISamplingConverter
57
53
 
58
54
  super().__init__(*args, **kwargs)
59
55
 
60
- self.provider = "OpenAI"
56
+ self.provider = provider_name
61
57
  # Initialize logger with name if available
62
58
  self.logger = get_logger(f"{__name__}.{self.name}" if self.name else __name__)
63
59
 
@@ -70,9 +66,7 @@ class OpenAIAugmentedLLM(
70
66
  self._reasoning_effort = self.context.config.openai.reasoning_effort
71
67
 
72
68
  # Determine if we're using a reasoning model
73
- chosen_model = (
74
- self.default_request_params.model if self.default_request_params else None
75
- )
69
+ chosen_model = self.default_request_params.model if self.default_request_params else None
76
70
  self._reasoning = chosen_model and (
77
71
  chosen_model.startswith("o3") or chosen_model.startswith("o1")
78
72
  )
@@ -85,11 +79,6 @@ class OpenAIAugmentedLLM(
85
79
  """Initialize OpenAI-specific default parameters"""
86
80
  chosen_model = kwargs.get("model", DEFAULT_OPENAI_MODEL)
87
81
 
88
- # Get default model from config if available
89
- if self.context and self.context.config and self.context.config.openai:
90
- if hasattr(self.context.config.openai, "default_model"):
91
- chosen_model = self.context.config.openai.default_model
92
-
93
82
  return RequestParams(
94
83
  model=chosen_model,
95
84
  systemPrompt=self.instruction,
@@ -120,11 +109,9 @@ class OpenAIAugmentedLLM(
120
109
  return api_key
121
110
 
122
111
  def _base_url(self) -> str:
123
- return (
124
- self.context.config.openai.base_url if self.context.config.openai else None
125
- )
112
+ return self.context.config.openai.base_url if self.context.config.openai else None
126
113
 
127
- async def generate(
114
+ async def generate_internal(
128
115
  self,
129
116
  message,
130
117
  request_params: RequestParams | None = None,
@@ -149,25 +136,21 @@ class OpenAIAugmentedLLM(
149
136
 
150
137
  system_prompt = self.instruction or params.systemPrompt
151
138
  if system_prompt:
152
- messages.append(
153
- ChatCompletionSystemMessageParam(role="system", content=system_prompt)
154
- )
139
+ messages.append(ChatCompletionSystemMessageParam(role="system", content=system_prompt))
155
140
 
156
141
  # Always include prompt messages, but only include conversation history
157
142
  # if use_history is True
158
143
  messages.extend(self.history.get(include_history=params.use_history))
159
144
 
160
145
  if isinstance(message, str):
161
- messages.append(
162
- ChatCompletionUserMessageParam(role="user", content=message)
163
- )
146
+ messages.append(ChatCompletionUserMessageParam(role="user", content=message))
164
147
  elif isinstance(message, list):
165
148
  messages.extend(message)
166
149
  else:
167
150
  messages.append(message)
168
151
 
169
152
  response = await self.aggregator.list_tools()
170
- available_tools: List[ChatCompletionToolParam] = [
153
+ available_tools: List[ChatCompletionToolParam] | None = [
171
154
  ChatCompletionToolParam(
172
155
  type="function",
173
156
  function={
@@ -180,17 +163,10 @@ class OpenAIAugmentedLLM(
180
163
  for tool in response.tools
181
164
  ]
182
165
  if not available_tools:
183
- available_tools = []
166
+ available_tools = None # deepseek does not allow empty array
184
167
 
185
168
  responses: List[ChatCompletionMessage] = []
186
- model = await self.select_model(params)
187
- chat_turn = len(messages) // 2
188
- if self._reasoning:
189
- self.show_user_message(
190
- str(message), f"{model} ({self._reasoning_effort})", chat_turn
191
- )
192
- else:
193
- self.show_user_message(str(message), model, chat_turn)
169
+ model = self.default_request_params.model
194
170
 
195
171
  # we do NOT send stop sequences as this causes errors with mutlimodal processing
196
172
  for i in range(params.max_iterations):
@@ -214,7 +190,7 @@ class OpenAIAugmentedLLM(
214
190
  arguments = {**arguments, **params.metadata}
215
191
 
216
192
  self.logger.debug(f"{arguments}")
217
- self._log_chat_progress(chat_turn, model=model)
193
+ self._log_chat_progress(self.chat_turn(), model=model)
218
194
 
219
195
  if response_model is None:
220
196
  executor_result = await self.executor.execute(
@@ -248,21 +224,14 @@ class OpenAIAugmentedLLM(
248
224
  # No response from the model, we're done
249
225
  break
250
226
 
251
- # TODO: saqadri - handle multiple choices for more complex interactions.
252
- # Keeping it simple for now because multiple choices will also complicate memory management
253
227
  choice = response.choices[0]
254
228
  message = choice.message
255
229
  responses.append(message)
256
230
 
257
- converted_message = self.convert_message_to_message_param(
258
- message, name=self.name
259
- )
231
+ converted_message = self.convert_message_to_message_param(message, name=self.name)
260
232
  messages.append(converted_message)
261
233
  message_text = converted_message.content
262
- if (
263
- choice.finish_reason in ["tool_calls", "function_call"]
264
- and message.tool_calls
265
- ):
234
+ if choice.finish_reason in ["tool_calls", "function_call"] and message.tool_calls:
266
235
  if message_text:
267
236
  await self.show_assistant_message(
268
237
  message_text,
@@ -290,9 +259,7 @@ class OpenAIAugmentedLLM(
290
259
  method="tools/call",
291
260
  params=CallToolRequestParams(
292
261
  name=tool_call.function.name,
293
- arguments=from_json(
294
- tool_call.function.arguments, allow_partial=True
295
- ),
262
+ arguments=from_json(tool_call.function.arguments, allow_partial=True),
296
263
  ),
297
264
  )
298
265
  result = await self.call_tool(tool_call_request, tool_call.id)
@@ -300,18 +267,14 @@ class OpenAIAugmentedLLM(
300
267
 
301
268
  tool_results.append((tool_call.id, result))
302
269
 
303
- messages.extend(
304
- OpenAIConverter.convert_function_results_to_openai(tool_results)
305
- )
270
+ messages.extend(OpenAIConverter.convert_function_results_to_openai(tool_results))
306
271
 
307
272
  self.logger.debug(
308
273
  f"Iteration {i}: Tool call results: {str(tool_results) if tool_results else 'None'}"
309
274
  )
310
275
  elif choice.finish_reason == "length":
311
276
  # We have reached the max tokens limit
312
- self.logger.debug(
313
- f"Iteration {i}: Stopping because finish_reason is 'length'"
314
- )
277
+ self.logger.debug(f"Iteration {i}: Stopping because finish_reason is 'length'")
315
278
  if request_params and request_params.maxTokens is not None:
316
279
  message_text = Text(
317
280
  f"the assistant has reached the maximum token limit ({request_params.maxTokens})",
@@ -334,9 +297,7 @@ class OpenAIAugmentedLLM(
334
297
  # TODO: saqadri - would be useful to return the reason for stopping to the caller
335
298
  break
336
299
  elif choice.finish_reason == "stop":
337
- self.logger.debug(
338
- f"Iteration {i}: Stopping because finish_reason is 'stop'"
339
- )
300
+ self.logger.debug(f"Iteration {i}: Stopping because finish_reason is 'stop'")
340
301
  if message_text:
341
302
  await self.show_assistant_message(message_text, "")
342
303
  break
@@ -361,7 +322,7 @@ class OpenAIAugmentedLLM(
361
322
  self,
362
323
  message,
363
324
  request_params: RequestParams | None = None,
364
- ):
325
+ ) -> str:
365
326
  """
366
327
  Process a query using an LLM and available tools.
367
328
  The default implementation uses OpenAI's ChatCompletion as the LLM.
@@ -371,11 +332,8 @@ class OpenAIAugmentedLLM(
371
332
  - "***SAVE_HISTORY <filename.md>" - Saves the conversation history to the specified file
372
333
  in MCP prompt format with user/assistant delimiters.
373
334
  """
374
- # Check if this is a special command to save history
375
- if isinstance(message, str) and message.startswith("***SAVE_HISTORY "):
376
- return await self._save_history_to_file(message)
377
335
 
378
- responses = await self.generate(
336
+ responses = await self.generate_internal(
379
337
  message=message,
380
338
  request_params=request_params,
381
339
  )
@@ -393,23 +351,11 @@ class OpenAIAugmentedLLM(
393
351
 
394
352
  return "\n".join(final_text)
395
353
 
396
- async def _apply_prompt_template_provider_specific(
354
+ async def _apply_prompt_provider_specific(
397
355
  self,
398
356
  multipart_messages: List["PromptMessageMultipart"],
399
357
  request_params: RequestParams | None = None,
400
- ) -> str:
401
- """
402
- OpenAI-specific implementation of apply_prompt_template that handles
403
- multimodal content natively.
404
-
405
- Args:
406
- multipart_messages: List of PromptMessageMultipart objects parsed from the prompt template
407
-
408
- Returns:
409
- String representation of the assistant's response if generated,
410
- or the last assistant message in the prompt
411
- """
412
-
358
+ ) -> PromptMessageMultipart:
413
359
  # TODO -- this is very similar to Anthropic (just the converter class changes).
414
360
  # TODO -- potential refactor to base class, standardize Converter interface
415
361
  # Check the last message role
@@ -417,9 +363,7 @@ class OpenAIAugmentedLLM(
417
363
 
418
364
  # Add all previous messages to history (or all messages if last is from assistant)
419
365
  messages_to_add = (
420
- multipart_messages[:-1]
421
- if last_message.role == "user"
422
- else multipart_messages
366
+ multipart_messages[:-1] if last_message.role == "user" else multipart_messages
423
367
  )
424
368
  converted = []
425
369
  for msg in messages_to_add:
@@ -428,95 +372,87 @@ class OpenAIAugmentedLLM(
428
372
 
429
373
  if last_message.role == "user":
430
374
  # For user messages: Generate response to the last one
431
- self.logger.debug(
432
- "Last message in prompt is from user, generating assistant response"
433
- )
375
+ self.logger.debug("Last message in prompt is from user, generating assistant response")
434
376
  message_param = OpenAIConverter.convert_to_openai(last_message)
435
- return await self.generate_str(message_param, request_params)
377
+ return Prompt.assistant(await self.generate_str(message_param, request_params))
436
378
  else:
437
379
  # For assistant messages: Return the last message content as text
438
- self.logger.debug(
439
- "Last message in prompt is from assistant, returning it directly"
440
- )
441
- return str(last_message)
380
+ self.logger.debug("Last message in prompt is from assistant, returning it directly")
381
+ return last_message
442
382
 
443
- 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:
444
389
  """
445
- 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.
446
394
 
447
395
  Args:
448
- 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
449
399
 
450
400
  Returns:
451
- Success or error message
401
+ The parsed response as a Pydantic model, or None if parsing fails
452
402
  """
453
- try:
454
- # Extract the filename from the command
455
- parts = command.split(" ", 1)
456
- if len(parts) != 2 or not parts[1].strip():
457
- return "Error: Invalid format. Expected '***SAVE_HISTORY <filename.md>'"
458
403
 
459
- filename = parts[1].strip()
404
+ if not "OpenAI" == self.provider:
405
+ return await super().structured(prompt, model, request_params)
460
406
 
461
- # Get all messages from history
462
- messages = self.history.get(include_history=True)
407
+ logger = get_logger(__name__)
463
408
 
464
- # Import required utilities
465
- from mcp_agent.workflows.llm.openai_utils import (
466
- openai_message_param_to_prompt_message_multipart,
467
- )
468
- from mcp_agent.mcp.prompt_serialization import (
469
- multipart_messages_to_delimited_format,
470
- )
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)
471
422
 
472
- # Convert message params to PromptMessageMultipart objects
473
- multipart_messages = []
474
- for msg in messages:
475
- # Skip system messages - PromptMessageMultipart only supports user and assistant roles
476
- if isinstance(msg, dict) and msg.get("role") == "system":
477
- continue
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
478
427
 
479
- # Convert the message to a multipart message
480
- multipart_messages.append(
481
- openai_message_param_to_prompt_message_multipart(msg)
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,
482
436
  )
483
437
 
484
- # Convert to delimited format
485
- delimited_content = multipart_messages_to_delimited_format(
486
- multipart_messages,
487
- user_delimiter="---USER",
488
- assistant_delimiter="---ASSISTANT",
489
- )
438
+ if response and isinstance(response[0], BaseException):
439
+ raise response[0]
490
440
 
491
- # Write to file
492
- with open(filename, "w", encoding="utf-8") as f:
493
- f.write("\n\n".join(delimited_content))
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
494
444
 
495
- self.logger.info(f"Saved conversation history to {filename}")
496
- return f"Done. Saved conversation history to {filename}"
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
497
449
 
498
450
  except Exception as e:
499
- self.logger.error(f"Error saving history: {str(e)}")
500
- return f"Error saving history: {str(e)}"
501
-
502
- async def generate_structured(
503
- self,
504
- message,
505
- response_model: Type[ModelT],
506
- request_params: RequestParams | None = None,
507
- ) -> ModelT:
508
- responses = await self.generate(
509
- message=message,
510
- request_params=request_params,
511
- response_model=response_model,
512
- )
513
- return responses[0].parsed
451
+ logger.debug(f"OpenAI beta parse failed: {str(e)}, falling back to standard method")
452
+ # Continue to standard method as fallback
514
453
 
515
- async def generate_prompt(
516
- self, prompt: "PromptMessageMultipart", request_params: RequestParams | None
517
- ) -> str:
518
- converted_prompt = OpenAIConverter.convert_to_openai(prompt)
519
- 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)
520
456
 
521
457
  async def pre_tool_call(self, tool_call_id: str | None, request: CallToolRequest):
522
458
  return request
@@ -525,30 +461,3 @@ class OpenAIAugmentedLLM(
525
461
  self, tool_call_id: str | None, request: CallToolRequest, result: CallToolResult
526
462
  ):
527
463
  return result
528
-
529
- def message_param_str(self, message: ChatCompletionMessageParam) -> str:
530
- """Convert an input message to a string representation."""
531
- if message.get("content"):
532
- content = message["content"]
533
- if isinstance(content, str):
534
- return content
535
- else: # content is a list
536
- final_text: List[str] = []
537
- for part in content:
538
- text_part = part.get("text")
539
- if text_part:
540
- final_text.append(str(text_part))
541
- else:
542
- final_text.append(str(part))
543
-
544
- return "\n".join(final_text)
545
-
546
- return str(message)
547
-
548
- def message_str(self, message: ChatCompletionMessage) -> str:
549
- """Convert an output message to a string representation."""
550
- content = message.content
551
- if content:
552
- return content
553
-
554
- return str(message)