datarobot-genai 0.2.31__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 +364 -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 +350 -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 +70 -0
- datarobot_genai/drmcp/core/dynamic_prompts/register.py +205 -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 +515 -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 +439 -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_config.py +111 -0
- datarobot_genai/drmcp/core/tool_filter.py +117 -0
- datarobot_genai/drmcp/core/utils.py +138 -0
- datarobot_genai/drmcp/server.py +19 -0
- datarobot_genai/drmcp/test_utils/__init__.py +13 -0
- datarobot_genai/drmcp/test_utils/clients/__init__.py +0 -0
- datarobot_genai/drmcp/test_utils/clients/anthropic.py +68 -0
- datarobot_genai/drmcp/test_utils/clients/base.py +300 -0
- datarobot_genai/drmcp/test_utils/clients/dr_gateway.py +58 -0
- datarobot_genai/drmcp/test_utils/clients/openai.py +68 -0
- datarobot_genai/drmcp/test_utils/elicitation_test_tool.py +89 -0
- datarobot_genai/drmcp/test_utils/integration_mcp_server.py +109 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +133 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +107 -0
- datarobot_genai/drmcp/test_utils/test_interactive.py +205 -0
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +220 -0
- datarobot_genai/drmcp/test_utils/utils.py +91 -0
- datarobot_genai/drmcp/tools/__init__.py +14 -0
- datarobot_genai/drmcp/tools/clients/__init__.py +14 -0
- datarobot_genai/drmcp/tools/clients/atlassian.py +188 -0
- datarobot_genai/drmcp/tools/clients/confluence.py +584 -0
- datarobot_genai/drmcp/tools/clients/gdrive.py +832 -0
- datarobot_genai/drmcp/tools/clients/jira.py +334 -0
- datarobot_genai/drmcp/tools/clients/microsoft_graph.py +479 -0
- datarobot_genai/drmcp/tools/clients/s3.py +28 -0
- datarobot_genai/drmcp/tools/confluence/__init__.py +14 -0
- datarobot_genai/drmcp/tools/confluence/tools.py +321 -0
- datarobot_genai/drmcp/tools/gdrive/__init__.py +0 -0
- datarobot_genai/drmcp/tools/gdrive/tools.py +347 -0
- datarobot_genai/drmcp/tools/jira/__init__.py +14 -0
- datarobot_genai/drmcp/tools/jira/tools.py +243 -0
- datarobot_genai/drmcp/tools/microsoft_graph/__init__.py +13 -0
- datarobot_genai/drmcp/tools/microsoft_graph/tools.py +198 -0
- datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
- datarobot_genai/drmcp/tools/predictive/data.py +133 -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 +90 -0
- datarobot_genai/drmcp/tools/predictive/training.py +661 -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 +275 -0
- datarobot_genai/nat/datarobot_auth_provider.py +110 -0
- datarobot_genai/nat/datarobot_llm_clients.py +318 -0
- datarobot_genai/nat/datarobot_llm_providers.py +130 -0
- datarobot_genai/nat/datarobot_mcp_client.py +266 -0
- datarobot_genai/nat/helpers.py +87 -0
- datarobot_genai/py.typed +0 -0
- datarobot_genai-0.2.31.dist-info/METADATA +145 -0
- datarobot_genai-0.2.31.dist-info/RECORD +125 -0
- datarobot_genai-0.2.31.dist-info/WHEEL +4 -0
- datarobot_genai-0.2.31.dist-info/entry_points.txt +5 -0
- datarobot_genai-0.2.31.dist-info/licenses/AUTHORS +2 -0
- datarobot_genai-0.2.31.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,238 @@
|
|
|
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
|
+
import json
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
from typing import Literal
|
|
20
|
+
|
|
21
|
+
from .base import MetadataBase
|
|
22
|
+
|
|
23
|
+
# Path to the schemas directory
|
|
24
|
+
SCHEMAS_DIR = Path(__file__).parent.parent / "schemas"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DrumTargetType(str, Enum):
|
|
28
|
+
BINARY = "binary"
|
|
29
|
+
REGRESSION = "regression"
|
|
30
|
+
ANOMALY = "anomaly"
|
|
31
|
+
UNSTRUCTURED = "unstructured"
|
|
32
|
+
MULTICLASS = "multiclass"
|
|
33
|
+
TEXT_GENERATION = "textgeneration"
|
|
34
|
+
GEO_POINT = "geopoint"
|
|
35
|
+
VECTOR_DATABASE = "vectordatabase"
|
|
36
|
+
AGENTIC_WORKFLOW = "agenticworkflow"
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def prediction_types(cls) -> set["DrumTargetType"]:
|
|
40
|
+
"""Get the set of DRUM target types that correspond to structured predictions."""
|
|
41
|
+
return {
|
|
42
|
+
DrumTargetType.BINARY,
|
|
43
|
+
DrumTargetType.REGRESSION,
|
|
44
|
+
DrumTargetType.ANOMALY,
|
|
45
|
+
DrumTargetType.MULTICLASS,
|
|
46
|
+
DrumTargetType.TEXT_GENERATION,
|
|
47
|
+
DrumTargetType.GEO_POINT,
|
|
48
|
+
DrumTargetType.VECTOR_DATABASE,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@lru_cache(maxsize=1)
|
|
53
|
+
def _get_prediction_fallback_schema() -> dict[str, Any]:
|
|
54
|
+
"""Get the default prediction input schema for DRUM deployments."""
|
|
55
|
+
schema_path = SCHEMAS_DIR / "drum_prediction_fallback_schema.json"
|
|
56
|
+
with open(schema_path) as f:
|
|
57
|
+
schema: dict[str, Any] = json.load(f)
|
|
58
|
+
return schema
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@lru_cache(maxsize=1)
|
|
62
|
+
def _get_agentic_fallback_schema() -> dict[str, Any]:
|
|
63
|
+
"""Get the default agentic workflow input schema for DRUM deployments."""
|
|
64
|
+
schema_path = SCHEMAS_DIR / "drum_agentic_fallback_schema.json"
|
|
65
|
+
with open(schema_path) as f:
|
|
66
|
+
schema: dict[str, Any] = json.load(f)
|
|
67
|
+
return schema
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_default_schema(target_type: str) -> dict[str, Any]:
|
|
71
|
+
"""Get the default input schema for a given DRUM target type, when
|
|
72
|
+
the deployment does not provide one. This fallback mechanism is here
|
|
73
|
+
to lower the friction of using DRUM deployments with MCP, for more
|
|
74
|
+
advanced use cases it is recommended to provide a custom input and
|
|
75
|
+
expose it via model-metadata.yaml inputSchema parameter.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
target_type: The target type of the DRUM deployment.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
A dictionary representing the default input schema wrapped in HTTP request structure.
|
|
83
|
+
"""
|
|
84
|
+
if target_type == DrumTargetType.AGENTIC_WORKFLOW:
|
|
85
|
+
return _get_agentic_fallback_schema()
|
|
86
|
+
|
|
87
|
+
if target_type in DrumTargetType.prediction_types():
|
|
88
|
+
return _get_prediction_fallback_schema()
|
|
89
|
+
|
|
90
|
+
return {}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_drum(metadata: dict[str, Any]) -> bool:
|
|
94
|
+
"""Check if the deployment is a DRUM deployment.
|
|
95
|
+
|
|
96
|
+
DRUM deployments are identified by the presence of both drum_server
|
|
97
|
+
and drum_version fields in the metadata response.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
metadata: The response retrieved from the custom model /info/ route.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
True if this is a DRUM deployment, False otherwise.
|
|
105
|
+
"""
|
|
106
|
+
drum_server = metadata.get("drum_server")
|
|
107
|
+
drum_version = metadata.get("drum_version")
|
|
108
|
+
return bool(drum_server or drum_version)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class DrumMetadataAdapter(MetadataBase):
|
|
112
|
+
"""Adapter for DRUM deployment metadata."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, metadata: dict[str, Any]):
|
|
115
|
+
"""Initialize adapter with validated metadata.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
metadata: Dictionary containing at minimum a 'target_type' key.
|
|
119
|
+
|
|
120
|
+
Note:
|
|
121
|
+
Use class methods `from_deployment_metadata()` or `from_target_type()`
|
|
122
|
+
for construction instead of calling this directly.
|
|
123
|
+
"""
|
|
124
|
+
self.metadata = metadata
|
|
125
|
+
self._validate_tool_support()
|
|
126
|
+
|
|
127
|
+
def _validate_tool_support(self) -> None:
|
|
128
|
+
"""Validate that DRUM deployments are supported in the current environment.
|
|
129
|
+
|
|
130
|
+
Raises
|
|
131
|
+
------
|
|
132
|
+
ValueError: If DRUM deployments are not supported.
|
|
133
|
+
"""
|
|
134
|
+
if self.target_type not in list(DrumTargetType):
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"The deployment target_type: {self.target_type} "
|
|
137
|
+
f"is not supported, to be registered as MCP Tool."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def from_deployment_metadata(cls, metadata: dict[str, Any]) -> "DrumMetadataAdapter":
|
|
142
|
+
"""Create adapter from full deployment metadata.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
metadata: The response retrieved from the custom model /info/ route.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
DrumMetadataAdapter instance.
|
|
150
|
+
|
|
151
|
+
Raises
|
|
152
|
+
------
|
|
153
|
+
ValueError: If metadata is not from a DRUM deployment.
|
|
154
|
+
"""
|
|
155
|
+
if not is_drum(metadata):
|
|
156
|
+
raise ValueError("Provided metadata is not from a DRUM deployment.")
|
|
157
|
+
return cls(metadata)
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_target_type(cls, target_type: str) -> "DrumMetadataAdapter":
|
|
161
|
+
"""Create adapter from target type only.
|
|
162
|
+
|
|
163
|
+
Used for testing/minimal setup when broader set of information
|
|
164
|
+
from metadata built from model-metadata.yaml information is
|
|
165
|
+
not available i.e. datarobot predictive models.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
target_type: The DRUM target type (e.g., 'binary', 'regression').
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
DrumMetadataAdapter instance with minimal metadata.
|
|
173
|
+
"""
|
|
174
|
+
return cls({"target_type": target_type.lower()})
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def target_type(self) -> str:
|
|
178
|
+
return str(self.metadata["target_type"])
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def name(self) -> str:
|
|
182
|
+
return str(self.model_metadata.get("name", ""))
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def description(self) -> str:
|
|
186
|
+
return str(self.model_metadata.get("description", ""))
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def endpoint(self) -> str:
|
|
190
|
+
"""Return the appropriate endpoint for the DRUM target type."""
|
|
191
|
+
predictions_endpoint = "/predictions"
|
|
192
|
+
|
|
193
|
+
endpoint_map: dict[str, str] = {
|
|
194
|
+
DrumTargetType.BINARY: predictions_endpoint,
|
|
195
|
+
DrumTargetType.REGRESSION: predictions_endpoint,
|
|
196
|
+
DrumTargetType.ANOMALY: predictions_endpoint,
|
|
197
|
+
DrumTargetType.MULTICLASS: predictions_endpoint,
|
|
198
|
+
DrumTargetType.TEXT_GENERATION: predictions_endpoint,
|
|
199
|
+
DrumTargetType.GEO_POINT: predictions_endpoint,
|
|
200
|
+
DrumTargetType.UNSTRUCTURED: "/predictionsUnstructured",
|
|
201
|
+
DrumTargetType.VECTOR_DATABASE: predictions_endpoint,
|
|
202
|
+
DrumTargetType.AGENTIC_WORKFLOW: "/chat/completions",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return endpoint_map[self.target_type]
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def model_metadata(self) -> dict[str, Any]:
|
|
209
|
+
result = self.metadata.get("model_metadata", {})
|
|
210
|
+
return dict(result)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def input_schema(self) -> dict[str, Any]:
|
|
214
|
+
input_schema = self.model_metadata.get("input_schema", get_default_schema(self.target_type))
|
|
215
|
+
|
|
216
|
+
if not input_schema or not isinstance(input_schema, dict):
|
|
217
|
+
raise ValueError(
|
|
218
|
+
"DRUM deployment is missing a valid input schema. Please make "
|
|
219
|
+
"sure the model-metadata.yaml file includes `inputSchema` "
|
|
220
|
+
"definition and that custom model is using datarobot-drum in "
|
|
221
|
+
"version v1.17.2 or later."
|
|
222
|
+
)
|
|
223
|
+
return dict(input_schema)
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def method(self) -> Literal["GET", "POST", "PATCH", "PUT", "DELETE"]:
|
|
227
|
+
return "POST"
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def headers(self) -> dict[str, str]:
|
|
231
|
+
"""Return HTTP headers required for this deployment type."""
|
|
232
|
+
if self.target_type in DrumTargetType.prediction_types():
|
|
233
|
+
# structured predictions send data as CSV bytes, which
|
|
234
|
+
# requires an explicit Content-Type header since aiohttp
|
|
235
|
+
# won't set it automatically
|
|
236
|
+
return {"Content-Type": "text/csv"}
|
|
237
|
+
|
|
238
|
+
return {}
|
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
"""Configuration assembly for DataRobot deployment tools.
|
|
16
|
+
|
|
17
|
+
This module is responsible for creating complete ExternalToolRegistrationConfig
|
|
18
|
+
objects from DataRobot deployments. It handles all aspects of configuration
|
|
19
|
+
including metadata fetching, URL construction, authentication, and schema assembly.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from urllib.parse import urljoin
|
|
24
|
+
|
|
25
|
+
import datarobot as dr
|
|
26
|
+
|
|
27
|
+
from datarobot_genai.drmcp.core.clients import get_api_client
|
|
28
|
+
from datarobot_genai.drmcp.core.dynamic_tools.deployment.adapters.base import MetadataBase
|
|
29
|
+
from datarobot_genai.drmcp.core.dynamic_tools.deployment.metadata import get_mcp_tool_metadata
|
|
30
|
+
from datarobot_genai.drmcp.core.dynamic_tools.register import ExternalToolRegistrationConfig
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_deployment_tool_config(
|
|
34
|
+
deployment: dr.Deployment,
|
|
35
|
+
) -> ExternalToolRegistrationConfig:
|
|
36
|
+
"""Create an ExternalToolRegistrationConfig from deployment.
|
|
37
|
+
|
|
38
|
+
This is the main public API of this module. It gathers all the information
|
|
39
|
+
needed to register a DataRobot deployment as an external tool, handling both
|
|
40
|
+
DRUM and standard deployments. It fetches metadata, extracts deployment-specific
|
|
41
|
+
configuration, and assembles everything into a complete registration config.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
deployment: The DataRobot deployment object.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
ExternalToolRegistrationConfig with all parameters needed for registration.
|
|
49
|
+
|
|
50
|
+
Raises
|
|
51
|
+
------
|
|
52
|
+
Exception: If metadata fetching or config assembly fails.
|
|
53
|
+
"""
|
|
54
|
+
# Fetch and validate metadata from the deployment
|
|
55
|
+
metadata = get_mcp_tool_metadata(deployment)
|
|
56
|
+
|
|
57
|
+
# Get deployment-specific infrastructure configuration
|
|
58
|
+
base_url = _get_deployment_base_url(deployment)
|
|
59
|
+
auth_headers = _get_deployment_auth_headers(deployment)
|
|
60
|
+
|
|
61
|
+
# Merge metadata headers with deployment headers
|
|
62
|
+
merged_headers = {**auth_headers, **metadata.headers}
|
|
63
|
+
|
|
64
|
+
# Build endpoint path
|
|
65
|
+
endpoint = metadata.endpoint.lstrip("/")
|
|
66
|
+
|
|
67
|
+
# Generate tool name and description
|
|
68
|
+
tool_name = _get_tool_name(deployment, metadata)
|
|
69
|
+
tool_description = _get_tool_description(deployment, metadata)
|
|
70
|
+
|
|
71
|
+
return ExternalToolRegistrationConfig(
|
|
72
|
+
name=tool_name,
|
|
73
|
+
title=deployment.label,
|
|
74
|
+
description=tool_description,
|
|
75
|
+
method=metadata.method,
|
|
76
|
+
base_url=base_url,
|
|
77
|
+
endpoint=endpoint,
|
|
78
|
+
headers=merged_headers,
|
|
79
|
+
input_schema=metadata.input_schema,
|
|
80
|
+
tags=set(), # Add missing tags parameter
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _is_serverless_deployment(deployment: dr.Deployment) -> bool:
|
|
85
|
+
"""Check if deployment is serverless."""
|
|
86
|
+
if not deployment.prediction_environment:
|
|
87
|
+
return False
|
|
88
|
+
return deployment.prediction_environment.get("platform") == "datarobotServerless"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _get_deployment_base_url(deployment: dr.Deployment) -> str:
|
|
92
|
+
"""Get base URL for deployment prediction server.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
deployment: DataRobot deployment instance
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
Formatted deployment URL including deployment ID path
|
|
100
|
+
|
|
101
|
+
Raises
|
|
102
|
+
------
|
|
103
|
+
ValueError: If prediction server cannot be determined
|
|
104
|
+
"""
|
|
105
|
+
api_client = get_api_client()
|
|
106
|
+
|
|
107
|
+
# Determine base URL based on deployment type
|
|
108
|
+
if _is_serverless_deployment(deployment):
|
|
109
|
+
base_url = api_client.endpoint
|
|
110
|
+
elif "datarobot-nginx" in api_client.endpoint:
|
|
111
|
+
# On-prem/ST SAAS environments
|
|
112
|
+
base_url = "http://datarobot-prediction-server:80/predApi/v1.0"
|
|
113
|
+
else:
|
|
114
|
+
# Regular prediction server
|
|
115
|
+
pred_server = deployment.default_prediction_server
|
|
116
|
+
if not pred_server:
|
|
117
|
+
raise ValueError(f"Deployment {deployment.id} has no default prediction server")
|
|
118
|
+
|
|
119
|
+
url = pred_server["url"]
|
|
120
|
+
if not url:
|
|
121
|
+
raise ValueError(f"Deployment {deployment.id} prediction server has no URL")
|
|
122
|
+
base_url = f"{url}/predApi/v1.0"
|
|
123
|
+
|
|
124
|
+
merged_url = urljoin(base_url.rstrip("/") + "/", f"deployments/{deployment.id}/")
|
|
125
|
+
return merged_url
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _get_deployment_auth_headers(deployment: dr.Deployment) -> dict[str, str]:
|
|
129
|
+
"""Get authentication headers for deployment.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
deployment: DataRobot deployment instance
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
-------
|
|
136
|
+
Dictionary of authentication headers
|
|
137
|
+
"""
|
|
138
|
+
headers = {"Authorization": f"Bearer {get_api_client().token}"}
|
|
139
|
+
|
|
140
|
+
# For non-serverless deployments, include datarobot-key
|
|
141
|
+
if not _is_serverless_deployment(deployment):
|
|
142
|
+
pred_server = deployment.default_prediction_server
|
|
143
|
+
if pred_server:
|
|
144
|
+
dr_key = pred_server.get("datarobot-key")
|
|
145
|
+
if dr_key:
|
|
146
|
+
headers["datarobot-key"] = dr_key
|
|
147
|
+
|
|
148
|
+
return headers
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _get_tool_name(deployment: dr.Deployment, metadata: MetadataBase) -> str:
|
|
152
|
+
"""Generate tool name from deployment and metadata."""
|
|
153
|
+
tool_name = deployment.label or metadata.name or f"deployment_{deployment.id}"
|
|
154
|
+
return _convert_tool_string(tool_name)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _get_additional_prediction_instructions(deployment_id: str) -> str:
|
|
158
|
+
"""Generate additional instructions for scoring prediction models, to make tool usage more
|
|
159
|
+
reliable.
|
|
160
|
+
"""
|
|
161
|
+
return f"""
|
|
162
|
+
|
|
163
|
+
Follow these steps in order:
|
|
164
|
+
1. Get deployment info: Call tools with the deployment_id="{deployment_id}" to learn about
|
|
165
|
+
features and requirements.
|
|
166
|
+
2. Retrieve features: Use `get_deployment_features` to see all required and optional features
|
|
167
|
+
with their importance scores.
|
|
168
|
+
3. Prepare data: Use `generate_prediction_data_template` to create the correctly structured
|
|
169
|
+
CSV format.
|
|
170
|
+
4. Consider feature importance: For high-importance features, always provide values (infer or
|
|
171
|
+
ask). Low-importance features can be left blank.
|
|
172
|
+
5. Validate: Run `validate_prediction_data` before submission to catch errors early.
|
|
173
|
+
6. Time series note: Ensure `datetime_column` and `series_id_columns` are properly formatted
|
|
174
|
+
if applicable.
|
|
175
|
+
|
|
176
|
+
Parameter details and format requirements are specified in the input schema below."""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _get_tool_description(deployment: dr.Deployment, metadata: MetadataBase) -> str:
|
|
180
|
+
"""Generate tool description from deployment and metadata.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
deployment: The DataRobot deployment object.
|
|
184
|
+
metadata: The metadata adapter containing tool information.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
Complete tool description, optionally enhanced with workflow instructions
|
|
189
|
+
for prediction endpoints.
|
|
190
|
+
"""
|
|
191
|
+
base_description = deployment.description or metadata.description
|
|
192
|
+
|
|
193
|
+
if metadata.endpoint.endswith("predictions"):
|
|
194
|
+
additional_instructions = _get_additional_prediction_instructions(deployment.id)
|
|
195
|
+
return f"{base_description}{additional_instructions}"
|
|
196
|
+
|
|
197
|
+
return base_description
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _convert_tool_string(text: str | None) -> str:
|
|
201
|
+
"""Convert a string to a valid tool name format.
|
|
202
|
+
|
|
203
|
+
Removes brackets, replaces spaces/hyphens with underscores, removes special
|
|
204
|
+
characters, converts to lowercase, and cleans up multiple underscores.
|
|
205
|
+
"""
|
|
206
|
+
if not text:
|
|
207
|
+
return ""
|
|
208
|
+
|
|
209
|
+
# Remove anything within brackets (including the brackets)
|
|
210
|
+
text = re.sub(r"\[.*?\]", "", text)
|
|
211
|
+
|
|
212
|
+
# Replace spaces with underscores
|
|
213
|
+
text = text.replace(" ", "_")
|
|
214
|
+
text = text.replace("-", "_")
|
|
215
|
+
|
|
216
|
+
# Remove all non-alphanumeric characters except underscores
|
|
217
|
+
text = re.sub(r"[^a-zA-Z0-9_]", "", text)
|
|
218
|
+
|
|
219
|
+
# Convert to lowercase
|
|
220
|
+
text = text.lower()
|
|
221
|
+
|
|
222
|
+
# Clean up any multiple underscores that might result
|
|
223
|
+
text = re.sub(r"_+", "_", text)
|
|
224
|
+
|
|
225
|
+
# Remove leading/trailing underscores
|
|
226
|
+
text = text.strip("_")
|
|
227
|
+
|
|
228
|
+
return text
|
|
@@ -0,0 +1,63 @@
|
|
|
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.tools.tool import Tool
|
|
18
|
+
|
|
19
|
+
from datarobot_genai.drmcp.core.clients import get_sdk_client
|
|
20
|
+
from datarobot_genai.drmcp.core.dynamic_tools.deployment.register import (
|
|
21
|
+
register_tool_of_datarobot_deployment,
|
|
22
|
+
)
|
|
23
|
+
from datarobot_genai.drmcp.core.mcp_instance import mcp
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def register_tool_for_deployment_id(deployment_id: str) -> Tool:
|
|
29
|
+
"""Register a tool for a specific deployment ID.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
deployment_id: The ID of the DataRobot deployment to register as a tool.
|
|
33
|
+
|
|
34
|
+
Raises
|
|
35
|
+
------
|
|
36
|
+
DynamicToolRegistrationError: If registration fails at any step.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
The registered Tool instance.
|
|
41
|
+
"""
|
|
42
|
+
deployment = get_sdk_client().Deployment.get(deployment_id)
|
|
43
|
+
registered_tool = await register_tool_of_datarobot_deployment(deployment)
|
|
44
|
+
return registered_tool
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def get_registered_tool_deployments() -> dict[str, str]:
|
|
48
|
+
"""Get the tool registered for the deployment in the MCP instance."""
|
|
49
|
+
deployments = await mcp.get_deployment_mapping()
|
|
50
|
+
return deployments
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def delete_registered_tool_deployment(deployment_id: str) -> bool:
|
|
54
|
+
"""Delete the tool registered for the deployment in the MCP instance."""
|
|
55
|
+
deployments = await mcp.get_deployment_mapping()
|
|
56
|
+
if deployment_id not in deployments:
|
|
57
|
+
logger.debug(f"No tool registered for deployment {deployment_id}")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
tool_name = deployments[deployment_id]
|
|
61
|
+
await mcp.remove_deployment_mapping(deployment_id)
|
|
62
|
+
logger.info(f"Deleted tool {tool_name} for deployment {deployment_id}")
|
|
63
|
+
return True
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import datarobot as dr
|
|
19
|
+
from datarobot.utils import from_api
|
|
20
|
+
|
|
21
|
+
from datarobot_genai.drmcp.core.clients import get_api_client
|
|
22
|
+
from datarobot_genai.drmcp.core.dynamic_tools.deployment.adapters.base import MetadataBase
|
|
23
|
+
from datarobot_genai.drmcp.core.dynamic_tools.deployment.adapters.default import Metadata
|
|
24
|
+
from datarobot_genai.drmcp.core.dynamic_tools.deployment.adapters.drum import DrumMetadataAdapter
|
|
25
|
+
from datarobot_genai.drmcp.core.dynamic_tools.deployment.adapters.drum import DrumTargetType
|
|
26
|
+
from datarobot_genai.drmcp.core.dynamic_tools.deployment.adapters.drum import is_drum
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_api_response(response: Any) -> dict[str, Any]:
|
|
32
|
+
"""Normalize API response to a dictionary.
|
|
33
|
+
|
|
34
|
+
The API can return either a list or a dict. This function ensures
|
|
35
|
+
we always get a dict, taking the first element if it's a list.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
response: The raw API response object.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
Normalized dictionary representation of the response.
|
|
43
|
+
"""
|
|
44
|
+
data = from_api(response.json())
|
|
45
|
+
if isinstance(data, list):
|
|
46
|
+
return data[0] if data else {}
|
|
47
|
+
if isinstance(data, dict):
|
|
48
|
+
return data
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get_model_attribute(model: dict[str, Any], key: str, default: str = "") -> str:
|
|
53
|
+
"""Safely extract a string attribute from a model dictionary.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
model: The deployment model dictionary.
|
|
57
|
+
key: The attribute key to retrieve.
|
|
58
|
+
default: Default value if key is not found.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
The attribute value as a lowercase string.
|
|
63
|
+
"""
|
|
64
|
+
value = model.get(key, default)
|
|
65
|
+
return str(value).lower() if value else default
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_datarobot_structured_prediction(deployment: dr.Deployment) -> str | None:
|
|
69
|
+
"""Check if deployment is a DataRobot structured prediction model.
|
|
70
|
+
|
|
71
|
+
DataRobot native predictive models with structured predictions don't support
|
|
72
|
+
metadata fetching via API, so they need special handling.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
deployment: The DataRobot deployment object.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
Target type string if it's a DataRobot structured prediction, None otherwise.
|
|
80
|
+
"""
|
|
81
|
+
if deployment.model is None:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
# deployment.model is a TypedDict at type-check time, but dict at runtime
|
|
85
|
+
# Cast it to Dict[str, Any] to allow dynamic key access
|
|
86
|
+
model_dict: dict[str, Any] = deployment.model # type: ignore[assignment]
|
|
87
|
+
target_type = _get_model_attribute(model_dict, "target_type")
|
|
88
|
+
build_env = _get_model_attribute(model_dict, "build_environment_type")
|
|
89
|
+
|
|
90
|
+
if not target_type or not build_env:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
if build_env == "datarobot" and target_type in DrumTargetType.prediction_types():
|
|
94
|
+
return target_type
|
|
95
|
+
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _fetch_deployment_metadata(deployment: dr.Deployment) -> dict[str, Any]:
|
|
100
|
+
"""Fetch metadata from deployment's directAccess/info endpoint.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
deployment: The DataRobot deployment object.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
Normalized metadata dictionary.
|
|
108
|
+
|
|
109
|
+
Raises
|
|
110
|
+
------
|
|
111
|
+
RuntimeError: If API call fails.
|
|
112
|
+
"""
|
|
113
|
+
api_client = get_api_client()
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
response = api_client.get(url=f"deployments/{deployment.id}/directAccess/info/")
|
|
117
|
+
response.raise_for_status()
|
|
118
|
+
return _normalize_api_response(response)
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
logger.error(f"Failed to fetch metadata for deployment {deployment.id}: {exc}")
|
|
121
|
+
raise RuntimeError(f"Could not retrieve metadata for deployment {deployment.id}") from exc
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_mcp_tool_metadata(deployment: dr.Deployment) -> MetadataBase:
|
|
125
|
+
"""Fetch and validate metadata for a given deployment.
|
|
126
|
+
|
|
127
|
+
This method retrieves deployment metadata from the /directAccess/info/
|
|
128
|
+
endpoint and validates it contains the required fields for tool registration.
|
|
129
|
+
It uses a universal approach where DRUM deployments are treated as a
|
|
130
|
+
special case and converted to a standard format.
|
|
131
|
+
|
|
132
|
+
The returned metadata must contain at minimum:
|
|
133
|
+
- input_schema: JSON schema describing the tool's input parameters
|
|
134
|
+
- endpoint: The deployment endpoint path to call
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
deployment: The DataRobot deployment object.
|
|
138
|
+
|
|
139
|
+
Raises
|
|
140
|
+
------
|
|
141
|
+
RuntimeError: If metadata fetching fails.
|
|
142
|
+
ValueError: If the deployment metadata is missing or invalid.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
MetadataBase adapter instance to expose required
|
|
147
|
+
metadata properties for tool registration in a standard way.
|
|
148
|
+
"""
|
|
149
|
+
# Check if this is a DataRobot native structured prediction model
|
|
150
|
+
# These don't support metadata API, so we create minimal metadata
|
|
151
|
+
target_type = _is_datarobot_structured_prediction(deployment)
|
|
152
|
+
if target_type:
|
|
153
|
+
return DrumMetadataAdapter.from_target_type(target_type)
|
|
154
|
+
|
|
155
|
+
# Fetch metadata from the deployment's info endpoint
|
|
156
|
+
metadata = _fetch_deployment_metadata(deployment)
|
|
157
|
+
|
|
158
|
+
# Return appropriate adapter based on metadata type
|
|
159
|
+
if is_drum(metadata):
|
|
160
|
+
return DrumMetadataAdapter.from_deployment_metadata(metadata)
|
|
161
|
+
|
|
162
|
+
return Metadata(metadata)
|