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.
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/METADATA +3 -4
- fast_agent_mcp-0.2.0.dist-info/RECORD +123 -0
- mcp_agent/__init__.py +75 -0
- mcp_agent/agents/agent.py +59 -371
- mcp_agent/agents/base_agent.py +522 -0
- mcp_agent/agents/workflow/__init__.py +1 -0
- mcp_agent/agents/workflow/chain_agent.py +173 -0
- mcp_agent/agents/workflow/evaluator_optimizer.py +362 -0
- mcp_agent/agents/workflow/orchestrator_agent.py +591 -0
- mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_models.py +27 -11
- mcp_agent/agents/workflow/parallel_agent.py +182 -0
- mcp_agent/agents/workflow/router_agent.py +307 -0
- mcp_agent/app.py +3 -1
- mcp_agent/cli/commands/bootstrap.py +18 -7
- mcp_agent/cli/commands/setup.py +12 -4
- mcp_agent/cli/main.py +1 -1
- mcp_agent/cli/terminal.py +1 -1
- mcp_agent/config.py +24 -35
- mcp_agent/context.py +3 -1
- mcp_agent/context_dependent.py +3 -1
- mcp_agent/core/agent_types.py +10 -7
- mcp_agent/core/direct_agent_app.py +179 -0
- mcp_agent/core/direct_decorators.py +443 -0
- mcp_agent/core/direct_factory.py +476 -0
- mcp_agent/core/enhanced_prompt.py +15 -20
- mcp_agent/core/fastagent.py +151 -337
- mcp_agent/core/interactive_prompt.py +424 -0
- mcp_agent/core/mcp_content.py +19 -11
- mcp_agent/core/prompt.py +6 -2
- mcp_agent/core/validation.py +89 -16
- mcp_agent/executor/decorator_registry.py +6 -2
- mcp_agent/executor/temporal.py +35 -11
- mcp_agent/executor/workflow_signal.py +8 -2
- mcp_agent/human_input/handler.py +3 -1
- mcp_agent/llm/__init__.py +2 -0
- mcp_agent/{workflows/llm → llm}/augmented_llm.py +131 -256
- mcp_agent/{workflows/llm → llm}/augmented_llm_passthrough.py +35 -107
- mcp_agent/llm/augmented_llm_playback.py +83 -0
- mcp_agent/{workflows/llm → llm}/model_factory.py +26 -8
- mcp_agent/llm/providers/__init__.py +8 -0
- mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +5 -1
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +37 -141
- mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +112 -148
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +78 -35
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +73 -44
- mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +18 -4
- mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +3 -3
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +3 -3
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +3 -3
- mcp_agent/{workflows/llm → llm}/sampling_converter.py +0 -21
- mcp_agent/{workflows/llm → llm}/sampling_format_converter.py +16 -1
- mcp_agent/logging/logger.py +2 -2
- mcp_agent/mcp/gen_client.py +9 -3
- mcp_agent/mcp/interfaces.py +67 -45
- mcp_agent/mcp/logger_textio.py +97 -0
- mcp_agent/mcp/mcp_agent_client_session.py +12 -4
- mcp_agent/mcp/mcp_agent_server.py +3 -1
- mcp_agent/mcp/mcp_aggregator.py +124 -93
- mcp_agent/mcp/mcp_connection_manager.py +21 -7
- mcp_agent/mcp/prompt_message_multipart.py +59 -1
- mcp_agent/mcp/prompt_render.py +77 -0
- mcp_agent/mcp/prompt_serialization.py +20 -13
- mcp_agent/mcp/prompts/prompt_constants.py +18 -0
- mcp_agent/mcp/prompts/prompt_helpers.py +327 -0
- mcp_agent/mcp/prompts/prompt_load.py +15 -5
- mcp_agent/mcp/prompts/prompt_server.py +154 -87
- mcp_agent/mcp/prompts/prompt_template.py +26 -35
- mcp_agent/mcp/resource_utils.py +3 -1
- mcp_agent/mcp/sampling.py +24 -15
- mcp_agent/mcp_server/agent_server.py +8 -5
- mcp_agent/mcp_server_registry.py +22 -9
- mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +1 -1
- mcp_agent/resources/examples/{data-analysis → in_dev}/slides.py +1 -1
- mcp_agent/resources/examples/internal/agent.py +4 -2
- mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
- mcp_agent/resources/examples/prompting/image_server.py +3 -1
- mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
- mcp_agent/ui/console_display.py +27 -7
- fast_agent_mcp-0.1.13.dist-info/RECORD +0 -164
- mcp_agent/core/agent_app.py +0 -570
- mcp_agent/core/agent_utils.py +0 -69
- mcp_agent/core/decorators.py +0 -448
- mcp_agent/core/factory.py +0 -422
- mcp_agent/core/proxies.py +0 -278
- mcp_agent/core/types.py +0 -22
- mcp_agent/eval/__init__.py +0 -0
- mcp_agent/mcp/stdio.py +0 -114
- mcp_agent/resources/examples/data-analysis/analysis-campaign.py +0 -188
- mcp_agent/resources/examples/data-analysis/analysis.py +0 -65
- mcp_agent/resources/examples/data-analysis/fastagent.config.yaml +0 -41
- mcp_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -1471
- mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +0 -53
- mcp_agent/resources/examples/researcher/fastagent.config.yaml +0 -66
- mcp_agent/resources/examples/researcher/researcher-eval.py +0 -53
- mcp_agent/resources/examples/researcher/researcher-imp.py +0 -189
- mcp_agent/resources/examples/researcher/researcher.py +0 -39
- mcp_agent/resources/examples/workflows/chaining.py +0 -45
- mcp_agent/resources/examples/workflows/evaluator.py +0 -79
- mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
- mcp_agent/resources/examples/workflows/human_input.py +0 -26
- mcp_agent/resources/examples/workflows/orchestrator.py +0 -74
- mcp_agent/resources/examples/workflows/parallel.py +0 -79
- mcp_agent/resources/examples/workflows/router.py +0 -54
- mcp_agent/resources/examples/workflows/sse.py +0 -23
- mcp_agent/telemetry/__init__.py +0 -0
- mcp_agent/telemetry/usage_tracking.py +0 -19
- mcp_agent/workflows/__init__.py +0 -0
- mcp_agent/workflows/embedding/__init__.py +0 -0
- mcp_agent/workflows/embedding/embedding_base.py +0 -58
- mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
- mcp_agent/workflows/embedding/embedding_openai.py +0 -37
- mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -447
- mcp_agent/workflows/intent_classifier/__init__.py +0 -0
- mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -117
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -130
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -41
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -41
- mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -150
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -58
- mcp_agent/workflows/llm/__init__.py +0 -0
- mcp_agent/workflows/llm/augmented_llm_playback.py +0 -111
- mcp_agent/workflows/llm/providers/__init__.py +0 -8
- mcp_agent/workflows/orchestrator/__init__.py +0 -0
- mcp_agent/workflows/orchestrator/orchestrator.py +0 -535
- mcp_agent/workflows/parallel/__init__.py +0 -0
- mcp_agent/workflows/parallel/fan_in.py +0 -320
- mcp_agent/workflows/parallel/fan_out.py +0 -181
- mcp_agent/workflows/parallel/parallel_llm.py +0 -149
- mcp_agent/workflows/router/__init__.py +0 -0
- mcp_agent/workflows/router/router_base.py +0 -338
- mcp_agent/workflows/router/router_embedding.py +0 -226
- mcp_agent/workflows/router/router_embedding_cohere.py +0 -59
- mcp_agent/workflows/router/router_embedding_openai.py +0 -59
- mcp_agent/workflows/router/router_llm.py +0 -304
- mcp_agent/workflows/swarm/__init__.py +0 -0
- mcp_agent/workflows/swarm/swarm.py +0 -292
- mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
- mcp_agent/workflows/swarm/swarm_openai.py +0 -41
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- /mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_prompts.py +0 -0
- /mcp_agent/{workflows/llm → llm}/memory.py +0 -0
- /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
|
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.
|
32
|
-
from mcp_agent.
|
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 =
|
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(
|
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 (
|
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(
|
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
|
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"
|
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 =
|
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(
|
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"
|
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[
|
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(
|
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(
|
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.
|
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
|
354
|
+
async def _apply_prompt_provider_specific(
|
356
355
|
self,
|
357
356
|
multipart_messages: List["PromptMessageMultipart"],
|
358
357
|
request_params: RequestParams | None = None,
|
359
|
-
) ->
|
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 =
|
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
|
381
|
+
return last_message
|
393
382
|
|
394
|
-
async def
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
404
|
+
if not "OpenAI" == self.provider:
|
405
|
+
return await super().structured(prompt, model, request_params)
|
411
406
|
|
412
|
-
|
413
|
-
messages = self.history.get(include_history=True)
|
407
|
+
logger = get_logger(__name__)
|
414
408
|
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
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
|
-
#
|
424
|
-
|
425
|
-
|
426
|
-
|
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
|
-
|
441
|
-
|
442
|
-
|
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
|
-
|
445
|
-
|
438
|
+
if response and isinstance(response[0], BaseException):
|
439
|
+
raise response[0]
|
446
440
|
|
447
|
-
|
448
|
-
|
449
|
-
|
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
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
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
|
-
|
465
|
-
|
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(
|
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)
|