datarobot-genai 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.
- datarobot_genai/__init__.py +19 -0
- datarobot_genai/core/__init__.py +0 -0
- datarobot_genai/core/agents/__init__.py +43 -0
- datarobot_genai/core/agents/base.py +195 -0
- datarobot_genai/core/chat/__init__.py +19 -0
- datarobot_genai/core/chat/auth.py +146 -0
- datarobot_genai/core/chat/client.py +178 -0
- datarobot_genai/core/chat/responses.py +297 -0
- datarobot_genai/core/cli/__init__.py +18 -0
- datarobot_genai/core/cli/agent_environment.py +47 -0
- datarobot_genai/core/cli/agent_kernel.py +211 -0
- datarobot_genai/core/custom_model.py +141 -0
- datarobot_genai/core/mcp/__init__.py +0 -0
- datarobot_genai/core/mcp/common.py +218 -0
- datarobot_genai/core/telemetry_agent.py +126 -0
- datarobot_genai/core/utils/__init__.py +3 -0
- datarobot_genai/core/utils/auth.py +234 -0
- datarobot_genai/core/utils/urls.py +64 -0
- datarobot_genai/crewai/__init__.py +24 -0
- datarobot_genai/crewai/agent.py +42 -0
- datarobot_genai/crewai/base.py +159 -0
- datarobot_genai/crewai/events.py +117 -0
- datarobot_genai/crewai/mcp.py +59 -0
- datarobot_genai/drmcp/__init__.py +78 -0
- datarobot_genai/drmcp/core/__init__.py +13 -0
- datarobot_genai/drmcp/core/auth.py +165 -0
- datarobot_genai/drmcp/core/clients.py +180 -0
- datarobot_genai/drmcp/core/config.py +250 -0
- datarobot_genai/drmcp/core/config_utils.py +174 -0
- datarobot_genai/drmcp/core/constants.py +18 -0
- datarobot_genai/drmcp/core/credentials.py +190 -0
- datarobot_genai/drmcp/core/dr_mcp_server.py +316 -0
- datarobot_genai/drmcp/core/dr_mcp_server_logo.py +136 -0
- datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +13 -0
- datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +130 -0
- datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +128 -0
- datarobot_genai/drmcp/core/dynamic_prompts/register.py +206 -0
- datarobot_genai/drmcp/core/dynamic_prompts/utils.py +33 -0
- datarobot_genai/drmcp/core/dynamic_tools/__init__.py +14 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +14 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +72 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +82 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +238 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +228 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +63 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +162 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +87 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +36 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +10 -0
- datarobot_genai/drmcp/core/dynamic_tools/register.py +254 -0
- datarobot_genai/drmcp/core/dynamic_tools/schema.py +532 -0
- datarobot_genai/drmcp/core/exceptions.py +25 -0
- datarobot_genai/drmcp/core/logging.py +98 -0
- datarobot_genai/drmcp/core/mcp_instance.py +542 -0
- datarobot_genai/drmcp/core/mcp_server_tools.py +129 -0
- datarobot_genai/drmcp/core/memory_management/__init__.py +13 -0
- datarobot_genai/drmcp/core/memory_management/manager.py +820 -0
- datarobot_genai/drmcp/core/memory_management/memory_tools.py +201 -0
- datarobot_genai/drmcp/core/routes.py +436 -0
- datarobot_genai/drmcp/core/routes_utils.py +30 -0
- datarobot_genai/drmcp/core/server_life_cycle.py +107 -0
- datarobot_genai/drmcp/core/telemetry.py +424 -0
- datarobot_genai/drmcp/core/tool_filter.py +108 -0
- datarobot_genai/drmcp/core/utils.py +131 -0
- datarobot_genai/drmcp/server.py +19 -0
- datarobot_genai/drmcp/test_utils/__init__.py +13 -0
- datarobot_genai/drmcp/test_utils/integration_mcp_server.py +102 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +96 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +94 -0
- datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py +234 -0
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +151 -0
- datarobot_genai/drmcp/test_utils/utils.py +91 -0
- datarobot_genai/drmcp/tools/__init__.py +14 -0
- datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
- datarobot_genai/drmcp/tools/predictive/data.py +97 -0
- datarobot_genai/drmcp/tools/predictive/deployment.py +91 -0
- datarobot_genai/drmcp/tools/predictive/deployment_info.py +392 -0
- datarobot_genai/drmcp/tools/predictive/model.py +148 -0
- datarobot_genai/drmcp/tools/predictive/predict.py +254 -0
- datarobot_genai/drmcp/tools/predictive/predict_realtime.py +307 -0
- datarobot_genai/drmcp/tools/predictive/project.py +72 -0
- datarobot_genai/drmcp/tools/predictive/training.py +651 -0
- datarobot_genai/langgraph/__init__.py +0 -0
- datarobot_genai/langgraph/agent.py +341 -0
- datarobot_genai/langgraph/mcp.py +73 -0
- datarobot_genai/llama_index/__init__.py +16 -0
- datarobot_genai/llama_index/agent.py +50 -0
- datarobot_genai/llama_index/base.py +299 -0
- datarobot_genai/llama_index/mcp.py +79 -0
- datarobot_genai/nat/__init__.py +0 -0
- datarobot_genai/nat/agent.py +258 -0
- datarobot_genai/nat/datarobot_llm_clients.py +249 -0
- datarobot_genai/nat/datarobot_llm_providers.py +130 -0
- datarobot_genai/py.typed +0 -0
- datarobot_genai-0.2.0.dist-info/METADATA +139 -0
- datarobot_genai-0.2.0.dist-info/RECORD +101 -0
- datarobot_genai-0.2.0.dist-info/WHEEL +4 -0
- datarobot_genai-0.2.0.dist-info/entry_points.txt +3 -0
- datarobot_genai-0.2.0.dist-info/licenses/AUTHORS +2 -0
- datarobot_genai-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,316 @@
|
|
|
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
|
+
import asyncio
|
|
16
|
+
import glob
|
|
17
|
+
import importlib
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from fastmcp import FastMCP
|
|
24
|
+
from starlette.middleware import Middleware
|
|
25
|
+
|
|
26
|
+
from .auth import initialize_oauth_middleware
|
|
27
|
+
from .config import get_config
|
|
28
|
+
from .credentials import get_credentials
|
|
29
|
+
from .dr_mcp_server_logo import log_server_custom_banner
|
|
30
|
+
from .dynamic_prompts.register import register_prompts_from_datarobot_prompt_management
|
|
31
|
+
from .dynamic_tools.deployment.register import register_tools_of_datarobot_deployments
|
|
32
|
+
from .logging import MCPLogging
|
|
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
|
+
from .memory_management.manager import MemoryManager
|
|
38
|
+
from .routes import register_routes
|
|
39
|
+
from .routes_utils import prefix_mount_path
|
|
40
|
+
from .server_life_cycle import BaseServerLifecycle
|
|
41
|
+
from .telemetry import OtelASGIMiddleware
|
|
42
|
+
from .telemetry import initialize_telemetry
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _import_modules_from_dir(
|
|
46
|
+
directory: str, package_prefix: str, module_name: str | None = None
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Dynamically import all modules from a directory."""
|
|
49
|
+
if not os.path.exists(directory):
|
|
50
|
+
return
|
|
51
|
+
if module_name:
|
|
52
|
+
module_name = f"{package_prefix}.{module_name}"
|
|
53
|
+
try:
|
|
54
|
+
importlib.import_module(module_name)
|
|
55
|
+
except ImportError as e:
|
|
56
|
+
logging.warning(f"Failed to import module {module_name}: {e}")
|
|
57
|
+
else:
|
|
58
|
+
for file in glob.glob(os.path.join(directory, "*.py")):
|
|
59
|
+
if os.path.basename(file) != "__init__.py":
|
|
60
|
+
module_name = f"{package_prefix}.{os.path.splitext(os.path.basename(file))[0]}"
|
|
61
|
+
try:
|
|
62
|
+
importlib.import_module(module_name)
|
|
63
|
+
except ImportError as e:
|
|
64
|
+
logging.warning(f"Failed to import module {module_name}: {e}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DataRobotMCPServer:
|
|
68
|
+
"""
|
|
69
|
+
DataRobot MCP server implementation using FastMCP framework.
|
|
70
|
+
|
|
71
|
+
This server can be extended by providing custom configuration, credentials,
|
|
72
|
+
and lifecycle handlers.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
mcp: FastMCP,
|
|
78
|
+
transport: str = "streamable-http",
|
|
79
|
+
config_factory: Callable[[], Any] | None = None,
|
|
80
|
+
credentials_factory: Callable[[], Any] | None = None,
|
|
81
|
+
lifecycle: BaseServerLifecycle | None = None,
|
|
82
|
+
additional_module_paths: list[tuple[str, str]] | None = None,
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Initialize the server.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
mcp: FastMCP instance
|
|
89
|
+
transport: Transport type ("streamable-http" or "stdio")
|
|
90
|
+
config_factory: Optional factory function for user config
|
|
91
|
+
credentials_factory: Optional factory function for user credentials
|
|
92
|
+
lifecycle: Optional lifecycle handler (defaults to BaseServerLifecycle())
|
|
93
|
+
additional_module_paths: Optional list of (directory, package_prefix) tuples for
|
|
94
|
+
loading additional modules
|
|
95
|
+
"""
|
|
96
|
+
# Initialize config and logging
|
|
97
|
+
self._config = get_config()
|
|
98
|
+
MCPLogging(self._config.app_log_level)
|
|
99
|
+
self._logger = logging.getLogger(self.__class__.__name__)
|
|
100
|
+
self._logger.info(f"Config initialized: {self._config}")
|
|
101
|
+
|
|
102
|
+
# Initialize credentials
|
|
103
|
+
self._credentials = get_credentials()
|
|
104
|
+
self._logger.info("Credentials initialized")
|
|
105
|
+
|
|
106
|
+
self._user_config = config_factory() if config_factory else None
|
|
107
|
+
self._logger.info(f"User config initialized: {self._user_config}")
|
|
108
|
+
self._user_credentials = credentials_factory() if credentials_factory else None
|
|
109
|
+
self._logger.info("User credentials initialized")
|
|
110
|
+
|
|
111
|
+
# Initialize lifecycle
|
|
112
|
+
self._lifecycle = lifecycle if lifecycle else BaseServerLifecycle()
|
|
113
|
+
self._logger.info("Lifecycle initialized")
|
|
114
|
+
|
|
115
|
+
self._mcp = mcp
|
|
116
|
+
self._mcp_transport = transport
|
|
117
|
+
|
|
118
|
+
# Initialize telemetry
|
|
119
|
+
initialize_telemetry(mcp)
|
|
120
|
+
|
|
121
|
+
# Initialize OAuth middleware
|
|
122
|
+
initialize_oauth_middleware(mcp)
|
|
123
|
+
|
|
124
|
+
# Initialize memory manager if AWS credentials are available
|
|
125
|
+
self._memory_manager: MemoryManager | None = None
|
|
126
|
+
if self._config.enable_memory_management:
|
|
127
|
+
if self._credentials.has_aws_credentials():
|
|
128
|
+
self._logger.info("Initializing memory manager")
|
|
129
|
+
try:
|
|
130
|
+
self._memory_manager = MemoryManager.get_instance()
|
|
131
|
+
except Exception as e:
|
|
132
|
+
self._logger.error(f"Error initializing memory manager: {e}")
|
|
133
|
+
self._logger.info("Skipping memory manager initialization")
|
|
134
|
+
self._memory_manager = None
|
|
135
|
+
else:
|
|
136
|
+
self._logger.info(
|
|
137
|
+
"No AWS credentials found, skipping memory manager initialization"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Load static tools modules
|
|
141
|
+
base_dir = os.path.dirname(os.path.dirname(__file__))
|
|
142
|
+
if self._config.enable_predictive_tools:
|
|
143
|
+
_import_modules_from_dir(
|
|
144
|
+
os.path.join(base_dir, "tools", "predictive"),
|
|
145
|
+
"datarobot_genai.drmcp.tools.predictive",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Load memory management tools if available
|
|
149
|
+
if self._memory_manager:
|
|
150
|
+
_import_modules_from_dir(
|
|
151
|
+
directory=os.path.join(base_dir, "core", "memory_management"),
|
|
152
|
+
package_prefix="datarobot_genai.drmcp.core.memory_management",
|
|
153
|
+
module_name="memory_tools",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Load additional recipe user modules if provided
|
|
157
|
+
if additional_module_paths:
|
|
158
|
+
for directory, package_prefix in additional_module_paths:
|
|
159
|
+
self._logger.info(f"Loading additional modules from {directory}")
|
|
160
|
+
_import_modules_from_dir(directory, package_prefix)
|
|
161
|
+
|
|
162
|
+
# Register HTTP routes if using streamable-http transport
|
|
163
|
+
if transport == "streamable-http":
|
|
164
|
+
register_routes(self._mcp)
|
|
165
|
+
|
|
166
|
+
def run(self, show_banner: bool = False) -> None:
|
|
167
|
+
"""Run the DataRobot MCP server synchronously."""
|
|
168
|
+
try:
|
|
169
|
+
# Validate configuration
|
|
170
|
+
if not self._credentials.has_datarobot_credentials():
|
|
171
|
+
self._logger.error("DataRobot credentials not configured")
|
|
172
|
+
raise ValueError("Missing required DataRobot credentials")
|
|
173
|
+
|
|
174
|
+
if self._config.mcp_server_register_dynamic_tools_on_startup:
|
|
175
|
+
self._logger.info("Registering dynamic tools from deployments...")
|
|
176
|
+
asyncio.run(register_tools_of_datarobot_deployments())
|
|
177
|
+
|
|
178
|
+
if self._config.mcp_server_register_dynamic_prompts_on_startup:
|
|
179
|
+
self._logger.info("Registering dynamic prompts from prompt management...")
|
|
180
|
+
asyncio.run(register_prompts_from_datarobot_prompt_management())
|
|
181
|
+
|
|
182
|
+
# List registered tools, prompts, and resources before starting server
|
|
183
|
+
tools = asyncio.run(self._mcp._list_tools_mcp())
|
|
184
|
+
prompts = asyncio.run(self._mcp._list_prompts_mcp())
|
|
185
|
+
resources = asyncio.run(self._mcp._list_resources_mcp())
|
|
186
|
+
|
|
187
|
+
tools_count = len(tools)
|
|
188
|
+
prompts_count = len(prompts)
|
|
189
|
+
resources_count = len(resources)
|
|
190
|
+
|
|
191
|
+
self._logger.info(f"Registered tools: {tools_count}")
|
|
192
|
+
for tool in tools:
|
|
193
|
+
self._logger.info(f" > {tool.name}")
|
|
194
|
+
self._logger.info(f"Registered prompts: {prompts_count}")
|
|
195
|
+
for prompt in prompts:
|
|
196
|
+
self._logger.info(f" > {prompt.name}")
|
|
197
|
+
self._logger.info(f"Registered resources: {resources_count}")
|
|
198
|
+
for resource in resources:
|
|
199
|
+
self._logger.info(f" > {resource.name}")
|
|
200
|
+
|
|
201
|
+
# Execute pre-server start actions
|
|
202
|
+
asyncio.run(self._lifecycle.pre_server_start(self._mcp))
|
|
203
|
+
|
|
204
|
+
# Create event loop for async operations
|
|
205
|
+
loop = asyncio.new_event_loop()
|
|
206
|
+
asyncio.set_event_loop(loop)
|
|
207
|
+
|
|
208
|
+
async def run_server(show_banner: bool = show_banner) -> None:
|
|
209
|
+
# Start server in background based on transport type
|
|
210
|
+
|
|
211
|
+
if show_banner:
|
|
212
|
+
log_server_custom_banner(
|
|
213
|
+
self._mcp,
|
|
214
|
+
self._mcp_transport,
|
|
215
|
+
port=self._config.mcp_server_port,
|
|
216
|
+
tools_count=tools_count,
|
|
217
|
+
prompts_count=prompts_count,
|
|
218
|
+
resources_count=resources_count,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if self._mcp_transport == "stdio":
|
|
222
|
+
server_task = asyncio.create_task(self._mcp.run_stdio_async(show_banner=False))
|
|
223
|
+
elif self._mcp_transport == "streamable-http":
|
|
224
|
+
server_task = asyncio.create_task(
|
|
225
|
+
self._mcp.run_http_async(
|
|
226
|
+
transport="http",
|
|
227
|
+
middleware=[Middleware(OtelASGIMiddleware)],
|
|
228
|
+
show_banner=False,
|
|
229
|
+
port=self._config.mcp_server_port,
|
|
230
|
+
log_level=self._config.mcp_server_log_level,
|
|
231
|
+
host=self._config.mcp_server_host,
|
|
232
|
+
stateless_http=True,
|
|
233
|
+
path=prefix_mount_path("/mcp"),
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
raise ValueError(f"Unsupported transport: {self._mcp_transport}")
|
|
238
|
+
|
|
239
|
+
# Give the server a moment to initialize
|
|
240
|
+
await asyncio.sleep(1)
|
|
241
|
+
|
|
242
|
+
# Execute post-server start actions
|
|
243
|
+
await self._lifecycle.post_server_start(self._mcp)
|
|
244
|
+
|
|
245
|
+
# Wait for server to complete
|
|
246
|
+
await server_task
|
|
247
|
+
|
|
248
|
+
# Start the server
|
|
249
|
+
self._logger.info("Starting MCP server...")
|
|
250
|
+
try:
|
|
251
|
+
loop.run_until_complete(run_server(show_banner=show_banner))
|
|
252
|
+
except KeyboardInterrupt:
|
|
253
|
+
self._logger.info("Server interrupted by user")
|
|
254
|
+
finally:
|
|
255
|
+
# Execute pre-shutdown actions
|
|
256
|
+
self._logger.info("Shutting down server...")
|
|
257
|
+
loop.run_until_complete(self._lifecycle.pre_server_shutdown(self._mcp))
|
|
258
|
+
loop.close()
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
self._logger.error(f"Server error: {e}")
|
|
262
|
+
raise
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def create_mcp_server(
|
|
266
|
+
config_factory: Callable[[], Any] | None = None,
|
|
267
|
+
credentials_factory: Callable[[], Any] | None = None,
|
|
268
|
+
lifecycle: BaseServerLifecycle | None = None,
|
|
269
|
+
additional_module_paths: list[tuple[str, str]] | None = None,
|
|
270
|
+
transport: str = "streamable-http",
|
|
271
|
+
) -> DataRobotMCPServer:
|
|
272
|
+
"""
|
|
273
|
+
Create a DataRobot MCP server.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
config_factory: Optional factory function for user config
|
|
277
|
+
credentials_factory: Optional factory function for user credentials
|
|
278
|
+
lifecycle: Optional lifecycle handler
|
|
279
|
+
additional_module_paths: Optional list of (directory, package_prefix) tuples
|
|
280
|
+
transport: Transport type ("streamable-http" or "stdio")
|
|
281
|
+
|
|
282
|
+
Returns
|
|
283
|
+
-------
|
|
284
|
+
Configured DataRobotMCPServer instance
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
```python
|
|
288
|
+
# Basic usage with defaults
|
|
289
|
+
server = create_mcp_server()
|
|
290
|
+
server.run()
|
|
291
|
+
|
|
292
|
+
# With custom configuration
|
|
293
|
+
from myapp.config import get_my_config
|
|
294
|
+
from myapp.lifecycle import MyLifecycle
|
|
295
|
+
|
|
296
|
+
server = create_mcp_server(
|
|
297
|
+
config_factory=get_my_config,
|
|
298
|
+
lifecycle=MyLifecycle(),
|
|
299
|
+
additional_module_paths=[
|
|
300
|
+
("/path/to/my/tools", "myapp.tools"),
|
|
301
|
+
("/path/to/my/prompts", "myapp.prompts"),
|
|
302
|
+
]
|
|
303
|
+
)
|
|
304
|
+
server.run()
|
|
305
|
+
```
|
|
306
|
+
"""
|
|
307
|
+
# Use the global mcp instance that tools are registered with
|
|
308
|
+
|
|
309
|
+
return DataRobotMCPServer(
|
|
310
|
+
mcp=mcp,
|
|
311
|
+
transport=transport,
|
|
312
|
+
config_factory=config_factory,
|
|
313
|
+
credentials_factory=credentials_factory,
|
|
314
|
+
lifecycle=lifecycle,
|
|
315
|
+
additional_module_paths=additional_module_paths,
|
|
316
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from fastmcp import FastMCP
|
|
19
|
+
from rich.align import Align
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.console import Group
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
from rich.text import Text
|
|
25
|
+
|
|
26
|
+
from datarobot_genai import __version__ as datarobot_genai_version
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Green color #81FBA5 = RGB(129, 251, 165)
|
|
30
|
+
def _apply_green(text: str) -> str:
|
|
31
|
+
"""Apply green color #81FBA5 to all characters in the text."""
|
|
32
|
+
# Apply ANSI escape code for RGB color #81FBA5 (129, 251, 165)
|
|
33
|
+
green_start = "\x1b[38;2;129;251;165m"
|
|
34
|
+
green_end = "\x1b[39m"
|
|
35
|
+
# Wrap the entire text (except trailing newline) with green color codes
|
|
36
|
+
lines = text.split("\n")
|
|
37
|
+
colored_lines = [green_start + line + green_end if line else "" for line in lines]
|
|
38
|
+
return "\n".join(colored_lines)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
DR_LOGO_ASCII = _apply_green(r"""
|
|
42
|
+
____ _ ____ _ _
|
|
43
|
+
| _ \ __ _| |_ __ _| _ \ ___ | |__ ___ | |_
|
|
44
|
+
| | | |/ _` | __/ _` | |_) / _ \| '_ \ / _ \| __|
|
|
45
|
+
| |_| | (_| | || (_| | _ < (_) | |_) | (_) | |_
|
|
46
|
+
|____/ \__,_|\__\__,_|_| \_\___/|_.__/ \___/ \__|
|
|
47
|
+
""")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def log_server_custom_banner(
|
|
51
|
+
server: FastMCP[Any],
|
|
52
|
+
transport: str,
|
|
53
|
+
*,
|
|
54
|
+
host: str | None = None,
|
|
55
|
+
port: int | None = None,
|
|
56
|
+
path: str | None = None,
|
|
57
|
+
tools_count: int | None = None,
|
|
58
|
+
prompts_count: int | None = None,
|
|
59
|
+
resources_count: int | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Create and log a formatted banner with server information and logo.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
transport: The transport protocol being used
|
|
66
|
+
server_name: Optional server name to display
|
|
67
|
+
host: Host address (for HTTP transports)
|
|
68
|
+
port: Port number (for HTTP transports)
|
|
69
|
+
path: Server path (for HTTP transports)
|
|
70
|
+
tools_count: Number of tools registered
|
|
71
|
+
prompts_count: Number of prompts registered
|
|
72
|
+
resources_count: Number of resources registered
|
|
73
|
+
"""
|
|
74
|
+
# Create the logo text
|
|
75
|
+
# Use Text with no_wrap and markup disabled to preserve ANSI escape codes
|
|
76
|
+
logo_text = Text.from_ansi(DR_LOGO_ASCII, no_wrap=True)
|
|
77
|
+
|
|
78
|
+
# Create the main title
|
|
79
|
+
title_text = Text(f"DataRobot MCP Server {datarobot_genai_version}", style="dim green")
|
|
80
|
+
stats_text = Text(
|
|
81
|
+
f"{tools_count} tools, {prompts_count} prompts, {resources_count} resources",
|
|
82
|
+
style="bold green",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Create the information table
|
|
86
|
+
info_table = Table.grid(padding=(0, 1))
|
|
87
|
+
info_table.add_column(style="bold", justify="center") # Emoji column
|
|
88
|
+
info_table.add_column(style="cyan", justify="left") # Label column
|
|
89
|
+
info_table.add_column(style="dim", justify="left") # Value column
|
|
90
|
+
|
|
91
|
+
match transport:
|
|
92
|
+
case "http" | "streamable-http":
|
|
93
|
+
display_transport = "HTTP"
|
|
94
|
+
case "sse":
|
|
95
|
+
display_transport = "SSE"
|
|
96
|
+
case "stdio":
|
|
97
|
+
display_transport = "STDIO"
|
|
98
|
+
|
|
99
|
+
info_table.add_row("🖥", "Server name:", Text(server.name + "\n", style="bold blue"))
|
|
100
|
+
info_table.add_row("📦", "Transport:", display_transport)
|
|
101
|
+
info_table.add_row("🌐", "MCP port:", str(port))
|
|
102
|
+
|
|
103
|
+
# Show connection info based on transport
|
|
104
|
+
if transport in ("http", "streamable-http", "sse") and host and port:
|
|
105
|
+
server_url = f"http://{host}:{port}"
|
|
106
|
+
if path:
|
|
107
|
+
server_url += f"/{path.lstrip('/')}"
|
|
108
|
+
info_table.add_row("🔗", "Server URL:", server_url)
|
|
109
|
+
|
|
110
|
+
# Add documentation link
|
|
111
|
+
info_table.add_row("", "", "")
|
|
112
|
+
info_table.add_row("📚", "Docs:", "https://docs.datarobot.com")
|
|
113
|
+
info_table.add_row("🚀", "Hosting:", "https://datarobot.com")
|
|
114
|
+
|
|
115
|
+
# Create panel with logo, title, and information using Group
|
|
116
|
+
panel_content = Group(
|
|
117
|
+
Align.center(logo_text),
|
|
118
|
+
"",
|
|
119
|
+
Align.center(title_text),
|
|
120
|
+
Align.center(stats_text),
|
|
121
|
+
"",
|
|
122
|
+
"",
|
|
123
|
+
Align.center(info_table),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
panel = Panel(
|
|
127
|
+
panel_content,
|
|
128
|
+
border_style="dim",
|
|
129
|
+
padding=(1, 4),
|
|
130
|
+
# expand=False,
|
|
131
|
+
width=80, # Set max width for the pane
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
console = Console(stderr=True)
|
|
135
|
+
# Center the panel itself
|
|
136
|
+
console.print(Group("\n", Align.center(panel), "\n"))
|
|
@@ -0,0 +1,13 @@
|
|
|
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.
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
import logging
|
|
16
|
+
|
|
17
|
+
from fastmcp.prompts.prompt import Prompt
|
|
18
|
+
|
|
19
|
+
from datarobot_genai.drmcp.core.dynamic_prompts.dr_lib import get_datarobot_prompt_template
|
|
20
|
+
from datarobot_genai.drmcp.core.dynamic_prompts.dr_lib import get_datarobot_prompt_template_version
|
|
21
|
+
from datarobot_genai.drmcp.core.dynamic_prompts.dr_lib import get_datarobot_prompt_template_versions
|
|
22
|
+
from datarobot_genai.drmcp.core.dynamic_prompts.dr_lib import get_datarobot_prompt_templates
|
|
23
|
+
from datarobot_genai.drmcp.core.dynamic_prompts.register import (
|
|
24
|
+
register_prompt_from_datarobot_prompt_management,
|
|
25
|
+
)
|
|
26
|
+
from datarobot_genai.drmcp.core.exceptions import DynamicPromptRegistrationError
|
|
27
|
+
from datarobot_genai.drmcp.core.mcp_instance import mcp
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def register_prompt_from_prompt_template_id_and_version(
|
|
33
|
+
prompt_template_id: str, prompt_template_version_id: str | None
|
|
34
|
+
) -> Prompt:
|
|
35
|
+
"""Register a Prompt for a specific prompt template ID and version.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
prompt_template_id: The ID of the DataRobot prompt template.
|
|
39
|
+
prompt_template_version_id: Optional ID of the DataRobot prompt template version.
|
|
40
|
+
If not provided latest will be used
|
|
41
|
+
|
|
42
|
+
Raises
|
|
43
|
+
------
|
|
44
|
+
DynamicPromptRegistrationError: If registration fails at any step.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
The registered Prompt instance.
|
|
49
|
+
"""
|
|
50
|
+
prompt_template = get_datarobot_prompt_template(prompt_template_id)
|
|
51
|
+
|
|
52
|
+
if not prompt_template:
|
|
53
|
+
raise DynamicPromptRegistrationError("Registration failed. Could not find prompt template.")
|
|
54
|
+
|
|
55
|
+
if not prompt_template_version_id:
|
|
56
|
+
return await register_prompt_from_datarobot_prompt_management(
|
|
57
|
+
prompt_template=prompt_template
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
prompt_template_version = get_datarobot_prompt_template_version(
|
|
61
|
+
prompt_template_id, prompt_template_version_id
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if not prompt_template_version:
|
|
65
|
+
raise DynamicPromptRegistrationError(
|
|
66
|
+
"Registration failed. Could not find prompt template version."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return await register_prompt_from_datarobot_prompt_management(
|
|
70
|
+
prompt_template=prompt_template, prompt_template_version=prompt_template_version
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def delete_registered_prompt_template(prompt_template_id: str) -> bool:
|
|
75
|
+
"""Delete the prompt registered for the prompt template id in the MCP instance."""
|
|
76
|
+
prompt_templates_mappings = await mcp.get_prompt_mapping()
|
|
77
|
+
if prompt_template_id not in prompt_templates_mappings:
|
|
78
|
+
logger.debug(f"No prompt registered for prompt template id {prompt_template_id}")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
prompt_template_version_id, prompt_name = prompt_templates_mappings[prompt_template_id]
|
|
82
|
+
await mcp.remove_prompt_mapping(prompt_template_id, prompt_template_version_id)
|
|
83
|
+
logger.info(
|
|
84
|
+
f"Deleted prompt name {prompt_name} for prompt template id {prompt_template_id}, "
|
|
85
|
+
f"version {prompt_template_version_id}"
|
|
86
|
+
)
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def refresh_registered_prompt_template() -> None:
|
|
91
|
+
"""Refresh all registered prompt templates in the MCP instance."""
|
|
92
|
+
prompt_templates = get_datarobot_prompt_templates()
|
|
93
|
+
prompt_templates_ids = {p.id for p in prompt_templates}
|
|
94
|
+
prompt_templates_versions = get_datarobot_prompt_template_versions(list(prompt_templates_ids))
|
|
95
|
+
|
|
96
|
+
mcp_prompt_templates_mappings = await mcp.get_prompt_mapping()
|
|
97
|
+
|
|
98
|
+
for prompt_template in prompt_templates:
|
|
99
|
+
prompt_template_versions = prompt_templates_versions.get(prompt_template.id)
|
|
100
|
+
if not prompt_template_versions:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
latest_version = max(prompt_template_versions, key=lambda v: v.version)
|
|
104
|
+
|
|
105
|
+
if prompt_template.id not in mcp_prompt_templates_mappings:
|
|
106
|
+
# New prompt template -> add
|
|
107
|
+
await register_prompt_from_datarobot_prompt_management(
|
|
108
|
+
prompt_template=prompt_template, prompt_template_version=latest_version
|
|
109
|
+
)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
mcp_prompt_template_version, mcp_prompt = mcp_prompt_templates_mappings[prompt_template.id]
|
|
113
|
+
|
|
114
|
+
if mcp_prompt_template_version != latest_version:
|
|
115
|
+
# Current version saved in MCP is not the latest one => update it
|
|
116
|
+
await register_prompt_from_datarobot_prompt_management(
|
|
117
|
+
prompt_template=prompt_template, prompt_template_version=latest_version
|
|
118
|
+
)
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# Else => mcp_prompt_template_version == latest_version
|
|
122
|
+
# For now it means nothing changed as there's no possibility to edit promp template version.
|
|
123
|
+
|
|
124
|
+
for mcp_prompt_template_id, (
|
|
125
|
+
mcp_prompt_template_version_id,
|
|
126
|
+
_,
|
|
127
|
+
) in mcp_prompt_templates_mappings.items():
|
|
128
|
+
if mcp_prompt_template_id not in prompt_templates_ids:
|
|
129
|
+
# We need to also delete prompt templates that are
|
|
130
|
+
await mcp.remove_prompt_mapping(mcp_prompt_template_id, mcp_prompt_template_version_id)
|