datarobot-genai 0.2.17__tar.gz → 0.2.25__tar.gz
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.
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/PKG-INFO +1 -1
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/pyproject.toml +4 -1
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/config.py +24 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dr_mcp_server.py +0 -3
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/mcp_instance.py +3 -88
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/tool_config.py +8 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/tool_filter.py +10 -1
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/utils.py +7 -0
- datarobot_genai-0.2.25/src/datarobot_genai/drmcp/test_utils/elicitation_test_tool.py +89 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/integration_mcp_server.py +7 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +9 -1
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +17 -4
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py +71 -8
- datarobot_genai-0.2.25/src/datarobot_genai/drmcp/test_utils/test_interactive.py +205 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/tool_base_ete.py +22 -20
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/clients/confluence.py +192 -1
- datarobot_genai-0.2.25/src/datarobot_genai/drmcp/tools/clients/gdrive.py +610 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/confluence/tools.py +133 -0
- datarobot_genai-0.2.25/src/datarobot_genai/drmcp/tools/gdrive/tools.py +177 -0
- datarobot_genai-0.2.25/src/datarobot_genai/drmcp/tools/predictive/data.py +125 -0
- datarobot_genai-0.2.25/src/datarobot_genai/drmcp/tools/predictive/project.py +90 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/training.py +160 -151
- datarobot_genai-0.2.25/src/datarobot_genai/py.typed +0 -0
- datarobot_genai-0.2.17/src/datarobot_genai/drmcp/core/mcp_server_tools.py +0 -129
- datarobot_genai-0.2.17/src/datarobot_genai/drmcp/tools/predictive/data.py +0 -97
- datarobot_genai-0.2.17/src/datarobot_genai/drmcp/tools/predictive/project.py +0 -72
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/.gitignore +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/AUTHORS +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/LICENSE +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/README.md +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/agents/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/agents/base.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/chat/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/chat/auth.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/chat/client.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/chat/responses.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/cli/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/cli/agent_environment.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/cli/agent_kernel.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/custom_model.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/mcp/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/mcp/common.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/telemetry_agent.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/utils/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/utils/auth.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/utils/urls.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/crewai/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/crewai/agent.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/crewai/base.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/crewai/events.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/crewai/mcp.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/auth.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/clients.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/config_utils.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/constants.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/credentials.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dr_mcp_server_logo.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_prompts/register.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_prompts/utils.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/register.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/schema.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/exceptions.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/logging.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/memory_management/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/memory_management/manager.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/memory_management/memory_tools.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/routes.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/routes_utils.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/server_life_cycle.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/telemetry.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/server.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/utils.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/clients/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/clients/atlassian.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/clients/jira.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/clients/s3.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/confluence/__init__.py +0 -0
- {datarobot_genai-0.2.17/src/datarobot_genai/langgraph → datarobot_genai-0.2.25/src/datarobot_genai/drmcp/tools/gdrive}/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/jira/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/jira/tools.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/deployment.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/deployment_info.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/model.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/predict.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/predict_realtime.py +0 -0
- {datarobot_genai-0.2.17/src/datarobot_genai/nat → datarobot_genai-0.2.25/src/datarobot_genai/langgraph}/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/langgraph/agent.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/langgraph/mcp.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/llama_index/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/llama_index/agent.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/llama_index/base.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/llama_index/mcp.py +0 -0
- /datarobot_genai-0.2.17/src/datarobot_genai/py.typed → /datarobot_genai-0.2.25/src/datarobot_genai/nat/__init__.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/agent.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/datarobot_auth_provider.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/datarobot_llm_clients.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/datarobot_llm_providers.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/datarobot_mcp_client.py +0 -0
- {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/helpers.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "datarobot-genai"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.25"
|
|
8
8
|
description = "Generic helpers for GenAI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10, <3.13"
|
|
@@ -205,6 +205,9 @@ asyncio_mode = "auto"
|
|
|
205
205
|
source = ["datarobot_genai"]
|
|
206
206
|
omit = [
|
|
207
207
|
"*/__init__.py",
|
|
208
|
+
"*/test_utils/*",
|
|
209
|
+
# nat requires Python >=3.11, can't be imported on 3.10 for coverage
|
|
210
|
+
"*/nat/*",
|
|
208
211
|
]
|
|
209
212
|
|
|
210
213
|
[tool.coverage.report]
|
|
@@ -245,6 +245,30 @@ class MCPServerConfig(BaseSettings):
|
|
|
245
245
|
os.getenv("CONFLUENCE_CLIENT_ID") and os.getenv("CONFLUENCE_CLIENT_SECRET")
|
|
246
246
|
)
|
|
247
247
|
|
|
248
|
+
# Gdrive tools
|
|
249
|
+
enable_gdrive_tools: bool = Field(
|
|
250
|
+
default=False,
|
|
251
|
+
validation_alias=AliasChoices(
|
|
252
|
+
RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "ENABLE_GDRIVE_TOOLS",
|
|
253
|
+
"ENABLE_GDRIVE_TOOLS",
|
|
254
|
+
),
|
|
255
|
+
description="Enable/disable GDrive tools",
|
|
256
|
+
)
|
|
257
|
+
is_gdrive_oauth_provider_configured: bool = Field(
|
|
258
|
+
default=False,
|
|
259
|
+
validation_alias=AliasChoices(
|
|
260
|
+
RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "IS_GDRIVE_OAUTH_PROVIDER_CONFIGURED",
|
|
261
|
+
"IS_GDRIVE_OAUTH_PROVIDER_CONFIGURED",
|
|
262
|
+
),
|
|
263
|
+
description="Whether GDrive OAuth provider is configured for GDrive integration",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def is_gdrive_oauth_configured(self) -> bool:
|
|
268
|
+
return self.is_gdrive_oauth_provider_configured or bool(
|
|
269
|
+
os.getenv("GDRIVE_CLIENT_ID") and os.getenv("GDRIVE_CLIENT_SECRET")
|
|
270
|
+
)
|
|
271
|
+
|
|
248
272
|
@field_validator(
|
|
249
273
|
"otel_attributes",
|
|
250
274
|
mode="before",
|
{datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dr_mcp_server.py
RENAMED
|
@@ -31,9 +31,6 @@ from .dynamic_prompts.register import register_prompts_from_datarobot_prompt_man
|
|
|
31
31
|
from .dynamic_tools.deployment.register import register_tools_of_datarobot_deployments
|
|
32
32
|
from .logging import MCPLogging
|
|
33
33
|
from .mcp_instance import mcp
|
|
34
|
-
from .mcp_server_tools import get_all_available_tags # noqa # pylint: disable=unused-import
|
|
35
|
-
from .mcp_server_tools import get_tool_info_by_name # noqa # pylint: disable=unused-import
|
|
36
|
-
from .mcp_server_tools import list_tools_by_tags # noqa # pylint: disable=unused-import
|
|
37
34
|
from .memory_management.manager import MemoryManager
|
|
38
35
|
from .routes import register_routes
|
|
39
36
|
from .routes_utils import prefix_mount_path
|
{datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/mcp_instance.py
RENAMED
|
@@ -16,17 +16,13 @@ import logging
|
|
|
16
16
|
from collections.abc import Callable
|
|
17
17
|
from functools import wraps
|
|
18
18
|
from typing import Any
|
|
19
|
-
from typing import overload
|
|
20
19
|
|
|
21
20
|
from fastmcp import Context
|
|
22
21
|
from fastmcp import FastMCP
|
|
23
22
|
from fastmcp.exceptions import NotFoundError
|
|
24
23
|
from fastmcp.prompts.prompt import Prompt
|
|
25
24
|
from fastmcp.server.dependencies import get_context
|
|
26
|
-
from fastmcp.tools import FunctionTool
|
|
27
25
|
from fastmcp.tools import Tool
|
|
28
|
-
from fastmcp.utilities.types import NotSet
|
|
29
|
-
from fastmcp.utilities.types import NotSetT
|
|
30
26
|
from mcp.types import AnyFunction
|
|
31
27
|
from mcp.types import Tool as MCPTool
|
|
32
28
|
from mcp.types import ToolAnnotations
|
|
@@ -120,86 +116,6 @@ class TaggedFastMCP(FastMCP):
|
|
|
120
116
|
"In stateless mode, clients will see changes on next request."
|
|
121
117
|
)
|
|
122
118
|
|
|
123
|
-
@overload
|
|
124
|
-
def tool(
|
|
125
|
-
self,
|
|
126
|
-
name_or_fn: AnyFunction,
|
|
127
|
-
*,
|
|
128
|
-
name: str | None = None,
|
|
129
|
-
title: str | None = None,
|
|
130
|
-
description: str | None = None,
|
|
131
|
-
tags: set[str] | None = None,
|
|
132
|
-
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
133
|
-
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
134
|
-
exclude_args: list[str] | None = None,
|
|
135
|
-
meta: dict[str, Any] | None = None,
|
|
136
|
-
enabled: bool | None = None,
|
|
137
|
-
) -> FunctionTool: ...
|
|
138
|
-
|
|
139
|
-
@overload
|
|
140
|
-
def tool(
|
|
141
|
-
self,
|
|
142
|
-
name_or_fn: str | None = None,
|
|
143
|
-
*,
|
|
144
|
-
name: str | None = None,
|
|
145
|
-
title: str | None = None,
|
|
146
|
-
description: str | None = None,
|
|
147
|
-
tags: set[str] | None = None,
|
|
148
|
-
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
149
|
-
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
150
|
-
exclude_args: list[str] | None = None,
|
|
151
|
-
meta: dict[str, Any] | None = None,
|
|
152
|
-
enabled: bool | None = None,
|
|
153
|
-
) -> Callable[[AnyFunction], FunctionTool]: ...
|
|
154
|
-
|
|
155
|
-
def tool(
|
|
156
|
-
self,
|
|
157
|
-
name_or_fn: str | Callable[..., Any] | None = None,
|
|
158
|
-
*,
|
|
159
|
-
name: str | None = None,
|
|
160
|
-
title: str | None = None,
|
|
161
|
-
description: str | None = None,
|
|
162
|
-
tags: set[str] | None = None,
|
|
163
|
-
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
|
164
|
-
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
|
165
|
-
exclude_args: list[str] | None = None,
|
|
166
|
-
meta: dict[str, Any] | None = None,
|
|
167
|
-
enabled: bool | None = None,
|
|
168
|
-
**kwargs: Any,
|
|
169
|
-
) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
|
|
170
|
-
"""
|
|
171
|
-
Extend tool decorator that supports tags and other annotations, while remaining
|
|
172
|
-
signature-compatible with FastMCP.tool to avoid recursion issues with partials.
|
|
173
|
-
"""
|
|
174
|
-
if isinstance(annotations, dict):
|
|
175
|
-
annotations = ToolAnnotations(**annotations)
|
|
176
|
-
|
|
177
|
-
# Ensure tags are available both via native fastmcp `tags` and inside annotations
|
|
178
|
-
if tags is not None:
|
|
179
|
-
tags_ = sorted(tags)
|
|
180
|
-
if annotations is None:
|
|
181
|
-
annotations = ToolAnnotations() # type: ignore[call-arg]
|
|
182
|
-
annotations.tags = tags_ # type: ignore[attr-defined, union-attr]
|
|
183
|
-
else:
|
|
184
|
-
# At this point, annotations is ToolAnnotations (not dict)
|
|
185
|
-
assert isinstance(annotations, ToolAnnotations)
|
|
186
|
-
annotations.tags = tags_ # type: ignore[attr-defined]
|
|
187
|
-
|
|
188
|
-
return super().tool(
|
|
189
|
-
name_or_fn,
|
|
190
|
-
name=name,
|
|
191
|
-
title=title,
|
|
192
|
-
description=description,
|
|
193
|
-
tags=tags,
|
|
194
|
-
output_schema=output_schema
|
|
195
|
-
if output_schema is not None
|
|
196
|
-
else kwargs.get("output_schema"),
|
|
197
|
-
annotations=annotations,
|
|
198
|
-
exclude_args=exclude_args,
|
|
199
|
-
meta=meta,
|
|
200
|
-
enabled=enabled,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
119
|
async def list_tools(
|
|
204
120
|
self, tags: list[str] | None = None, match_all: bool = False
|
|
205
121
|
) -> list[MCPTool]:
|
|
@@ -488,11 +404,10 @@ async def register_tools(
|
|
|
488
404
|
# Apply dr_mcp_extras to the memory-aware function
|
|
489
405
|
wrapped_fn = dr_mcp_extras()(memory_aware_fn)
|
|
490
406
|
|
|
491
|
-
# Create annotations
|
|
492
|
-
annotations =
|
|
493
|
-
if tags is not None:
|
|
494
|
-
annotations.tags = tags # type: ignore[attr-defined]
|
|
407
|
+
# Create annotations only when additional metadata is required
|
|
408
|
+
annotations: ToolAnnotations | None = None # type: ignore[assignment]
|
|
495
409
|
if deployment_id is not None:
|
|
410
|
+
annotations = ToolAnnotations() # type: ignore[call-arg]
|
|
496
411
|
annotations.deployment_id = deployment_id # type: ignore[attr-defined]
|
|
497
412
|
|
|
498
413
|
tool = Tool.from_function(
|
{datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/tool_config.py
RENAMED
|
@@ -29,6 +29,7 @@ class ToolType(str, Enum):
|
|
|
29
29
|
PREDICTIVE = "predictive"
|
|
30
30
|
JIRA = "jira"
|
|
31
31
|
CONFLUENCE = "confluence"
|
|
32
|
+
GDRIVE = "gdrive"
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
class ToolConfig(TypedDict):
|
|
@@ -64,6 +65,13 @@ TOOL_CONFIGS: dict[ToolType, ToolConfig] = {
|
|
|
64
65
|
package_prefix="datarobot_genai.drmcp.tools.confluence",
|
|
65
66
|
config_field_name="enable_confluence_tools",
|
|
66
67
|
),
|
|
68
|
+
ToolType.GDRIVE: ToolConfig(
|
|
69
|
+
name="gdrive",
|
|
70
|
+
oauth_check=lambda config: config.is_gdrive_oauth_configured,
|
|
71
|
+
directory="gdrive",
|
|
72
|
+
package_prefix="datarobot_genai.drmcp.tools.gdrive",
|
|
73
|
+
config_field_name="enable_gdrive_tools",
|
|
74
|
+
),
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
|
{datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/tool_filter.py
RENAMED
|
@@ -41,7 +41,7 @@ def filter_tools_by_tags(
|
|
|
41
41
|
filtered_tools = []
|
|
42
42
|
|
|
43
43
|
for tool in tools:
|
|
44
|
-
tool_tags =
|
|
44
|
+
tool_tags = get_tool_tags(tool)
|
|
45
45
|
|
|
46
46
|
if not tool_tags:
|
|
47
47
|
continue
|
|
@@ -68,9 +68,18 @@ def get_tool_tags(tool: Tool | MCPTool) -> list[str]:
|
|
|
68
68
|
-------
|
|
69
69
|
List of tags for the tool
|
|
70
70
|
"""
|
|
71
|
+
# Primary: native FastMCP meta location
|
|
72
|
+
if hasattr(tool, "meta") and getattr(tool, "meta"):
|
|
73
|
+
fastmcp_meta = tool.meta.get("_fastmcp", {})
|
|
74
|
+
meta_tags = fastmcp_meta.get("tags", [])
|
|
75
|
+
if isinstance(meta_tags, list):
|
|
76
|
+
return meta_tags
|
|
77
|
+
|
|
78
|
+
# Fallback: annotations.tags (for compatibility during transition)
|
|
71
79
|
if tool.annotations and hasattr(tool.annotations, "tags"):
|
|
72
80
|
tags = getattr(tool.annotations, "tags", [])
|
|
73
81
|
return tags if isinstance(tags, list) else []
|
|
82
|
+
|
|
74
83
|
return []
|
|
75
84
|
|
|
76
85
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import base64
|
|
15
15
|
import uuid
|
|
16
16
|
from typing import Any
|
|
17
|
+
from urllib.parse import urlparse
|
|
17
18
|
|
|
18
19
|
import boto3
|
|
19
20
|
from fastmcp.resources import HttpResource
|
|
@@ -129,3 +130,9 @@ def format_response_as_tool_result(data: bytes, content_type: str, charset: str)
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
return ToolResult(structured_content=payload)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def is_valid_url(url: str) -> bool:
|
|
136
|
+
"""Check if a URL is valid."""
|
|
137
|
+
result = urlparse(url)
|
|
138
|
+
return all([result.scheme, result.netloc])
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Copyright 2025 DataRobot, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Test tool for elicitation testing.
|
|
16
|
+
|
|
17
|
+
This module registers a test tool that can be used to test elicitation support.
|
|
18
|
+
It should be imported in tests that need it.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from fastmcp import Context
|
|
22
|
+
from fastmcp.server.context import AcceptedElicitation
|
|
23
|
+
from fastmcp.server.context import CancelledElicitation
|
|
24
|
+
from fastmcp.server.context import DeclinedElicitation
|
|
25
|
+
|
|
26
|
+
from datarobot_genai.drmcp.core.mcp_instance import mcp
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@mcp.tool(
|
|
30
|
+
name="get_user_greeting",
|
|
31
|
+
description=(
|
|
32
|
+
"Get a personalized greeting for a user. "
|
|
33
|
+
"Requires a username - if not provided, will request it via elicitation."
|
|
34
|
+
),
|
|
35
|
+
tags={"test", "elicitation"},
|
|
36
|
+
)
|
|
37
|
+
async def get_user_greeting(ctx: Context, username: str | None = None) -> dict:
|
|
38
|
+
"""
|
|
39
|
+
Get a personalized greeting for a user.
|
|
40
|
+
|
|
41
|
+
This tool demonstrates FastMCP's built-in elicitation by requiring a username parameter.
|
|
42
|
+
If username is not provided, it uses ctx.elicit() to request it from the user.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
ctx: FastMCP context (automatically injected)
|
|
46
|
+
username: The username to greet. If None, elicitation will be triggered.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
Dictionary with greeting message or error if elicitation was declined/cancelled
|
|
51
|
+
"""
|
|
52
|
+
if not username:
|
|
53
|
+
# Use elicitation to request username from the client
|
|
54
|
+
try:
|
|
55
|
+
result = await ctx.elicit(
|
|
56
|
+
message="Username is required to generate a personalized greeting",
|
|
57
|
+
response_type=str,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if isinstance(result, AcceptedElicitation):
|
|
61
|
+
username = result.data
|
|
62
|
+
elif isinstance(result, DeclinedElicitation):
|
|
63
|
+
return {
|
|
64
|
+
"status": "error",
|
|
65
|
+
"error": "Username declined by user",
|
|
66
|
+
"message": "Cannot generate greeting without username",
|
|
67
|
+
}
|
|
68
|
+
elif isinstance(result, CancelledElicitation):
|
|
69
|
+
return {
|
|
70
|
+
"status": "error",
|
|
71
|
+
"error": "Operation cancelled",
|
|
72
|
+
"message": "Greeting request was cancelled",
|
|
73
|
+
}
|
|
74
|
+
except Exception:
|
|
75
|
+
# Elicitation not supported by client - return graceful skip
|
|
76
|
+
return {
|
|
77
|
+
"status": "skipped",
|
|
78
|
+
"message": (
|
|
79
|
+
"Elicitation not supported by client. "
|
|
80
|
+
"Username parameter is required when client does not support elicitation."
|
|
81
|
+
),
|
|
82
|
+
"elicitation_supported": False,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"status": "success",
|
|
87
|
+
"message": f"Hello, {username}! Welcome to the DataRobot MCP server.",
|
|
88
|
+
"username": username,
|
|
89
|
+
}
|
|
@@ -26,6 +26,13 @@ from typing import Any
|
|
|
26
26
|
|
|
27
27
|
from datarobot_genai.drmcp import create_mcp_server
|
|
28
28
|
|
|
29
|
+
# Import elicitation test tool to register it with the MCP server
|
|
30
|
+
try:
|
|
31
|
+
from datarobot_genai.drmcp.test_utils import elicitation_test_tool # noqa: F401
|
|
32
|
+
except ImportError:
|
|
33
|
+
# Test utils not available (e.g., running in production)
|
|
34
|
+
pass
|
|
35
|
+
|
|
29
36
|
# Import user components (will be used conditionally)
|
|
30
37
|
try:
|
|
31
38
|
from app.core.server_lifecycle import ServerLifecycle # type: ignore # noqa: F401
|
|
@@ -15,6 +15,7 @@ import asyncio
|
|
|
15
15
|
import os
|
|
16
16
|
from collections.abc import AsyncGenerator
|
|
17
17
|
from contextlib import asynccontextmanager
|
|
18
|
+
from typing import Any
|
|
18
19
|
|
|
19
20
|
import aiohttp
|
|
20
21
|
from aiohttp import ClientSession as HttpClientSession
|
|
@@ -78,6 +79,7 @@ def get_headers() -> dict[str, str]:
|
|
|
78
79
|
@asynccontextmanager
|
|
79
80
|
async def ete_test_mcp_session(
|
|
80
81
|
additional_headers: dict[str, str] | None = None,
|
|
82
|
+
elicitation_callback: Any | None = None,
|
|
81
83
|
) -> AsyncGenerator[ClientSession, None]:
|
|
82
84
|
"""Create an MCP session for each test.
|
|
83
85
|
|
|
@@ -85,6 +87,10 @@ async def ete_test_mcp_session(
|
|
|
85
87
|
----------
|
|
86
88
|
additional_headers : dict[str, str], optional
|
|
87
89
|
Additional headers to include in the MCP session (e.g., auth headers for testing).
|
|
90
|
+
elicitation_callback : callable, optional
|
|
91
|
+
Callback function to handle elicitation requests from the server.
|
|
92
|
+
The callback should have signature:
|
|
93
|
+
async def callback(context, params: ElicitRequestParams) -> ElicitResult
|
|
88
94
|
"""
|
|
89
95
|
try:
|
|
90
96
|
headers = get_headers()
|
|
@@ -96,7 +102,9 @@ async def ete_test_mcp_session(
|
|
|
96
102
|
write_stream,
|
|
97
103
|
_,
|
|
98
104
|
):
|
|
99
|
-
async with ClientSession(
|
|
105
|
+
async with ClientSession(
|
|
106
|
+
read_stream, write_stream, elicitation_callback=elicitation_callback
|
|
107
|
+
) as session:
|
|
100
108
|
await asyncio.wait_for(session.initialize(), timeout=5)
|
|
101
109
|
yield session
|
|
102
110
|
except asyncio.TimeoutError:
|
|
@@ -17,6 +17,7 @@ import contextlib
|
|
|
17
17
|
import os
|
|
18
18
|
from collections.abc import AsyncGenerator
|
|
19
19
|
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
20
21
|
|
|
21
22
|
from mcp import ClientSession
|
|
22
23
|
from mcp.client.stdio import StdioServerParameters
|
|
@@ -34,7 +35,12 @@ def integration_test_mcp_server_params() -> StdioServerParameters:
|
|
|
34
35
|
or "https://test.datarobot.com/api/v2",
|
|
35
36
|
"MCP_SERVER_LOG_LEVEL": os.environ.get("MCP_SERVER_LOG_LEVEL") or "WARNING",
|
|
36
37
|
"APP_LOG_LEVEL": os.environ.get("APP_LOG_LEVEL") or "WARNING",
|
|
37
|
-
|
|
38
|
+
# Disable all OTEL telemetry for integration tests
|
|
39
|
+
"OTEL_ENABLED": "false",
|
|
40
|
+
"OTEL_SDK_DISABLED": "true",
|
|
41
|
+
"OTEL_TRACES_EXPORTER": "none",
|
|
42
|
+
"OTEL_LOGS_EXPORTER": "none",
|
|
43
|
+
"OTEL_METRICS_EXPORTER": "none",
|
|
38
44
|
"MCP_SERVER_REGISTER_DYNAMIC_TOOLS_ON_STARTUP": os.environ.get(
|
|
39
45
|
"MCP_SERVER_REGISTER_DYNAMIC_TOOLS_ON_STARTUP"
|
|
40
46
|
)
|
|
@@ -64,7 +70,9 @@ def integration_test_mcp_server_params() -> StdioServerParameters:
|
|
|
64
70
|
|
|
65
71
|
@contextlib.asynccontextmanager
|
|
66
72
|
async def integration_test_mcp_session(
|
|
67
|
-
server_params: StdioServerParameters | None = None,
|
|
73
|
+
server_params: StdioServerParameters | None = None,
|
|
74
|
+
timeout: int = 30,
|
|
75
|
+
elicitation_callback: Any | None = None,
|
|
68
76
|
) -> AsyncGenerator[ClientSession, None]:
|
|
69
77
|
"""
|
|
70
78
|
Create and connect a client for the MCP server as a context manager.
|
|
@@ -72,6 +80,7 @@ async def integration_test_mcp_session(
|
|
|
72
80
|
Args:
|
|
73
81
|
server_params: Parameters for configuring the server connection
|
|
74
82
|
timeout: Timeout
|
|
83
|
+
elicitation_callback: Optional callback for handling elicitation requests
|
|
75
84
|
|
|
76
85
|
Yields
|
|
77
86
|
------
|
|
@@ -86,8 +95,12 @@ async def integration_test_mcp_session(
|
|
|
86
95
|
|
|
87
96
|
try:
|
|
88
97
|
async with stdio_client(server_params) as (read_stream, write_stream):
|
|
89
|
-
async with ClientSession(
|
|
90
|
-
|
|
98
|
+
async with ClientSession(
|
|
99
|
+
read_stream, write_stream, elicitation_callback=elicitation_callback
|
|
100
|
+
) as session:
|
|
101
|
+
init_result = await asyncio.wait_for(session.initialize(), timeout=timeout)
|
|
102
|
+
# Store the init result on the session for tests that need to inspect capabilities
|
|
103
|
+
session._init_result = init_result # type: ignore[attr-defined]
|
|
91
104
|
yield session
|
|
92
105
|
|
|
93
106
|
except asyncio.TimeoutError:
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
|
+
from ast import literal_eval
|
|
16
17
|
from typing import Any
|
|
17
18
|
|
|
18
19
|
import openai
|
|
@@ -44,12 +45,39 @@ class LLMResponse:
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
class LLMMCPClient:
|
|
47
|
-
"""
|
|
48
|
+
"""
|
|
49
|
+
Client for interacting with LLMs via MCP.
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
Note: Elicitation is handled at the protocol level by FastMCP's ctx.elicit().
|
|
52
|
+
Tools using FastMCP's built-in elicitation will work automatically.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
config: str,
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Initialize the LLM MCP client.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
config: Configuration string or dict with:
|
|
64
|
+
- openai_api_key: OpenAI API key
|
|
65
|
+
- openai_api_base: Optional Azure OpenAI endpoint
|
|
66
|
+
- openai_api_deployment_id: Optional Azure deployment ID
|
|
67
|
+
- openai_api_version: Optional Azure API version
|
|
68
|
+
- model: Model name (default: "gpt-3.5-turbo")
|
|
69
|
+
- save_llm_responses: Whether to save responses (default: True)
|
|
70
|
+
"""
|
|
51
71
|
# Parse config string to extract parameters
|
|
52
|
-
|
|
72
|
+
if isinstance(config, str):
|
|
73
|
+
# Try JSON first (safer), fall back to literal_eval for Python dict strings
|
|
74
|
+
try:
|
|
75
|
+
config_dict = json.loads(config)
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
# Fall back to literal_eval for Python dict literal strings
|
|
78
|
+
config_dict = literal_eval(config)
|
|
79
|
+
else:
|
|
80
|
+
config_dict = config
|
|
53
81
|
|
|
54
82
|
openai_api_key = config_dict.get("openai_api_key")
|
|
55
83
|
openai_api_base = config_dict.get("openai_api_base")
|
|
@@ -93,7 +121,21 @@ class LLMMCPClient:
|
|
|
93
121
|
async def _call_mcp_tool(
|
|
94
122
|
self, tool_name: str, parameters: dict[str, Any], mcp_session: ClientSession
|
|
95
123
|
) -> str:
|
|
96
|
-
"""
|
|
124
|
+
"""
|
|
125
|
+
Call an MCP tool and return the result as a string.
|
|
126
|
+
|
|
127
|
+
Note: Elicitation is handled at the protocol level by FastMCP's ctx.elicit().
|
|
128
|
+
Tools using FastMCP's built-in elicitation will work automatically.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
tool_name: Name of the tool to call
|
|
132
|
+
parameters: Parameters to pass to the tool
|
|
133
|
+
mcp_session: MCP client session
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
Result text from the tool call
|
|
138
|
+
"""
|
|
97
139
|
result: CallToolResult = await mcp_session.call_tool(tool_name, parameters)
|
|
98
140
|
content = (
|
|
99
141
|
result.content[0].text
|
|
@@ -177,7 +219,26 @@ class LLMMCPClient:
|
|
|
177
219
|
async def process_prompt_with_mcp_support(
|
|
178
220
|
self, prompt: str, mcp_session: ClientSession, output_file_name: str = ""
|
|
179
221
|
) -> LLMResponse:
|
|
180
|
-
"""
|
|
222
|
+
"""
|
|
223
|
+
Process a prompt with MCP tool support and elicitation handling.
|
|
224
|
+
|
|
225
|
+
This method:
|
|
226
|
+
1. Adds MCP tools to available tools
|
|
227
|
+
2. Sends prompt to LLM
|
|
228
|
+
3. Processes tool calls
|
|
229
|
+
4. Continues until LLM provides final response
|
|
230
|
+
|
|
231
|
+
Note: Elicitation is handled at the protocol level by FastMCP's ctx.elicit().
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
prompt: User prompt
|
|
235
|
+
mcp_session: MCP client session
|
|
236
|
+
output_file_name: Optional file name to save response
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
LLMResponse with content, tool calls, and tool results
|
|
241
|
+
"""
|
|
181
242
|
# Add MCP tools to available tools
|
|
182
243
|
await self._add_mcp_tool_to_available_tools(mcp_session)
|
|
183
244
|
|
|
@@ -191,8 +252,10 @@ class LLMMCPClient:
|
|
|
191
252
|
"content": (
|
|
192
253
|
"You are a helpful AI assistant that can use tools to help users. "
|
|
193
254
|
"If you need more information to provide a complete response, you can make "
|
|
194
|
-
"multiple tool calls
|
|
195
|
-
"
|
|
255
|
+
"multiple tool calls or ask the user for more info, but prefer tool calls "
|
|
256
|
+
"when possible. "
|
|
257
|
+
"When dealing with file paths, use them as raw paths without converting "
|
|
258
|
+
"to file:// URLs."
|
|
196
259
|
),
|
|
197
260
|
},
|
|
198
261
|
{"role": "user", "content": prompt},
|