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.
- datarobot_genai/__init__.py +19 -0
- datarobot_genai/core/__init__.py +0 -0
- datarobot_genai/core/agents/__init__.py +43 -0
- datarobot_genai/core/agents/base.py +195 -0
- datarobot_genai/core/chat/__init__.py +19 -0
- datarobot_genai/core/chat/auth.py +146 -0
- datarobot_genai/core/chat/client.py +178 -0
- datarobot_genai/core/chat/responses.py +297 -0
- datarobot_genai/core/cli/__init__.py +18 -0
- datarobot_genai/core/cli/agent_environment.py +47 -0
- datarobot_genai/core/cli/agent_kernel.py +211 -0
- datarobot_genai/core/custom_model.py +141 -0
- datarobot_genai/core/mcp/__init__.py +0 -0
- datarobot_genai/core/mcp/common.py +218 -0
- datarobot_genai/core/telemetry_agent.py +126 -0
- datarobot_genai/core/utils/__init__.py +3 -0
- datarobot_genai/core/utils/auth.py +234 -0
- datarobot_genai/core/utils/urls.py +64 -0
- datarobot_genai/crewai/__init__.py +24 -0
- datarobot_genai/crewai/agent.py +42 -0
- datarobot_genai/crewai/base.py +159 -0
- datarobot_genai/crewai/events.py +117 -0
- datarobot_genai/crewai/mcp.py +59 -0
- datarobot_genai/drmcp/__init__.py +78 -0
- datarobot_genai/drmcp/core/__init__.py +13 -0
- datarobot_genai/drmcp/core/auth.py +165 -0
- datarobot_genai/drmcp/core/clients.py +180 -0
- datarobot_genai/drmcp/core/config.py +364 -0
- datarobot_genai/drmcp/core/config_utils.py +174 -0
- datarobot_genai/drmcp/core/constants.py +18 -0
- datarobot_genai/drmcp/core/credentials.py +190 -0
- datarobot_genai/drmcp/core/dr_mcp_server.py +350 -0
- datarobot_genai/drmcp/core/dr_mcp_server_logo.py +136 -0
- datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +13 -0
- datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +130 -0
- datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +70 -0
- datarobot_genai/drmcp/core/dynamic_prompts/register.py +205 -0
- datarobot_genai/drmcp/core/dynamic_prompts/utils.py +33 -0
- datarobot_genai/drmcp/core/dynamic_tools/__init__.py +14 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +14 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +72 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +82 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +238 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +228 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +63 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +162 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +87 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +36 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +10 -0
- datarobot_genai/drmcp/core/dynamic_tools/register.py +254 -0
- datarobot_genai/drmcp/core/dynamic_tools/schema.py +532 -0
- datarobot_genai/drmcp/core/exceptions.py +25 -0
- datarobot_genai/drmcp/core/logging.py +98 -0
- datarobot_genai/drmcp/core/mcp_instance.py +515 -0
- datarobot_genai/drmcp/core/memory_management/__init__.py +13 -0
- datarobot_genai/drmcp/core/memory_management/manager.py +820 -0
- datarobot_genai/drmcp/core/memory_management/memory_tools.py +201 -0
- datarobot_genai/drmcp/core/routes.py +439 -0
- datarobot_genai/drmcp/core/routes_utils.py +30 -0
- datarobot_genai/drmcp/core/server_life_cycle.py +107 -0
- datarobot_genai/drmcp/core/telemetry.py +424 -0
- datarobot_genai/drmcp/core/tool_config.py +111 -0
- datarobot_genai/drmcp/core/tool_filter.py +117 -0
- datarobot_genai/drmcp/core/utils.py +138 -0
- datarobot_genai/drmcp/server.py +19 -0
- datarobot_genai/drmcp/test_utils/__init__.py +13 -0
- datarobot_genai/drmcp/test_utils/clients/__init__.py +0 -0
- datarobot_genai/drmcp/test_utils/clients/anthropic.py +68 -0
- datarobot_genai/drmcp/test_utils/clients/base.py +300 -0
- datarobot_genai/drmcp/test_utils/clients/dr_gateway.py +58 -0
- datarobot_genai/drmcp/test_utils/clients/openai.py +68 -0
- datarobot_genai/drmcp/test_utils/elicitation_test_tool.py +89 -0
- datarobot_genai/drmcp/test_utils/integration_mcp_server.py +109 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +133 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +107 -0
- datarobot_genai/drmcp/test_utils/test_interactive.py +205 -0
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +220 -0
- datarobot_genai/drmcp/test_utils/utils.py +91 -0
- datarobot_genai/drmcp/tools/__init__.py +14 -0
- datarobot_genai/drmcp/tools/clients/__init__.py +14 -0
- datarobot_genai/drmcp/tools/clients/atlassian.py +188 -0
- datarobot_genai/drmcp/tools/clients/confluence.py +584 -0
- datarobot_genai/drmcp/tools/clients/gdrive.py +832 -0
- datarobot_genai/drmcp/tools/clients/jira.py +334 -0
- datarobot_genai/drmcp/tools/clients/microsoft_graph.py +479 -0
- datarobot_genai/drmcp/tools/clients/s3.py +28 -0
- datarobot_genai/drmcp/tools/confluence/__init__.py +14 -0
- datarobot_genai/drmcp/tools/confluence/tools.py +321 -0
- datarobot_genai/drmcp/tools/gdrive/__init__.py +0 -0
- datarobot_genai/drmcp/tools/gdrive/tools.py +347 -0
- datarobot_genai/drmcp/tools/jira/__init__.py +14 -0
- datarobot_genai/drmcp/tools/jira/tools.py +243 -0
- datarobot_genai/drmcp/tools/microsoft_graph/__init__.py +13 -0
- datarobot_genai/drmcp/tools/microsoft_graph/tools.py +198 -0
- datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
- datarobot_genai/drmcp/tools/predictive/data.py +133 -0
- datarobot_genai/drmcp/tools/predictive/deployment.py +91 -0
- datarobot_genai/drmcp/tools/predictive/deployment_info.py +392 -0
- datarobot_genai/drmcp/tools/predictive/model.py +148 -0
- datarobot_genai/drmcp/tools/predictive/predict.py +254 -0
- datarobot_genai/drmcp/tools/predictive/predict_realtime.py +307 -0
- datarobot_genai/drmcp/tools/predictive/project.py +90 -0
- datarobot_genai/drmcp/tools/predictive/training.py +661 -0
- datarobot_genai/langgraph/__init__.py +0 -0
- datarobot_genai/langgraph/agent.py +341 -0
- datarobot_genai/langgraph/mcp.py +73 -0
- datarobot_genai/llama_index/__init__.py +16 -0
- datarobot_genai/llama_index/agent.py +50 -0
- datarobot_genai/llama_index/base.py +299 -0
- datarobot_genai/llama_index/mcp.py +79 -0
- datarobot_genai/nat/__init__.py +0 -0
- datarobot_genai/nat/agent.py +275 -0
- datarobot_genai/nat/datarobot_auth_provider.py +110 -0
- datarobot_genai/nat/datarobot_llm_clients.py +318 -0
- datarobot_genai/nat/datarobot_llm_providers.py +130 -0
- datarobot_genai/nat/datarobot_mcp_client.py +266 -0
- datarobot_genai/nat/helpers.py +87 -0
- datarobot_genai/py.typed +0 -0
- datarobot_genai-0.2.31.dist-info/METADATA +145 -0
- datarobot_genai-0.2.31.dist-info/RECORD +125 -0
- datarobot_genai-0.2.31.dist-info/WHEEL +4 -0
- datarobot_genai-0.2.31.dist-info/entry_points.txt +5 -0
- datarobot_genai-0.2.31.dist-info/licenses/AUTHORS +2 -0
- datarobot_genai-0.2.31.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,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
|