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,59 @@
1
+ # Copyright 2025 DataRobot, Inc. and its affiliates.
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
+ MCP integration for CrewAI using MCPServerAdapter.
17
+
18
+ This module provides MCP server connection management for CrewAI agents.
19
+ """
20
+
21
+ from collections.abc import Generator
22
+ from contextlib import contextmanager
23
+ from typing import Any
24
+
25
+ from crewai_tools import MCPServerAdapter
26
+
27
+ from datarobot_genai.core.mcp.common import MCPConfig
28
+
29
+
30
+ @contextmanager
31
+ def mcp_tools_context(
32
+ authorization_context: dict[str, Any] | None = None,
33
+ forwarded_headers: dict[str, str] | None = None,
34
+ ) -> Generator[list[Any], None, None]:
35
+ """Context manager for MCP tools that handles connection lifecycle."""
36
+ config = MCPConfig(
37
+ authorization_context=authorization_context,
38
+ forwarded_headers=forwarded_headers,
39
+ )
40
+ # If no MCP server configured, return empty tools list
41
+ if not config.server_config:
42
+ print("No MCP server configured, using empty tools list", flush=True)
43
+ yield []
44
+ return
45
+
46
+ print(f"Connecting to MCP server: {config.server_config['url']}", flush=True)
47
+
48
+ # Use MCPServerAdapter as context manager with the server config
49
+ try:
50
+ with MCPServerAdapter(config.server_config) as tools:
51
+ print(
52
+ f"Successfully connected to MCP server, got {len(tools)} tools",
53
+ flush=True,
54
+ )
55
+ yield tools
56
+ except Exception as exc:
57
+ # Gracefully degrade when connection fails or adapter initialization raises
58
+ print(f"Failed to connect to MCP server: {exc}", flush=True)
59
+ yield []
@@ -0,0 +1,78 @@
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
+ DataRobot MCP Server Library.
17
+
18
+ A reusable library for building Model Context Protocol (MCP) servers with DataRobot integration.
19
+ """
20
+
21
+ # Export main server components
22
+ from datarobot_genai.drmcp.test_utils.clients.openai import OpenAILLMMCPClient
23
+ from datarobot_genai.drmcp.test_utils.mcp_utils_ete import ete_test_mcp_session
24
+ from datarobot_genai.drmcp.test_utils.mcp_utils_ete import get_dr_mcp_server_url
25
+ from datarobot_genai.drmcp.test_utils.mcp_utils_ete import get_headers
26
+ from datarobot_genai.drmcp.test_utils.mcp_utils_integration import integration_test_mcp_session
27
+ from datarobot_genai.drmcp.test_utils.tool_base_ete import ETETestExpectations
28
+ from datarobot_genai.drmcp.test_utils.tool_base_ete import ToolBaseE2E
29
+ from datarobot_genai.drmcp.test_utils.tool_base_ete import ToolCallTestExpectations
30
+
31
+ from .core.clients import get_sdk_client
32
+ from .core.config import MCPServerConfig
33
+ from .core.config import get_config
34
+ from .core.config_utils import extract_datarobot_credential_runtime_param_payload
35
+ from .core.config_utils import extract_datarobot_dict_runtime_param_payload
36
+ from .core.config_utils import extract_datarobot_runtime_param_payload
37
+ from .core.constants import RUNTIME_PARAM_ENV_VAR_NAME_PREFIX
38
+ from .core.credentials import MCPServerCredentials
39
+ from .core.credentials import get_credentials
40
+ from .core.dr_mcp_server import BaseServerLifecycle
41
+ from .core.dr_mcp_server import DataRobotMCPServer
42
+ from .core.dr_mcp_server import create_mcp_server
43
+ from .core.logging import MCPLogging
44
+ from .core.mcp_instance import dr_mcp_tool
45
+ from .core.mcp_instance import register_tools
46
+
47
+ __all__ = [
48
+ # Main server
49
+ "DataRobotMCPServer",
50
+ "create_mcp_server",
51
+ "BaseServerLifecycle",
52
+ # Configuration
53
+ "get_config",
54
+ "MCPServerConfig",
55
+ # Credentials
56
+ "get_credentials",
57
+ "MCPServerCredentials",
58
+ # Constants
59
+ "RUNTIME_PARAM_ENV_VAR_NAME_PREFIX",
60
+ # User extensibility
61
+ "get_sdk_client",
62
+ "dr_mcp_tool",
63
+ "register_tools",
64
+ # Utilities
65
+ "MCPLogging",
66
+ "extract_datarobot_runtime_param_payload",
67
+ "extract_datarobot_dict_runtime_param_payload",
68
+ "extract_datarobot_credential_runtime_param_payload",
69
+ # Test utilities
70
+ "get_dr_mcp_server_url",
71
+ "get_headers",
72
+ "ete_test_mcp_session",
73
+ "OpenAILLMMCPClient",
74
+ "ETETestExpectations",
75
+ "ToolBaseE2E",
76
+ "ToolCallTestExpectations",
77
+ "integration_test_mcp_session",
78
+ ]
@@ -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,165 @@
1
+ # Copyright 2025 DataRobot, Inc. and its affiliates.
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
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from typing import Any
19
+
20
+ from datarobot.auth.session import AuthCtx
21
+ from datarobot.models.genai.agent.auth import ToolAuth
22
+ from fastmcp.server.dependencies import get_context
23
+ from fastmcp.server.dependencies import get_http_headers
24
+ from fastmcp.server.middleware import CallNext
25
+ from fastmcp.server.middleware import Middleware
26
+ from fastmcp.server.middleware import MiddlewareContext
27
+ from fastmcp.tools.tool import ToolResult
28
+
29
+ from datarobot_genai.core.utils.auth import AsyncOAuthTokenProvider
30
+ from datarobot_genai.core.utils.auth import AuthContextHeaderHandler
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ AUTH_CTX_KEY = "authorization_context"
36
+
37
+
38
+ class OAuthMiddleWare(Middleware):
39
+ """Middleware that parses `x-datarobot-authorization-context` for tool calls.
40
+
41
+ The header is expected to be a JWT-encoded token representing an authentication
42
+ context compatible with :class:`datarobot.auth.session.AuthCtx`.
43
+
44
+ Attributes
45
+ ----------
46
+ auth_handler : AuthContextHeaderHandler
47
+ Handler for encoding/decoding JWT tokens containing auth context.
48
+ """
49
+
50
+ def __init__(self, auth_handler: AuthContextHeaderHandler | None = None) -> None:
51
+ self.auth_handler = auth_handler or AuthContextHeaderHandler()
52
+
53
+ async def on_call_tool(
54
+ self, context: MiddlewareContext, call_next: CallNext[Any, ToolResult]
55
+ ) -> ToolResult:
56
+ """Parse header and attach an AuthCtx to the context before running the tool.
57
+
58
+ Parameters
59
+ ----------
60
+ context : MiddlewareContext
61
+ The middleware context that will be passed to the tool.
62
+ call_next : CallNext[Any, ToolResult]
63
+ The next handler in the middleware chain.
64
+
65
+ Returns
66
+ -------
67
+ ToolResult
68
+ The result from the tool execution.
69
+ """
70
+ auth_context = self._extract_auth_context()
71
+ if not auth_context:
72
+ logger.debug("No valid authorization context extracted from request headers.")
73
+
74
+ if context.fastmcp_context is not None:
75
+ context.fastmcp_context.set_state(AUTH_CTX_KEY, auth_context)
76
+ logger.debug("Authorization context attached to state.")
77
+
78
+ return await call_next(context)
79
+
80
+ def _extract_auth_context(self) -> AuthCtx | None:
81
+ """Extract and validate authentication context from request headers.
82
+
83
+ Returns
84
+ -------
85
+ Optional[AuthCtx]
86
+ The validated authentication context, or None if extraction fails.
87
+ """
88
+ try:
89
+ headers = get_http_headers()
90
+ return self.auth_handler.get_context(headers)
91
+ except (ValueError, KeyError, TypeError) as exc:
92
+ logger.warning("Failed to extract auth context from headers: %s", exc, exc_info=True)
93
+ return None
94
+ except Exception as exc:
95
+ logger.error("Unexpected error extracting auth context: %s", exc, exc_info=True)
96
+ return None
97
+
98
+
99
+ async def must_get_auth_context() -> AuthCtx:
100
+ """Retrieve the AuthCtx from the current request context or raise error.
101
+
102
+ Raises
103
+ ------
104
+ RuntimeError
105
+ If no authorization context is found in the request.
106
+
107
+ Returns
108
+ -------
109
+ AuthCtx
110
+ The authorization context associated with the current request.
111
+ """
112
+ context = get_context()
113
+
114
+ auth_ctx = context.get_state(AUTH_CTX_KEY)
115
+ if not auth_ctx:
116
+ raise RuntimeError("Could not retrieve authorization context from FastMCP context state.")
117
+
118
+ return auth_ctx
119
+
120
+
121
+ async def get_access_token(provider_type: str | None = None) -> str:
122
+ """Retrieve access token from the DataRobot OAuth Provider Service.
123
+
124
+ OAuth access tokens can be retrieved only for providers where the user completed
125
+ the OAuth flow and granted consent.
126
+
127
+ Note:
128
+ * Currently, only On-Behalf-Of (OBO) tokens are supported, which allow tools to
129
+ act on behalf of the authenticated user, after the user has granted his consent.
130
+
131
+ Parameters
132
+ ----------
133
+ provider_type : str, optional
134
+ The name of the OAuth provider. It should match the name of the provider configured
135
+ during provider setup. If no value is provided and only one OAuth provider exists, that
136
+ provider will be used. If multiple providers exist and none is specified, an error will be
137
+ raised.
138
+
139
+ Returns
140
+ -------
141
+ The oauth access token.
142
+ """
143
+ auth_ctx = await must_get_auth_context()
144
+ logger.debug("Retrieved authorization context")
145
+
146
+ oauth_token_provider = AsyncOAuthTokenProvider(auth_ctx)
147
+ oauth_access_token = await oauth_token_provider.get_token(
148
+ auth_type=ToolAuth.OBO,
149
+ provider_type=provider_type,
150
+ )
151
+ return oauth_access_token
152
+
153
+
154
+ def initialize_oauth_middleware(mcp: Any) -> None:
155
+ """Initialize and register OAuth middleware with the MCP server.
156
+
157
+ Parameters
158
+ ----------
159
+ mcp : FastMCP
160
+ The FastMCP server instance to register the middleware with.
161
+ secret_key : Optional[str]
162
+ Secret key for JWT validation. If None, uses the value from config.
163
+ """
164
+ mcp.add_middleware(OAuthMiddleWare())
165
+ logger.info("OAuth middleware registered successfully")
@@ -0,0 +1,180 @@
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
+ from typing import cast
18
+
19
+ import datarobot as dr
20
+ from datarobot.context import Context as DRContext
21
+ from datarobot.rest import RESTClientObject
22
+ from fastmcp.server.dependencies import get_http_headers
23
+
24
+ from datarobot_genai.core.utils.auth import AuthContextHeaderHandler
25
+ from datarobot_genai.core.utils.auth import DRAppCtx
26
+
27
+ from .credentials import get_credentials
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Header names to check for authorization tokens (in order of preference)
32
+ HEADER_TOKEN_CANDIDATE_NAMES = [
33
+ "authorization",
34
+ "x-datarobot-api-token",
35
+ "x-datarobot-api-key",
36
+ ]
37
+
38
+
39
+ def _extract_token_from_headers(headers: dict[str, str]) -> str | None:
40
+ """
41
+ Extract a Bearer token from headers by checking multiple header name candidates.
42
+
43
+ Args:
44
+ headers: Dictionary of headers (keys should be lowercase)
45
+
46
+ Returns
47
+ -------
48
+ The extracted token string, or None if not found
49
+ """
50
+ for candidate_name in HEADER_TOKEN_CANDIDATE_NAMES:
51
+ auth_header = headers.get(candidate_name)
52
+ if not auth_header:
53
+ continue
54
+
55
+ if not isinstance(auth_header, str):
56
+ continue
57
+
58
+ # Handle Bearer token format
59
+ bearer_prefix = "bearer "
60
+ if auth_header.lower().startswith(bearer_prefix):
61
+ token = auth_header[len(bearer_prefix) :].strip()
62
+ else:
63
+ # Assume it's a plain token
64
+ token = auth_header.strip()
65
+
66
+ if token:
67
+ return token
68
+
69
+ return None
70
+
71
+
72
+ def _extract_token_from_auth_context(headers: dict[str, str]) -> str | None:
73
+ """
74
+ Extract API token from authorization context metadata as a fallback.
75
+
76
+ Args:
77
+ headers: Dictionary of headers (keys should be lowercase)
78
+
79
+ Returns
80
+ -------
81
+ The extracted API key from auth context metadata, or None if not found
82
+ """
83
+ try:
84
+ auth_handler = AuthContextHeaderHandler()
85
+
86
+ auth_ctx = auth_handler.get_context(headers)
87
+ if not auth_ctx or not auth_ctx.metadata:
88
+ return None
89
+
90
+ metadata = auth_ctx.metadata
91
+ if not isinstance(metadata, dict):
92
+ return None
93
+
94
+ dr_ctx: DRAppCtx = DRAppCtx(**metadata.get("dr_ctx", {}))
95
+ if dr_ctx.api_key:
96
+ logger.debug("Extracted token from auth context")
97
+ return dr_ctx.api_key
98
+
99
+ return None
100
+
101
+ except Exception as e:
102
+ logger.debug(f"Failed to get token from auth context: {e}")
103
+ return None
104
+
105
+
106
+ def extract_token_from_headers(headers: dict[str, str]) -> str | None:
107
+ """
108
+ Extract a token from headers with multiple fallback strategies.
109
+
110
+ This function attempts to extract a token in the following order:
111
+ 1. From standard authorization headers (Bearer token, x-datarobot-api-token, etc.)
112
+ 2. From authorization context metadata (dr_ctx.api_key)
113
+
114
+ Args:
115
+ headers: Dictionary of headers (keys should be lowercase)
116
+
117
+ Returns
118
+ -------
119
+ The extracted token string, or None if not found
120
+ """
121
+ if token := _extract_token_from_headers(headers):
122
+ return token
123
+
124
+ if token := _extract_token_from_auth_context(headers):
125
+ return token
126
+
127
+ return None
128
+
129
+
130
+ def get_sdk_client() -> Any:
131
+ """
132
+ Get a DataRobot SDK client, using the user's Bearer token from the request.
133
+
134
+ This function attempts to extract the Bearer token from the HTTP request headers
135
+ with fallback strategies:
136
+ 1. Standard authorization headers (Bearer token, x-datarobot-api-token, etc.)
137
+ 2. Authorization context metadata (dr_ctx.api_key)
138
+ 3. Application credentials as final fallback
139
+ """
140
+ token = None
141
+
142
+ try:
143
+ headers = get_http_headers()
144
+ if headers:
145
+ token = extract_token_from_headers(headers)
146
+ if token:
147
+ logger.debug("Using API token found in HTTP headers")
148
+ except Exception:
149
+ # No HTTP context e.g. stdio transport
150
+ logger.warning(
151
+ "Could not get HTTP headers, falling back to application credentials", exc_info=True
152
+ )
153
+
154
+ credentials = get_credentials()
155
+
156
+ # Fallback: Use application token
157
+ if not token:
158
+ token = credentials.datarobot.application_api_token
159
+ logger.debug("Using application API token from credentials")
160
+
161
+ dr.Client(token=token, endpoint=credentials.datarobot.endpoint)
162
+ # The trafaret setting up a use case in the context, seem to mess up the tool calls
163
+ DRContext.use_case = None
164
+ return dr
165
+
166
+
167
+ def get_api_client() -> RESTClientObject:
168
+ """Get a DataRobot SDK api client using application credentials."""
169
+ dr = get_sdk_client()
170
+
171
+ return cast(RESTClientObject, dr.client.get_client())
172
+
173
+
174
+ def get_s3_bucket_info() -> dict[str, str]:
175
+ """Get S3 bucket configuration."""
176
+ credentials = get_credentials()
177
+ return {
178
+ "bucket": credentials.aws_predictions_s3_bucket,
179
+ "prefix": credentials.aws_predictions_s3_prefix,
180
+ }