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.

Potentially problematic release.


This version of datarobot-genai might be problematic. Click here for more details.

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,107 @@
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
+ from fastmcp import FastMCP
16
+
17
+
18
+ class BaseServerLifecycle:
19
+ """
20
+ Base server lifecycle interface with safe default implementations.
21
+
22
+ This class provides hooks that are called at different stages of the server lifecycle.
23
+ Subclasses can override any or all of these methods to add custom behavior.
24
+ All methods have safe no-op defaults, so you only need to implement what you need.
25
+
26
+ Lifecycle Order:
27
+ 1. pre_server_start() - Before server initialization
28
+ 2. Server starts
29
+ 3. post_server_start() - After server is ready
30
+ 4. Server runs...
31
+ 5. Shutdown signal received
32
+ 6. pre_server_shutdown() - Before server cleanup
33
+ 7. Server stops
34
+
35
+ Example:
36
+ ```python
37
+ class MyLifecycle(BaseServerLifecycle):
38
+ async def pre_server_start(self, mcp: FastMCP) -> None:
39
+ # Initialize resources
40
+ self.db = await connect_database()
41
+
42
+ async def pre_server_shutdown(self, mcp: FastMCP) -> None:
43
+ # Clean up resources
44
+ await self.db.close()
45
+
46
+ # post_server_start not implemented - will use safe default (no-op)
47
+ ```
48
+ """
49
+
50
+ async def pre_server_start(self, mcp: FastMCP) -> None:
51
+ """
52
+ Call before the server starts.
53
+
54
+ Use this to:
55
+ - Initialize resources
56
+ - Set up connections
57
+ - Validate configuration
58
+ - Prepare server state
59
+
60
+ Args:
61
+ mcp: The FastMCP instance that will be started
62
+
63
+ Note:
64
+ Override this method in your subclass to add custom initialization.
65
+ The default implementation is a safe no-op.
66
+ """
67
+ pass
68
+
69
+ async def post_server_start(self, mcp: FastMCP) -> None:
70
+ """
71
+ Call after the server has started and is ready to handle requests.
72
+
73
+ Use this to:
74
+ - Register additional handlers
75
+ - Start background tasks
76
+ - Initialize delayed resources
77
+ - Log startup completion
78
+
79
+ Args:
80
+ mcp: The running FastMCP instance
81
+
82
+ Note:
83
+ Override this method in your subclass to add post-startup logic.
84
+ The default implementation is a safe no-op.
85
+ """
86
+ pass
87
+
88
+ async def pre_server_shutdown(self, mcp: FastMCP) -> None:
89
+ """
90
+ Call before the server shuts down.
91
+
92
+ Use this to:
93
+ - Close database connections
94
+ - Save application state
95
+ - Clean up temporary files
96
+ - Stop background tasks
97
+ - Release resources
98
+
99
+ Args:
100
+ mcp: The running FastMCP instance
101
+
102
+ Note:
103
+ Override this method in your subclass to add cleanup logic.
104
+ The default implementation is a safe no-op.
105
+ This is ALWAYS called, even on Ctrl+C or errors.
106
+ """
107
+ pass
@@ -0,0 +1,424 @@
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 functools
16
+ import inspect
17
+ import logging
18
+ import os
19
+ from collections.abc import Callable
20
+ from collections.abc import Iterator
21
+ from collections.abc import Mapping
22
+ from contextlib import contextmanager
23
+ from typing import Any
24
+ from typing import TypeVar
25
+
26
+ from fastmcp import FastMCP
27
+ from fastmcp.server.middleware import CallNext
28
+ from fastmcp.server.middleware import Middleware
29
+ from fastmcp.server.middleware import MiddlewareContext
30
+ from fastmcp.tools.tool import ToolResult
31
+ from opentelemetry import trace
32
+ from opentelemetry._logs import set_logger_provider
33
+ from opentelemetry.context import attach
34
+ from opentelemetry.context import detach
35
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
36
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
37
+ from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor
38
+ from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
39
+ from opentelemetry.instrumentation.requests import RequestsInstrumentor
40
+ from opentelemetry.propagate import extract
41
+ from opentelemetry.sdk._logs import LoggerProvider
42
+ from opentelemetry.sdk._logs import LoggingHandler
43
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
44
+ from opentelemetry.sdk.resources import Resource
45
+ from opentelemetry.sdk.trace import TracerProvider
46
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
47
+ from opentelemetry.trace import Span
48
+ from opentelemetry.trace import SpanContext
49
+ from opentelemetry.trace import Status
50
+ from opentelemetry.trace import StatusCode
51
+ from opentelemetry.trace import format_trace_id
52
+ from starlette.middleware.base import BaseHTTPMiddleware
53
+ from starlette.middleware.base import RequestResponseEndpoint
54
+ from starlette.requests import Request
55
+ from starlette.responses import Response
56
+
57
+ from .config import get_config
58
+ from .credentials import get_credentials
59
+
60
+ root_logger = logging.getLogger(__name__)
61
+ tracer = trace.get_tracer(__name__)
62
+
63
+ # Track instrumentation state
64
+ _INSTRUMENTED = False
65
+
66
+
67
+ @contextmanager
68
+ def with_otel_context(carrier: Mapping[str, str]) -> Iterator[Any]:
69
+ ctx = extract(carrier)
70
+ token = attach(ctx)
71
+ try:
72
+ yield
73
+ finally:
74
+ detach(token)
75
+
76
+
77
+ class OpenTelemetryMiddleware(Middleware):
78
+ def __init__(self, tracer_name: str = "fastmcp") -> None:
79
+ self.tracer = trace.get_tracer(tracer_name)
80
+
81
+ async def on_request(self, context: MiddlewareContext, call_next: CallNext[Any, Any]) -> Any:
82
+ with tracer.start_as_current_span(f"mcp.request.{context.method}") as span:
83
+ span.set_attribute("mcp.source", context.source)
84
+ span.set_attribute("mcp.type", context.type)
85
+ span.set_attribute("mcp.method", context.method or "")
86
+ try:
87
+ result = await call_next(context)
88
+ span.set_status(Status(StatusCode.OK))
89
+ except Exception as e:
90
+ span.set_status(Status(StatusCode.ERROR, str(e)))
91
+ span.record_exception(e)
92
+ raise
93
+ return result
94
+
95
+ async def on_call_tool(
96
+ self, context: MiddlewareContext, call_next: CallNext[Any, ToolResult]
97
+ ) -> ToolResult:
98
+ tool_name = context.message.name
99
+
100
+ with self.tracer.start_as_current_span(f"tool.{tool_name}") as span:
101
+ span.set_attributes(
102
+ {
103
+ "mcp.tool.name": tool_name,
104
+ "mcp.tool.arguments": str(context.message.arguments),
105
+ }
106
+ )
107
+ try:
108
+ result = await call_next(context)
109
+ span.set_attribute("mcp.tool.success", True)
110
+ if hasattr(result, "content"):
111
+ span.set_attribute("mcp.tool.content_length", len(str(result.content)))
112
+
113
+ span.set_status(Status(StatusCode.OK))
114
+ return result
115
+
116
+ except Exception as e:
117
+ span.set_attribute("mcp.tool.success", False)
118
+ span.set_attribute("mcp.tool.error", str(e))
119
+ span.set_status(Status(StatusCode.ERROR, str(e)))
120
+ span.record_exception(e)
121
+ raise
122
+
123
+
124
+ class OtelASGIMiddleware(BaseHTTPMiddleware):
125
+ """ASGI middleware extracts trace_id and parent span id from raw http request
126
+ and set them in the context, so downsream trace can link to them
127
+ if avaialble.
128
+ """
129
+
130
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
131
+ with with_otel_context(request.headers):
132
+ response = await call_next(request)
133
+ return response
134
+
135
+
136
+ def _setup_otel_env_variables() -> None:
137
+ """Set up OpenTelemetry environment variables for DataRobot integration."""
138
+ # do not override if already set
139
+ if os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") or os.environ.get(
140
+ "OTEL_EXPORTER_OTLP_HEADERS"
141
+ ):
142
+ root_logger.info(
143
+ "OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_HEADERS already set, skipping"
144
+ )
145
+ return
146
+
147
+ credentials = get_credentials()
148
+
149
+ config = get_config()
150
+ otlp_endpoint = config.otel_collector_base_url
151
+ entity_id = config.otel_entity_id
152
+
153
+ otlp_headers = (
154
+ f"X-DataRobot-Api-Key={credentials.datarobot.application_api_token},"
155
+ f"X-DataRobot-Entity-Id={entity_id}"
156
+ )
157
+ os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = otlp_endpoint
158
+ os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = otlp_headers
159
+ root_logger.info(
160
+ f"Using OTEL_EXPORTER_OTLP_ENDPOINT: {otlp_endpoint} with X-DataRobot-Entity-Id {entity_id}"
161
+ )
162
+
163
+
164
+ def _setup_otel_exporter() -> None:
165
+ """Set up OpenTelemetry exporter with SimpleSpanProcessor."""
166
+ otlp_exporter = OTLPSpanExporter()
167
+ span_processor = SimpleSpanProcessor(otlp_exporter)
168
+ provider = trace.get_tracer_provider()
169
+ # mypy: TracerProvider has add_span_processor at runtime; typing may lag
170
+ if hasattr(provider, "add_span_processor"):
171
+ provider.add_span_processor(span_processor)
172
+
173
+
174
+ class _ExcludeOtelLogsFilter(logging.Filter):
175
+ """A logging filter to exclude logs from the opentelemetry library."""
176
+
177
+ def filter(self, record: logging.LogRecord) -> bool:
178
+ return not record.name.startswith("opentelemetry")
179
+
180
+
181
+ def _setup_otel_logging(resource: Resource) -> LoggerProvider:
182
+ logger_provider = LoggerProvider(resource=resource)
183
+ set_logger_provider(logger_provider)
184
+ exporter = OTLPLogExporter()
185
+ logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
186
+ handler = LoggingHandler(level=logging.INFO, logger_provider=logger_provider)
187
+ handler.addFilter(_ExcludeOtelLogsFilter())
188
+ logging.getLogger().addHandler(handler)
189
+ return logger_provider
190
+
191
+
192
+ def _setup_http_instrumentors() -> None:
193
+ """Set up HTTP client instrumentors.
194
+
195
+ This function is idempotent - it will only instrument clients once.
196
+ """
197
+ # Use a local variable to avoid global statement warning
198
+ instrumented = _INSTRUMENTED
199
+ if instrumented:
200
+ root_logger.debug("HTTP clients already instrumented")
201
+ return
202
+
203
+ root_logger.info("Setting up HTTP client instrumentation")
204
+ try:
205
+ # Instrument requests library
206
+ RequestsInstrumentor().instrument()
207
+ root_logger.debug("Instrumented requests library")
208
+ except Exception as e:
209
+ root_logger.warning(f"Failed to instrument requests: {e}")
210
+
211
+ try:
212
+ # Instrument aiohttp client
213
+ AioHttpClientInstrumentor().instrument()
214
+ root_logger.debug("Instrumented aiohttp client")
215
+ except Exception as e:
216
+ root_logger.warning(f"Failed to instrument aiohttp: {e}")
217
+
218
+ try:
219
+ # Instrument httpx
220
+ HTTPXClientInstrumentor().instrument()
221
+ root_logger.debug("Instrumented httpx")
222
+ except Exception as e:
223
+ root_logger.warning(f"Failed to instrument httpx: {e}")
224
+
225
+ globals()["_INSTRUMENTED"] = True
226
+ root_logger.info("HTTP client instrumentation complete")
227
+
228
+
229
+ def _prepare_shared_attributes(
230
+ attributes: dict[str, Any],
231
+ ) -> dict[str, str]:
232
+ """Set custom attributes on a span."""
233
+ # Flatten nested attributes
234
+ flattened_attrs = {}
235
+ for key, value in attributes.items():
236
+ if isinstance(value, dict):
237
+ for sub_key, sub_value in value.items():
238
+ flattened_attrs[f"{key}.{sub_key}"] = sub_value
239
+
240
+ elif isinstance(value, (str, int, float, bool)):
241
+ flattened_attrs[key] = value
242
+ return flattened_attrs
243
+
244
+
245
+ def _set_otel_attributes(span: Span, attributes: dict[str, Any]) -> None:
246
+ """Set custom attributes on a span."""
247
+ attrs = _prepare_shared_attributes(attributes=attributes)
248
+ span.set_attributes(attrs)
249
+
250
+
251
+ def initialize_telemetry(mcp: FastMCP) -> None:
252
+ """Initialize OpenTelemetry for the FastMCP application."""
253
+ config = get_config()
254
+
255
+ # If OpenTelemetry is disabled, return None
256
+ if not config.otel_enabled:
257
+ root_logger.info("OpenTelemetry is disabled")
258
+ return None
259
+
260
+ # If OTEL_ENTITY_ID is not set, skip telemetry
261
+ if not config.otel_entity_id and not os.environ.get("OTEL_EXPORTER_OTLP_HEADERS"):
262
+ root_logger.info(
263
+ "Neither OTEL_ENTITY_ID nor OTEL_EXPORTER_OTLP_HEADERS is set, skipping telemetry"
264
+ )
265
+ return None
266
+
267
+ resource_attrs = {"datarobot.service.name": config.mcp_server_name}
268
+ if config.otel_attributes:
269
+ resource_attrs.update(_prepare_shared_attributes(config.otel_attributes))
270
+ resource = Resource.create(resource_attrs)
271
+
272
+ # Set up tracer provider with service name from config
273
+ provider = TracerProvider(resource=resource)
274
+ trace.set_tracer_provider(provider)
275
+
276
+ # Setup environment
277
+ _setup_otel_env_variables()
278
+
279
+ # Setup OTEL exporter
280
+ _setup_otel_exporter()
281
+ _setup_otel_logging(resource)
282
+
283
+ # Setup HTTP client instrumentation
284
+ if config.otel_enabled_http_instrumentors:
285
+ _setup_http_instrumentors()
286
+
287
+ mcp.add_middleware(OpenTelemetryMiddleware(__name__))
288
+
289
+
290
+ """Helper functions for OpenTelemetry instrumentation of tools."""
291
+
292
+
293
+ def _add_parameters_to_span(
294
+ span: Span, func: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]
295
+ ) -> None:
296
+ """Add function parameters as span attributes.
297
+
298
+ Only adds simple types (str, int, float, bool) to avoid complex object serialization.
299
+ Skips 'self' parameter for methods.
300
+ """
301
+ # Get parameter names
302
+ sig = inspect.signature(func)
303
+ param_names = list(sig.parameters.keys())
304
+
305
+ # Skip 'self' parameter for methods (only if first param is named 'self')
306
+ start_idx = 1 if args and param_names and param_names[0] == "self" else 0
307
+ param_names = param_names[start_idx:]
308
+ args = args[start_idx:]
309
+
310
+ # Add positional arguments
311
+ for name, value in zip(param_names, args):
312
+ if isinstance(value, (str, int, float, bool)):
313
+ span.set_attribute(f"tool.param.{name}", value)
314
+
315
+ # Add keyword arguments
316
+ for name, value in kwargs.items():
317
+ if isinstance(value, (str, int, float, bool)):
318
+ span.set_attribute(f"tool.param.{name}", value)
319
+
320
+
321
+ def get_trace_id() -> str | None:
322
+ """Get the current trace ID if available."""
323
+ current_span = trace.get_current_span()
324
+ if not current_span:
325
+ return None
326
+
327
+ context: SpanContext = current_span.get_span_context()
328
+ if not context.is_valid:
329
+ return None
330
+
331
+ return str(format_trace_id(context.trace_id))
332
+
333
+
334
+ T = TypeVar("T", bound=Callable[..., Any])
335
+
336
+
337
+ def trace_execution(trace_name: str | None = None, trace_type: str = "tool") -> Callable[[T], T]:
338
+ """Trace tool execution.
339
+
340
+ Args:
341
+ trace_name: Optional name for the span. If not provided, uses the function name.
342
+ trace_type: Optional type for the span. If not provided, uses "tool".
343
+
344
+ Example:
345
+ @trace_execution()
346
+ async def my_tool(self, param1: str) -> str:
347
+ return "result"
348
+
349
+ @trace_execution("custom_name")
350
+ async def another_tool(self, param1: str) -> str:
351
+ return "result"
352
+ """
353
+
354
+ def decorator(func: T) -> T:
355
+ def _create_span_for_tool(
356
+ trace_name: str | None,
357
+ trace_type: str,
358
+ args: tuple[Any, ...],
359
+ kwargs: dict[str, Any],
360
+ ) -> Span:
361
+ # Get span name from decorator arg, function name, or class method name
362
+ span_name = trace_name
363
+ if not span_name:
364
+ if (
365
+ args
366
+ and hasattr(args[0], "__class__")
367
+ and not isinstance(args[0], (str, int, float, bool))
368
+ ):
369
+ # If it's a method, include the class name
370
+ span_name = f"{args[0].__class__.__name__}.{func.__name__}"
371
+ else:
372
+ # Just use the function name without any prefix
373
+ span_name = func.__name__
374
+
375
+ # Start a new span
376
+ tracer = trace.get_tracer(__name__)
377
+ span = tracer.start_span(f"{trace_type}.{span_name}")
378
+
379
+ # Add standard attributes
380
+ span.set_attribute("mcp.type", trace_type)
381
+ span.set_attribute(f"{trace_type}.name", span_name)
382
+
383
+ # Add tool parameters as span attributes
384
+ _add_parameters_to_span(span, func, args, kwargs)
385
+
386
+ # Add configured attributes from config
387
+ config = get_config()
388
+ if config.otel_attributes:
389
+ _set_otel_attributes(span, config.otel_attributes)
390
+
391
+ return span
392
+
393
+ @functools.wraps(func)
394
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
395
+ span = _create_span_for_tool(trace_name, trace_type, args, kwargs)
396
+ try:
397
+ result = await func(*args, **kwargs)
398
+ span.set_attribute(f"{trace_type}.success", True)
399
+ return result
400
+ except Exception as e:
401
+ span.set_attribute(f"{trace_type}.success", False)
402
+ span.record_exception(e)
403
+ raise e
404
+ finally:
405
+ span.end()
406
+
407
+ @functools.wraps(func)
408
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
409
+ span = _create_span_for_tool(trace_name, trace_type, args, kwargs)
410
+ try:
411
+ result = func(*args, **kwargs)
412
+ span.set_attribute(f"{trace_type}.success", True)
413
+ return result
414
+ except Exception as e:
415
+ span.set_attribute(f"{trace_type}.success", False)
416
+ span.record_exception(e)
417
+ raise e
418
+ finally:
419
+ span.end()
420
+
421
+ # Use appropriate wrapper based on whether the function is async
422
+ return async_wrapper if inspect.iscoroutinefunction(func) else sync_wrapper # type: ignore[return-value]
423
+
424
+ return decorator
@@ -0,0 +1,111 @@
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
+ """Tool configuration and enablement logic."""
16
+
17
+ from collections.abc import Callable
18
+ from enum import Enum
19
+ from typing import TYPE_CHECKING
20
+ from typing import TypedDict
21
+
22
+ if TYPE_CHECKING:
23
+ from .config import MCPServerConfig
24
+
25
+
26
+ class ToolType(str, Enum):
27
+ """Enumeration of available tool types."""
28
+
29
+ PREDICTIVE = "predictive"
30
+ JIRA = "jira"
31
+ CONFLUENCE = "confluence"
32
+ GDRIVE = "gdrive"
33
+ MICROSOFT_GRAPH = "microsoft_graph"
34
+
35
+
36
+ class ToolConfig(TypedDict):
37
+ """Configuration for a tool type."""
38
+
39
+ name: str
40
+ oauth_check: Callable[["MCPServerConfig"], bool] | None
41
+ directory: str
42
+ package_prefix: str
43
+ config_field_name: str
44
+
45
+
46
+ # Tool configuration registry
47
+ TOOL_CONFIGS: dict[ToolType, ToolConfig] = {
48
+ ToolType.PREDICTIVE: ToolConfig(
49
+ name="predictive",
50
+ oauth_check=None,
51
+ directory="predictive",
52
+ package_prefix="datarobot_genai.drmcp.tools.predictive",
53
+ config_field_name="enable_predictive_tools",
54
+ ),
55
+ ToolType.JIRA: ToolConfig(
56
+ name="jira",
57
+ oauth_check=lambda config: config.tool_config.is_atlassian_oauth_configured,
58
+ directory="jira",
59
+ package_prefix="datarobot_genai.drmcp.tools.jira",
60
+ config_field_name="enable_jira_tools",
61
+ ),
62
+ ToolType.CONFLUENCE: ToolConfig(
63
+ name="confluence",
64
+ oauth_check=lambda config: config.tool_config.is_atlassian_oauth_configured,
65
+ directory="confluence",
66
+ package_prefix="datarobot_genai.drmcp.tools.confluence",
67
+ config_field_name="enable_confluence_tools",
68
+ ),
69
+ ToolType.GDRIVE: ToolConfig(
70
+ name="gdrive",
71
+ oauth_check=lambda config: config.tool_config.is_google_oauth_configured,
72
+ directory="gdrive",
73
+ package_prefix="datarobot_genai.drmcp.tools.gdrive",
74
+ config_field_name="enable_gdrive_tools",
75
+ ),
76
+ ToolType.MICROSOFT_GRAPH: ToolConfig(
77
+ name="microsoft_graph",
78
+ oauth_check=lambda config: config.tool_config.is_microsoft_oauth_configured,
79
+ directory="microsoft_graph",
80
+ package_prefix="datarobot_genai.drmcp.tools.microsoft_graph",
81
+ config_field_name="enable_microsoft_graph_tools",
82
+ ),
83
+ }
84
+
85
+
86
+ def get_tool_enable_config_name(tool_type: ToolType) -> str:
87
+ """Get the configuration field name for enabling a tool."""
88
+ return TOOL_CONFIGS[tool_type]["config_field_name"]
89
+
90
+
91
+ def is_tool_enabled(tool_type: ToolType, config: "MCPServerConfig") -> bool:
92
+ """
93
+ Check if a tool is enabled based on configuration.
94
+
95
+ Args:
96
+ tool_type: The type of tool to check
97
+ config: The server configuration
98
+
99
+ Returns
100
+ -------
101
+ True if the tool is enabled, False otherwise
102
+ """
103
+ tool_config_registry = TOOL_CONFIGS[tool_type]
104
+ enable_config_name = tool_config_registry["config_field_name"]
105
+ is_enabled = getattr(config.tool_config, enable_config_name)
106
+
107
+ # If tool is enabled, check OAuth requirements if needed
108
+ if is_enabled and tool_config_registry["oauth_check"] is not None:
109
+ return tool_config_registry["oauth_check"](config)
110
+
111
+ return is_enabled