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.
Files changed (125) hide show
  1. datarobot_genai/__init__.py +19 -0
  2. datarobot_genai/core/__init__.py +0 -0
  3. datarobot_genai/core/agents/__init__.py +43 -0
  4. datarobot_genai/core/agents/base.py +195 -0
  5. datarobot_genai/core/chat/__init__.py +19 -0
  6. datarobot_genai/core/chat/auth.py +146 -0
  7. datarobot_genai/core/chat/client.py +178 -0
  8. datarobot_genai/core/chat/responses.py +297 -0
  9. datarobot_genai/core/cli/__init__.py +18 -0
  10. datarobot_genai/core/cli/agent_environment.py +47 -0
  11. datarobot_genai/core/cli/agent_kernel.py +211 -0
  12. datarobot_genai/core/custom_model.py +141 -0
  13. datarobot_genai/core/mcp/__init__.py +0 -0
  14. datarobot_genai/core/mcp/common.py +218 -0
  15. datarobot_genai/core/telemetry_agent.py +126 -0
  16. datarobot_genai/core/utils/__init__.py +3 -0
  17. datarobot_genai/core/utils/auth.py +234 -0
  18. datarobot_genai/core/utils/urls.py +64 -0
  19. datarobot_genai/crewai/__init__.py +24 -0
  20. datarobot_genai/crewai/agent.py +42 -0
  21. datarobot_genai/crewai/base.py +159 -0
  22. datarobot_genai/crewai/events.py +117 -0
  23. datarobot_genai/crewai/mcp.py +59 -0
  24. datarobot_genai/drmcp/__init__.py +78 -0
  25. datarobot_genai/drmcp/core/__init__.py +13 -0
  26. datarobot_genai/drmcp/core/auth.py +165 -0
  27. datarobot_genai/drmcp/core/clients.py +180 -0
  28. datarobot_genai/drmcp/core/config.py +364 -0
  29. datarobot_genai/drmcp/core/config_utils.py +174 -0
  30. datarobot_genai/drmcp/core/constants.py +18 -0
  31. datarobot_genai/drmcp/core/credentials.py +190 -0
  32. datarobot_genai/drmcp/core/dr_mcp_server.py +350 -0
  33. datarobot_genai/drmcp/core/dr_mcp_server_logo.py +136 -0
  34. datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +13 -0
  35. datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +130 -0
  36. datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +70 -0
  37. datarobot_genai/drmcp/core/dynamic_prompts/register.py +205 -0
  38. datarobot_genai/drmcp/core/dynamic_prompts/utils.py +33 -0
  39. datarobot_genai/drmcp/core/dynamic_tools/__init__.py +14 -0
  40. datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
  41. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +14 -0
  42. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +72 -0
  43. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +82 -0
  44. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +238 -0
  45. datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +228 -0
  46. datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +63 -0
  47. datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +162 -0
  48. datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +87 -0
  49. datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +36 -0
  50. datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +10 -0
  51. datarobot_genai/drmcp/core/dynamic_tools/register.py +254 -0
  52. datarobot_genai/drmcp/core/dynamic_tools/schema.py +532 -0
  53. datarobot_genai/drmcp/core/exceptions.py +25 -0
  54. datarobot_genai/drmcp/core/logging.py +98 -0
  55. datarobot_genai/drmcp/core/mcp_instance.py +515 -0
  56. datarobot_genai/drmcp/core/memory_management/__init__.py +13 -0
  57. datarobot_genai/drmcp/core/memory_management/manager.py +820 -0
  58. datarobot_genai/drmcp/core/memory_management/memory_tools.py +201 -0
  59. datarobot_genai/drmcp/core/routes.py +439 -0
  60. datarobot_genai/drmcp/core/routes_utils.py +30 -0
  61. datarobot_genai/drmcp/core/server_life_cycle.py +107 -0
  62. datarobot_genai/drmcp/core/telemetry.py +424 -0
  63. datarobot_genai/drmcp/core/tool_config.py +111 -0
  64. datarobot_genai/drmcp/core/tool_filter.py +117 -0
  65. datarobot_genai/drmcp/core/utils.py +138 -0
  66. datarobot_genai/drmcp/server.py +19 -0
  67. datarobot_genai/drmcp/test_utils/__init__.py +13 -0
  68. datarobot_genai/drmcp/test_utils/clients/__init__.py +0 -0
  69. datarobot_genai/drmcp/test_utils/clients/anthropic.py +68 -0
  70. datarobot_genai/drmcp/test_utils/clients/base.py +300 -0
  71. datarobot_genai/drmcp/test_utils/clients/dr_gateway.py +58 -0
  72. datarobot_genai/drmcp/test_utils/clients/openai.py +68 -0
  73. datarobot_genai/drmcp/test_utils/elicitation_test_tool.py +89 -0
  74. datarobot_genai/drmcp/test_utils/integration_mcp_server.py +109 -0
  75. datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +133 -0
  76. datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +107 -0
  77. datarobot_genai/drmcp/test_utils/test_interactive.py +205 -0
  78. datarobot_genai/drmcp/test_utils/tool_base_ete.py +220 -0
  79. datarobot_genai/drmcp/test_utils/utils.py +91 -0
  80. datarobot_genai/drmcp/tools/__init__.py +14 -0
  81. datarobot_genai/drmcp/tools/clients/__init__.py +14 -0
  82. datarobot_genai/drmcp/tools/clients/atlassian.py +188 -0
  83. datarobot_genai/drmcp/tools/clients/confluence.py +584 -0
  84. datarobot_genai/drmcp/tools/clients/gdrive.py +832 -0
  85. datarobot_genai/drmcp/tools/clients/jira.py +334 -0
  86. datarobot_genai/drmcp/tools/clients/microsoft_graph.py +479 -0
  87. datarobot_genai/drmcp/tools/clients/s3.py +28 -0
  88. datarobot_genai/drmcp/tools/confluence/__init__.py +14 -0
  89. datarobot_genai/drmcp/tools/confluence/tools.py +321 -0
  90. datarobot_genai/drmcp/tools/gdrive/__init__.py +0 -0
  91. datarobot_genai/drmcp/tools/gdrive/tools.py +347 -0
  92. datarobot_genai/drmcp/tools/jira/__init__.py +14 -0
  93. datarobot_genai/drmcp/tools/jira/tools.py +243 -0
  94. datarobot_genai/drmcp/tools/microsoft_graph/__init__.py +13 -0
  95. datarobot_genai/drmcp/tools/microsoft_graph/tools.py +198 -0
  96. datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
  97. datarobot_genai/drmcp/tools/predictive/data.py +133 -0
  98. datarobot_genai/drmcp/tools/predictive/deployment.py +91 -0
  99. datarobot_genai/drmcp/tools/predictive/deployment_info.py +392 -0
  100. datarobot_genai/drmcp/tools/predictive/model.py +148 -0
  101. datarobot_genai/drmcp/tools/predictive/predict.py +254 -0
  102. datarobot_genai/drmcp/tools/predictive/predict_realtime.py +307 -0
  103. datarobot_genai/drmcp/tools/predictive/project.py +90 -0
  104. datarobot_genai/drmcp/tools/predictive/training.py +661 -0
  105. datarobot_genai/langgraph/__init__.py +0 -0
  106. datarobot_genai/langgraph/agent.py +341 -0
  107. datarobot_genai/langgraph/mcp.py +73 -0
  108. datarobot_genai/llama_index/__init__.py +16 -0
  109. datarobot_genai/llama_index/agent.py +50 -0
  110. datarobot_genai/llama_index/base.py +299 -0
  111. datarobot_genai/llama_index/mcp.py +79 -0
  112. datarobot_genai/nat/__init__.py +0 -0
  113. datarobot_genai/nat/agent.py +275 -0
  114. datarobot_genai/nat/datarobot_auth_provider.py +110 -0
  115. datarobot_genai/nat/datarobot_llm_clients.py +318 -0
  116. datarobot_genai/nat/datarobot_llm_providers.py +130 -0
  117. datarobot_genai/nat/datarobot_mcp_client.py +266 -0
  118. datarobot_genai/nat/helpers.py +87 -0
  119. datarobot_genai/py.typed +0 -0
  120. datarobot_genai-0.2.31.dist-info/METADATA +145 -0
  121. datarobot_genai-0.2.31.dist-info/RECORD +125 -0
  122. datarobot_genai-0.2.31.dist-info/WHEEL +4 -0
  123. datarobot_genai-0.2.31.dist-info/entry_points.txt +5 -0
  124. datarobot_genai-0.2.31.dist-info/licenses/AUTHORS +2 -0
  125. 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)