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.
Files changed (101) 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 +250 -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 +316 -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 +128 -0
  37. datarobot_genai/drmcp/core/dynamic_prompts/register.py +206 -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 +542 -0
  56. datarobot_genai/drmcp/core/mcp_server_tools.py +129 -0
  57. datarobot_genai/drmcp/core/memory_management/__init__.py +13 -0
  58. datarobot_genai/drmcp/core/memory_management/manager.py +820 -0
  59. datarobot_genai/drmcp/core/memory_management/memory_tools.py +201 -0
  60. datarobot_genai/drmcp/core/routes.py +436 -0
  61. datarobot_genai/drmcp/core/routes_utils.py +30 -0
  62. datarobot_genai/drmcp/core/server_life_cycle.py +107 -0
  63. datarobot_genai/drmcp/core/telemetry.py +424 -0
  64. datarobot_genai/drmcp/core/tool_filter.py +108 -0
  65. datarobot_genai/drmcp/core/utils.py +131 -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/integration_mcp_server.py +102 -0
  69. datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +96 -0
  70. datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +94 -0
  71. datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py +234 -0
  72. datarobot_genai/drmcp/test_utils/tool_base_ete.py +151 -0
  73. datarobot_genai/drmcp/test_utils/utils.py +91 -0
  74. datarobot_genai/drmcp/tools/__init__.py +14 -0
  75. datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
  76. datarobot_genai/drmcp/tools/predictive/data.py +97 -0
  77. datarobot_genai/drmcp/tools/predictive/deployment.py +91 -0
  78. datarobot_genai/drmcp/tools/predictive/deployment_info.py +392 -0
  79. datarobot_genai/drmcp/tools/predictive/model.py +148 -0
  80. datarobot_genai/drmcp/tools/predictive/predict.py +254 -0
  81. datarobot_genai/drmcp/tools/predictive/predict_realtime.py +307 -0
  82. datarobot_genai/drmcp/tools/predictive/project.py +72 -0
  83. datarobot_genai/drmcp/tools/predictive/training.py +651 -0
  84. datarobot_genai/langgraph/__init__.py +0 -0
  85. datarobot_genai/langgraph/agent.py +341 -0
  86. datarobot_genai/langgraph/mcp.py +73 -0
  87. datarobot_genai/llama_index/__init__.py +16 -0
  88. datarobot_genai/llama_index/agent.py +50 -0
  89. datarobot_genai/llama_index/base.py +299 -0
  90. datarobot_genai/llama_index/mcp.py +79 -0
  91. datarobot_genai/nat/__init__.py +0 -0
  92. datarobot_genai/nat/agent.py +258 -0
  93. datarobot_genai/nat/datarobot_llm_clients.py +249 -0
  94. datarobot_genai/nat/datarobot_llm_providers.py +130 -0
  95. datarobot_genai/py.typed +0 -0
  96. datarobot_genai-0.2.0.dist-info/METADATA +139 -0
  97. datarobot_genai-0.2.0.dist-info/RECORD +101 -0
  98. datarobot_genai-0.2.0.dist-info/WHEEL +4 -0
  99. datarobot_genai-0.2.0.dist-info/entry_points.txt +3 -0
  100. datarobot_genai-0.2.0.dist-info/licenses/AUTHORS +2 -0
  101. datarobot_genai-0.2.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,87 @@
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 logging
15
+
16
+ import datarobot as dr
17
+ from fastmcp.tools.tool import Tool
18
+
19
+ from datarobot_genai.drmcp.core.clients import get_api_client
20
+ from datarobot_genai.drmcp.core.dynamic_tools.deployment.config import create_deployment_tool_config
21
+ from datarobot_genai.drmcp.core.dynamic_tools.register import register_external_tool
22
+ from datarobot_genai.drmcp.core.exceptions import DynamicToolRegistrationError
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ async def register_tools_of_datarobot_deployments() -> None:
28
+ """Register tools for all deployments tagged as 'tool' in DataRobot."""
29
+ deployment_ids = get_datarobot_tool_deployments()
30
+ logger.info(f"Found {len(deployment_ids)} tool deployments.")
31
+
32
+ # Try to register each tool deployment, continue on failure
33
+ for deployment_id in deployment_ids:
34
+ try:
35
+ deployment = dr.Deployment.get(deployment_id)
36
+ await register_tool_of_datarobot_deployment(deployment)
37
+ except DynamicToolRegistrationError:
38
+ pass
39
+ except Exception as exc:
40
+ logger.error(f"Unexpected error for deployment {deployment_id}: {exc}")
41
+ pass
42
+
43
+
44
+ async def register_tool_of_datarobot_deployment(
45
+ deployment: dr.Deployment,
46
+ ) -> Tool:
47
+ """Register a single tool for a given deployment.
48
+
49
+ Args:
50
+ deployment: The tool deployment within DataRobot.
51
+
52
+ Raises
53
+ ------
54
+ DynamicToolRegistrationError: If registration fails at any step.
55
+
56
+ Returns
57
+ -------
58
+ The registered Tool instance.
59
+ """
60
+ logger.info(f"Found tool: id: {deployment.id}, label: {deployment.label}")
61
+
62
+ try:
63
+ # Create the configuration object with all necessary information
64
+ # This includes fetching metadata and assembling all deployment-specific data
65
+ config = create_deployment_tool_config(deployment)
66
+
67
+ # Register using generic external tool registration with the config
68
+ tool = await register_external_tool(config, deployment_id=deployment.id)
69
+
70
+ return tool
71
+
72
+ except Exception as exc:
73
+ logger.error(f"Skipping deployment {deployment.id}. Registration failed: {exc}")
74
+ raise DynamicToolRegistrationError("Registration failed. Could not create tool.") from exc
75
+
76
+
77
+ def get_datarobot_tool_deployments() -> list[str]:
78
+ """Fetch deployments from DataRobot that are tagged as 'tool'."""
79
+ # Replace this with dr.Deployment.list when the 3.9.0 version
80
+ # of datarobot python SDK is released.
81
+ deployments_data = dr.utils.pagination.unpaginate(
82
+ initial_url="deployments/",
83
+ initial_params={"tag_values": "tool", "tag_keys": "tool"},
84
+ client=get_api_client(),
85
+ )
86
+
87
+ return [deployment["id"] for deployment in deployments_data]
@@ -0,0 +1,36 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "json": {
5
+ "type": "object",
6
+ "properties": {
7
+ "model": {
8
+ "type": "string",
9
+ "default": "unknown",
10
+ "description": "The LLM model to use. If no explicit instructions are provided leave default."
11
+ },
12
+ "messages": {
13
+ "type": "array",
14
+ "items": {
15
+ "type": "object",
16
+ "properties": {
17
+ "role": {
18
+ "type": "string",
19
+ "enum": ["system", "user"],
20
+ "description": "The role of the message author. 'system' for instructions, 'user' for user messages."
21
+ },
22
+ "content": {
23
+ "type": "string",
24
+ "description": "The content of the message"
25
+ }
26
+ },
27
+ "required": ["role", "content"]
28
+ }
29
+ }
30
+ },
31
+ "required": ["messages"],
32
+ "description": "Chat completions request body. When sending a request please focus on the user messages only. If there are no explicit instructions regarding `model` set it to `unknown`"
33
+ }
34
+ },
35
+ "required": ["json"]
36
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "data": {
5
+ "type": "string",
6
+ "description": "CSV-formatted data for prediction. Each row represents one prediction request. Column names must match the feature names from get_deployment_features tool."
7
+ }
8
+ },
9
+ "required": ["data"]
10
+ }
@@ -0,0 +1,254 @@
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 logging
17
+ from collections.abc import Callable
18
+ from collections.abc import Coroutine
19
+ from collections.abc import MutableMapping
20
+ from typing import Any
21
+ from typing import Literal
22
+ from urllib.parse import urljoin
23
+
24
+ import aiohttp
25
+ from aiohttp import ClientTimeout
26
+ from aiohttp_retry import ExponentialRetry
27
+ from aiohttp_retry import RetryClient
28
+ from fastmcp.server.dependencies import get_http_headers
29
+ from fastmcp.tools.tool import Tool
30
+ from fastmcp.tools.tool import ToolResult
31
+ from pydantic import BaseModel
32
+ from pydantic import Field
33
+ from requests.structures import CaseInsensitiveDict # type: ignore[import-untyped]
34
+
35
+ from datarobot_genai.drmcp.core.config import get_config
36
+ from datarobot_genai.drmcp.core.dynamic_tools.schema import create_input_schema_pydantic_model
37
+ from datarobot_genai.drmcp.core.mcp_instance import register_tools
38
+ from datarobot_genai.drmcp.core.utils import format_response_as_tool_result
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ # HTTP request retry configuration
44
+ REQUEST_RETRY_SLEEP = 0.1
45
+ REQUEST_MAX_RETRY = 5
46
+ REQUEST_RETRYABLE_STATUS_CODES = {429, 570, 502, 503, 504}
47
+
48
+ # HTTP connection timeouts in seconds
49
+ REQUEST_CONNECT_TIMEOUT = 30
50
+ REQUEST_TOTAL_TIMEOUT = 600
51
+
52
+ # Headers that should be forwarded from incoming requests.
53
+ # Keep this lower-cased for case-insensitive comparisons.
54
+ REQUEST_FORWARDED_HEADERS: set[str] = {
55
+ "x-agent-id",
56
+ "x-datarobot-authorization-context",
57
+ }
58
+
59
+
60
+ class ExternalToolRegistrationConfig(BaseModel):
61
+ """Configuration for registering an external HTTP API endpoint as an MCP tool.
62
+
63
+ This specification defines how to register a generic external HTTP API as a tool
64
+ that can be called by LLM agents through the MCP (Model Context Protocol) server.
65
+ The tool acts as a bridge between the LLM and any external HTTP API, automatically
66
+ handling request construction, retry logic, and response formatting.
67
+ """
68
+
69
+ name: str = Field(..., description="Name of the tool.")
70
+ title: str | None = Field(None, description="Title for LLMs and users.")
71
+ description: str | None = Field(None, description="Description for LLMs and users.")
72
+ method: Literal["GET", "POST", "PATCH", "PUT", "DELETE"] = Field(
73
+ ..., description="HTTP method to use."
74
+ )
75
+ base_url: str = Field(..., description="Base URL of the external API.")
76
+ endpoint: str = Field(
77
+ ...,
78
+ description="URL endpoint/route for the external API, may include path params.",
79
+ )
80
+ headers: dict[str, str] | None = Field(
81
+ None, description="Optional static headers to include in requests."
82
+ )
83
+ input_schema: dict[str, Any] = Field(
84
+ ..., description="Pydantic schema defining the tool's input schema."
85
+ )
86
+ tags: set[str] | None = Field(
87
+ None, description="Optional tags for tool categorization and filtering."
88
+ )
89
+
90
+
91
+ def _external_tool_callable_factory(
92
+ spec: ExternalToolRegistrationConfig,
93
+ ) -> Callable[[Any], Coroutine[Any, Any, ToolResult]]:
94
+ """Dynamically creates an async callable that makes HTTP requests
95
+ based on the given spec. This callable is the execution logic of the
96
+ tool making the external API call.
97
+
98
+ Args:
99
+ spec: Configuration specifying how to make the HTTP request.
100
+
101
+ Returns
102
+ -------
103
+ An async callable that takes validated inputs and returns a ToolResult.
104
+ """
105
+ config = get_config()
106
+
107
+ input_model = create_input_schema_pydantic_model(
108
+ input_schema=spec.input_schema,
109
+ allow_empty=config.tool_registration_allow_empty_schema,
110
+ )
111
+
112
+ async def call_external_tool(inputs: input_model) -> ToolResult: # type: ignore[valid-type]
113
+ request_input = inputs.model_dump() # type: ignore[attr-defined]
114
+
115
+ # Extract request parameters
116
+ path_params = request_input.get("path_params", {})
117
+ params = request_input.get("query_params")
118
+ data = request_input.get("data")
119
+ json = request_input.get("json")
120
+ headers = await get_outbound_headers(spec)
121
+
122
+ # Build full URL with path params
123
+ url = urljoin(spec.base_url, spec.endpoint.format(**path_params))
124
+
125
+ # Configure timeouts
126
+ client_timeout = ClientTimeout(
127
+ total=REQUEST_TOTAL_TIMEOUT,
128
+ connect=REQUEST_CONNECT_TIMEOUT,
129
+ )
130
+
131
+ # Configure retry strategy with exponential backoff
132
+ retry_options = ExponentialRetry(
133
+ attempts=REQUEST_MAX_RETRY,
134
+ start_timeout=REQUEST_RETRY_SLEEP,
135
+ statuses=REQUEST_RETRYABLE_STATUS_CODES,
136
+ exceptions={
137
+ aiohttp.ClientError,
138
+ aiohttp.ServerTimeoutError,
139
+ asyncio.TimeoutError,
140
+ },
141
+ )
142
+
143
+ # Execute request with retry logic
144
+ async with aiohttp.ClientSession(timeout=client_timeout) as session:
145
+ retry_client = RetryClient(
146
+ client_session=session,
147
+ retry_options=retry_options,
148
+ logger=logger,
149
+ )
150
+
151
+ async with retry_client.request(
152
+ method=spec.method.upper(),
153
+ url=url,
154
+ params=params,
155
+ data=data,
156
+ json=json,
157
+ headers=headers,
158
+ ) as response:
159
+ content = await response.read()
160
+
161
+ return format_response_as_tool_result(
162
+ data=content,
163
+ content_type=response.content_type,
164
+ charset=response.charset or "utf-8",
165
+ )
166
+
167
+ return call_external_tool
168
+
169
+
170
+ async def register_external_tool(config: ExternalToolRegistrationConfig, **kwargs: Any) -> Tool:
171
+ """Create and register a generic HTTP tool in the MCP server.
172
+
173
+ This function creates a dynamic tool that makes HTTP requests to external APIs
174
+ and registers it with the MCP server for use by LLM agents.
175
+
176
+ Args:
177
+ config: ExternalToolRegistrationConfig object containing all tool parameters.
178
+ **kwargs: Additional keyword arguments to pass to tools registration.
179
+
180
+ Returns
181
+ -------
182
+ The registered Tool instance with full MCP integration.
183
+
184
+ Raises
185
+ ------
186
+ ValueError: If required path parameters are missing from input_schema.
187
+ aiohttp.ClientError: If the HTTP request fails during tool execution.
188
+
189
+ Example:
190
+ >>> config = ExternalToolRegistrationConfig(
191
+ ... name="get_user",
192
+ ... description="Fetch user by ID",
193
+ ... base_url="https://api.example.com/v1",
194
+ ... endpoint="users/{user_id}",
195
+ ... method="GET",
196
+ ... input_schema={
197
+ ... "type": "object",
198
+ ... "properties": {
199
+ ... "path_params": {
200
+ ... "type": "object",
201
+ ... "properties": {"user_id": {"type": "string"}},
202
+ ... "required": ["user_id"]
203
+ ... }
204
+ ... }
205
+ ... },
206
+ ... tags={"example", "user"}
207
+ ... )
208
+ >>> tool = await register_external_tool(config=config)
209
+
210
+ Note:
211
+ The tool remains registered until explicitly removed or the server restarts.
212
+ """
213
+ external_tool_callable = _external_tool_callable_factory(config)
214
+
215
+ registered_tool = await register_tools(
216
+ fn=external_tool_callable,
217
+ name=config.name,
218
+ title=config.title,
219
+ description=config.description,
220
+ tags=config.tags,
221
+ **kwargs,
222
+ )
223
+
224
+ return registered_tool
225
+
226
+
227
+ async def get_outbound_headers(spec: ExternalToolRegistrationConfig) -> dict[str, str]:
228
+ """Retrieve headers to send to the external tool.
229
+
230
+ The method forwards whitelisted headers from current FastMCP
231
+ HTTP request, with tool specific headers and user overrides.
232
+ """
233
+ headers = get_http_headers()
234
+
235
+ # Headers from the incoming request to be forwarded (case-insensitive, but preserve
236
+ # original casing)
237
+ forwarded_headers: dict[str, str] = {
238
+ key: value for key, value in headers.items() if key.lower() in REQUEST_FORWARDED_HEADERS
239
+ }
240
+
241
+ # Tool-specific static headers with user-overrides
242
+ spec_headers = spec.headers or {}
243
+
244
+ # Use CaseInsensitiveDict for merging forwarded and spec_headers,
245
+ # to prevent duplicates differing only by case.
246
+ out_headers: MutableMapping[str, str] = CaseInsensitiveDict()
247
+
248
+ # Insert spec headers first so their casing is preserved if overridden.
249
+ out_headers.update(spec_headers)
250
+ for key, value in forwarded_headers.items():
251
+ if key not in out_headers:
252
+ out_headers[key] = value
253
+
254
+ return dict(out_headers)