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.
- {fast_agent_mcp-0.2.16.dist-info → fast_agent_mcp-0.2.17.dist-info}/METADATA +4 -6
- {fast_agent_mcp-0.2.16.dist-info → fast_agent_mcp-0.2.17.dist-info}/RECORD +43 -43
- mcp_agent/agents/base_agent.py +50 -6
- mcp_agent/agents/workflow/orchestrator_agent.py +6 -7
- mcp_agent/agents/workflow/router_agent.py +70 -136
- mcp_agent/app.py +1 -124
- mcp_agent/cli/commands/setup.py +1 -1
- mcp_agent/config.py +16 -13
- mcp_agent/context.py +4 -22
- mcp_agent/core/agent_types.py +2 -2
- mcp_agent/core/direct_decorators.py +2 -2
- mcp_agent/core/direct_factory.py +2 -1
- mcp_agent/core/fastagent.py +1 -1
- mcp_agent/core/request_params.py +5 -1
- mcp_agent/executor/workflow_signal.py +0 -2
- mcp_agent/llm/augmented_llm.py +183 -57
- mcp_agent/llm/augmented_llm_passthrough.py +1 -1
- mcp_agent/llm/augmented_llm_playback.py +21 -1
- mcp_agent/llm/memory.py +3 -3
- mcp_agent/llm/model_factory.py +3 -1
- mcp_agent/llm/provider_key_manager.py +1 -0
- mcp_agent/llm/provider_types.py +2 -1
- mcp_agent/llm/providers/augmented_llm_anthropic.py +49 -10
- mcp_agent/llm/providers/augmented_llm_deepseek.py +0 -2
- mcp_agent/llm/providers/augmented_llm_google.py +30 -0
- mcp_agent/llm/providers/augmented_llm_openai.py +95 -158
- mcp_agent/llm/providers/multipart_converter_openai.py +10 -27
- mcp_agent/llm/providers/sampling_converter_openai.py +5 -6
- mcp_agent/mcp/interfaces.py +6 -1
- mcp_agent/mcp/mcp_aggregator.py +2 -8
- mcp_agent/mcp/prompt_message_multipart.py +25 -2
- mcp_agent/resources/examples/data-analysis/analysis-campaign.py +2 -2
- mcp_agent/resources/examples/in_dev/agent_build.py +1 -1
- mcp_agent/resources/examples/internal/job.py +1 -1
- mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +1 -1
- mcp_agent/resources/examples/prompting/agent.py +0 -2
- mcp_agent/resources/examples/prompting/fastagent.config.yaml +2 -3
- mcp_agent/resources/examples/researcher/fastagent.config.yaml +1 -6
- mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -1
- mcp_agent/resources/examples/workflows/parallel.py +1 -1
- mcp_agent/executor/decorator_registry.py +0 -112
- {fast_agent_mcp-0.2.16.dist-info → fast_agent_mcp-0.2.17.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.16.dist-info → fast_agent_mcp-0.2.17.dist-info}/entry_points.txt +0 -0
- {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
|
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-
|
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
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
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
|
-
|
122
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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.
|
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
|
-
|
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
|
169
|
+
"OpenAI completion response:",
|
183
170
|
data=response,
|
184
171
|
)
|
185
172
|
|
186
173
|
if isinstance(response, AuthenticationError):
|
187
174
|
raise ProviderKeyError(
|
188
|
-
"
|
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
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
395
|
-
|
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
|
-
#
|
404
|
-
|
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
|
-
) ->
|
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
|
-
#
|
75
|
-
if
|
76
|
-
|
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
|
-
) ->
|
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(
|
15
|
+
class OpenAISamplingConverter(
|
16
|
+
ProviderFormatConverter[ChatCompletionMessageParam, ChatCompletionMessage]
|
17
|
+
):
|
19
18
|
@classmethod
|
20
|
-
def from_prompt_message(cls, message: PromptMessage) ->
|
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,
|
mcp_agent/mcp/interfaces.py
CHANGED
@@ -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
|
-
|
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
|
...
|
mcp_agent/mcp/mcp_aggregator.py
CHANGED
@@ -482,7 +482,7 @@ class MCPAggregator(ContextDependent):
|
|
482
482
|
|
483
483
|
async def get_prompt(
|
484
484
|
self,
|
485
|
-
prompt_name: str
|
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
|
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-
|
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-
|
58
|
+
model="gpt-4.1",
|
59
59
|
)
|
60
60
|
# Social media content generator
|
61
61
|
@fast.agent(
|