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.
- 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,141 @@
|
|
|
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
|
+
#
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""Helpers to implement DataRobot Custom Model chat entrypoints."""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
from collections.abc import AsyncGenerator
|
|
24
|
+
from collections.abc import Iterator
|
|
25
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
26
|
+
from typing import Any
|
|
27
|
+
from typing import Literal
|
|
28
|
+
|
|
29
|
+
from openai.types.chat import CompletionCreateParams
|
|
30
|
+
from openai.types.chat.completion_create_params import CompletionCreateParamsNonStreaming
|
|
31
|
+
from openai.types.chat.completion_create_params import CompletionCreateParamsStreaming
|
|
32
|
+
|
|
33
|
+
from datarobot_genai.core.chat import CustomModelChatResponse
|
|
34
|
+
from datarobot_genai.core.chat import CustomModelStreamingResponse
|
|
35
|
+
from datarobot_genai.core.chat import to_custom_model_chat_response
|
|
36
|
+
from datarobot_genai.core.chat import to_custom_model_streaming_response
|
|
37
|
+
from datarobot_genai.core.chat.auth import resolve_authorization_context
|
|
38
|
+
from datarobot_genai.core.telemetry_agent import instrument
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_model() -> tuple[ThreadPoolExecutor, asyncio.AbstractEventLoop]:
|
|
44
|
+
"""Initialize a dedicated event loop within a worker thread.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
(ThreadPoolExecutor, asyncio.AbstractEventLoop)
|
|
49
|
+
A single-worker executor and the associated event loop.
|
|
50
|
+
"""
|
|
51
|
+
thread_pool_executor = ThreadPoolExecutor(1)
|
|
52
|
+
event_loop = asyncio.new_event_loop()
|
|
53
|
+
thread_pool_executor.submit(asyncio.set_event_loop, event_loop).result()
|
|
54
|
+
return (thread_pool_executor, event_loop)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def chat_entrypoint(
|
|
58
|
+
agent_cls: type[Any],
|
|
59
|
+
completion_create_params: CompletionCreateParams
|
|
60
|
+
| CompletionCreateParamsNonStreaming
|
|
61
|
+
| CompletionCreateParamsStreaming,
|
|
62
|
+
load_model_result: tuple[ThreadPoolExecutor, asyncio.AbstractEventLoop],
|
|
63
|
+
*,
|
|
64
|
+
work_dir: str | None = None,
|
|
65
|
+
framework: Literal["crewai", "langgraph", "llamaindex", "nat"] | None = None,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
) -> CustomModelChatResponse | Iterator[CustomModelStreamingResponse]:
|
|
68
|
+
"""Run a generic Custom Model chat entrypoint for agent-based implementations.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
agent_cls : Type[Any]
|
|
73
|
+
The agent class to instantiate. Must define an ``async invoke(...)`` method
|
|
74
|
+
returning either:
|
|
75
|
+
- a tuple (response_text, pipeline_interactions, usage_metrics)
|
|
76
|
+
- or an async generator yielding (delta_text, pipeline_interactions, usage_metrics)
|
|
77
|
+
completion_create_params : CompletionCreateParams | ...
|
|
78
|
+
Parameters supplied by OpenAI-compatible Chat API.
|
|
79
|
+
load_model_result : tuple[ThreadPoolExecutor, asyncio.AbstractEventLoop]
|
|
80
|
+
Values returned by :func:`load_model`.
|
|
81
|
+
work_dir : Optional[str]
|
|
82
|
+
Working directory to ``chdir`` into before invoking the agent. This is useful
|
|
83
|
+
when relative paths are used in agent templates.
|
|
84
|
+
framework : Optional[Literal["crewai", "langgraph", "llamaindex", "nat"]]
|
|
85
|
+
When provided, idempotently instruments HTTP clients, OpenAI SDK, and the
|
|
86
|
+
given framework. If omitted, general instrumentation is still applied.
|
|
87
|
+
**kwargs : Any
|
|
88
|
+
Extra values forwarded for header-based auth context extraction.
|
|
89
|
+
"""
|
|
90
|
+
thread_pool_executor, event_loop = load_model_result
|
|
91
|
+
|
|
92
|
+
# Set up telemetry (idempotent). When framework is provided, instrument it as well.
|
|
93
|
+
try:
|
|
94
|
+
instrument(framework)
|
|
95
|
+
except Exception:
|
|
96
|
+
# Instrumentation is best-effort; proceed regardless
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Optionally change working directory for frameworks which rely on relative paths
|
|
100
|
+
if work_dir:
|
|
101
|
+
try:
|
|
102
|
+
os.chdir(work_dir)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.warning(f"Failed to change working directory to {work_dir}: {e}")
|
|
105
|
+
|
|
106
|
+
# Retrieve authorization context using all supported methods for downstream agents/tools
|
|
107
|
+
completion_create_params["authorization_context"] = resolve_authorization_context(
|
|
108
|
+
completion_create_params, **kwargs
|
|
109
|
+
)
|
|
110
|
+
# Keep only allowed headers from the forwarded_headers.
|
|
111
|
+
incoming_headers = kwargs.get("headers", {}) or {}
|
|
112
|
+
allowed_headers = {"x-datarobot-api-token", "x-datarobot-api-key"}
|
|
113
|
+
forwarded_headers = {k: v for k, v in incoming_headers.items() if k.lower() in allowed_headers}
|
|
114
|
+
completion_create_params["forwarded_headers"] = forwarded_headers
|
|
115
|
+
|
|
116
|
+
# Instantiate user agent with all supplied completion params including auth context
|
|
117
|
+
agent = agent_cls(**completion_create_params)
|
|
118
|
+
|
|
119
|
+
# Invoke the agent and check if it returns a generator or a tuple
|
|
120
|
+
result = thread_pool_executor.submit(
|
|
121
|
+
event_loop.run_until_complete,
|
|
122
|
+
agent.invoke(completion_create_params=completion_create_params),
|
|
123
|
+
).result()
|
|
124
|
+
|
|
125
|
+
# Streaming response (async generator)
|
|
126
|
+
if isinstance(result, AsyncGenerator):
|
|
127
|
+
return to_custom_model_streaming_response(
|
|
128
|
+
thread_pool_executor,
|
|
129
|
+
event_loop,
|
|
130
|
+
result,
|
|
131
|
+
model=completion_create_params.get("model"),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Non-streaming response
|
|
135
|
+
response_text, pipeline_interactions, usage_metrics = result
|
|
136
|
+
return to_custom_model_chat_response(
|
|
137
|
+
response_text,
|
|
138
|
+
pipeline_interactions,
|
|
139
|
+
usage_metrics,
|
|
140
|
+
model=completion_create_params.get("model"),
|
|
141
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,218 @@
|
|
|
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
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import re
|
|
18
|
+
from http import HTTPStatus
|
|
19
|
+
from typing import Any
|
|
20
|
+
from typing import Literal
|
|
21
|
+
|
|
22
|
+
import requests
|
|
23
|
+
from datarobot.core.config import DataRobotAppFrameworkBaseSettings
|
|
24
|
+
from pydantic import field_validator
|
|
25
|
+
|
|
26
|
+
from datarobot_genai.core.utils.auth import AuthContextHeaderHandler
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MCPConfig(DataRobotAppFrameworkBaseSettings):
|
|
32
|
+
"""Configuration for MCP server connection.
|
|
33
|
+
|
|
34
|
+
Derived values are exposed as properties rather than stored, avoiding
|
|
35
|
+
Pydantic field validation/serialization concerns for internal helpers.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
external_mcp_url: str | None = None
|
|
39
|
+
external_mcp_headers: str | None = None
|
|
40
|
+
external_mcp_transport: Literal["sse", "streamable-http"] = "streamable-http"
|
|
41
|
+
mcp_deployment_id: str | None = None
|
|
42
|
+
datarobot_endpoint: str | None = None
|
|
43
|
+
datarobot_api_token: str | None = None
|
|
44
|
+
authorization_context: dict[str, Any] | None = None
|
|
45
|
+
forwarded_headers: dict[str, str] | None = None
|
|
46
|
+
mcp_server_port: int | None = None
|
|
47
|
+
|
|
48
|
+
_auth_context_handler: AuthContextHeaderHandler | None = None
|
|
49
|
+
_server_config: dict[str, Any] | None = None
|
|
50
|
+
|
|
51
|
+
@field_validator("external_mcp_headers", mode="before")
|
|
52
|
+
@classmethod
|
|
53
|
+
def validate_external_mcp_headers(cls, value: str | None) -> str | None:
|
|
54
|
+
if value is None:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
candidate = value.strip()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
json.loads(candidate)
|
|
61
|
+
except json.JSONDecodeError:
|
|
62
|
+
msg = "external_mcp_headers must be valid JSON"
|
|
63
|
+
logger.warning(msg)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return candidate
|
|
67
|
+
|
|
68
|
+
@field_validator("mcp_deployment_id", mode="before")
|
|
69
|
+
@classmethod
|
|
70
|
+
def validate_mcp_deployment_id(cls, value: str | None) -> str | None:
|
|
71
|
+
if value is None:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
candidate = value.strip()
|
|
75
|
+
|
|
76
|
+
if not re.fullmatch(r"[0-9a-fA-F]{24}", candidate):
|
|
77
|
+
msg = "mcp_deployment_id must be a valid 24-character hex ID"
|
|
78
|
+
logger.warning(msg)
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
return candidate
|
|
82
|
+
|
|
83
|
+
def _authorization_bearer_header(self) -> dict[str, str]:
|
|
84
|
+
"""Return Authorization header with Bearer token or empty dict."""
|
|
85
|
+
if not self.datarobot_api_token:
|
|
86
|
+
return {}
|
|
87
|
+
auth = (
|
|
88
|
+
self.datarobot_api_token
|
|
89
|
+
if self.datarobot_api_token.startswith("Bearer ")
|
|
90
|
+
else f"Bearer {self.datarobot_api_token}"
|
|
91
|
+
)
|
|
92
|
+
return {"Authorization": auth}
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def auth_context_handler(self) -> AuthContextHeaderHandler:
|
|
96
|
+
if self._auth_context_handler is None:
|
|
97
|
+
self._auth_context_handler = AuthContextHeaderHandler()
|
|
98
|
+
return self._auth_context_handler
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def server_config(self) -> dict[str, Any] | None:
|
|
102
|
+
if self._server_config is None:
|
|
103
|
+
self._server_config = self._build_server_config()
|
|
104
|
+
return self._server_config
|
|
105
|
+
|
|
106
|
+
def _authorization_context_header(self) -> dict[str, str]:
|
|
107
|
+
"""Return X-DataRobot-Authorization-Context header or empty dict."""
|
|
108
|
+
try:
|
|
109
|
+
return self.auth_context_handler.get_header(self.authorization_context)
|
|
110
|
+
except (LookupError, RuntimeError):
|
|
111
|
+
# Authorization context not available (e.g., in tests)
|
|
112
|
+
return {}
|
|
113
|
+
|
|
114
|
+
def _build_authenticated_headers(self) -> dict[str, str]:
|
|
115
|
+
"""Build headers for authenticated requests.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
Dictionary containing forwarded headers (if available) and authentication headers.
|
|
120
|
+
"""
|
|
121
|
+
headers: dict[str, str] = {}
|
|
122
|
+
if self.forwarded_headers:
|
|
123
|
+
headers.update(self.forwarded_headers)
|
|
124
|
+
headers.update(self._authorization_bearer_header())
|
|
125
|
+
headers.update(self._authorization_context_header())
|
|
126
|
+
return headers
|
|
127
|
+
|
|
128
|
+
def _check_localhost_server(self, url: str, timeout: float = 2.0) -> bool:
|
|
129
|
+
"""Check if MCP server is running on localhost.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
url : str
|
|
134
|
+
The URL to check.
|
|
135
|
+
timeout : float, optional
|
|
136
|
+
Request timeout in seconds (default: 2.0).
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
bool
|
|
141
|
+
True if server is running and responding with OK status, False otherwise.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
response = requests.get(url, timeout=timeout)
|
|
145
|
+
return (
|
|
146
|
+
response.status_code == HTTPStatus.OK
|
|
147
|
+
and response.json().get("message") == "DataRobot MCP Server is running"
|
|
148
|
+
)
|
|
149
|
+
except requests.RequestException as e:
|
|
150
|
+
logger.debug(f"Failed to connect to MCP server at {url}: {e}")
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
def _build_server_config(self) -> dict[str, Any] | None:
|
|
154
|
+
"""
|
|
155
|
+
Get MCP server configuration.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
Server configuration dict with url, transport, and optional headers,
|
|
160
|
+
or None if not configured.
|
|
161
|
+
"""
|
|
162
|
+
if self.mcp_deployment_id:
|
|
163
|
+
# DataRobot deployment ID - requires authentication
|
|
164
|
+
if self.datarobot_endpoint is None:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
"When using a DataRobot hosted MCP deployment, datarobot_endpoint must be set."
|
|
167
|
+
)
|
|
168
|
+
if self.datarobot_api_token is None:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
"When using a DataRobot hosted MCP deployment, datarobot_api_token must be set."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
base_url = self.datarobot_endpoint.rstrip("/")
|
|
174
|
+
if not base_url.endswith("/api/v2"):
|
|
175
|
+
base_url = f"{base_url}/api/v2"
|
|
176
|
+
|
|
177
|
+
url = f"{base_url}/deployments/{self.mcp_deployment_id}/directAccess/mcp"
|
|
178
|
+
headers = self._build_authenticated_headers()
|
|
179
|
+
|
|
180
|
+
logger.info(f"Using DataRobot hosted MCP deployment: {url}")
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
"url": url,
|
|
184
|
+
"transport": "streamable-http",
|
|
185
|
+
"headers": headers,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if self.external_mcp_url:
|
|
189
|
+
# External MCP URL - no authentication needed
|
|
190
|
+
headers = {}
|
|
191
|
+
|
|
192
|
+
# Merge external headers if provided
|
|
193
|
+
if self.external_mcp_headers:
|
|
194
|
+
external_headers = json.loads(self.external_mcp_headers)
|
|
195
|
+
headers.update(external_headers)
|
|
196
|
+
|
|
197
|
+
logger.info(f"Using external MCP URL: {self.external_mcp_url}")
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
"url": self.external_mcp_url.rstrip("/"),
|
|
201
|
+
"transport": self.external_mcp_transport,
|
|
202
|
+
"headers": headers,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# No MCP configuration found, setup localhost if running locally
|
|
206
|
+
if self.mcp_server_port:
|
|
207
|
+
url = f"http://localhost:{self.mcp_server_port}"
|
|
208
|
+
if self._check_localhost_server(url):
|
|
209
|
+
headers = self._build_authenticated_headers()
|
|
210
|
+
logger.info(f"Using localhost MCP server: {url}")
|
|
211
|
+
return {
|
|
212
|
+
"url": f"{url}/mcp",
|
|
213
|
+
"transport": "streamable-http",
|
|
214
|
+
"headers": headers,
|
|
215
|
+
}
|
|
216
|
+
logger.warning(f"MCP server is not running or not responding at {url}")
|
|
217
|
+
|
|
218
|
+
return None
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
"""Lightweight, idempotent client/framework instrumentation for agents."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
from typing import Any
|
|
23
|
+
from typing import Literal
|
|
24
|
+
from typing import cast
|
|
25
|
+
|
|
26
|
+
# Suppress the "Attempting to instrument while already instrumented" warning
|
|
27
|
+
logging.getLogger("opentelemetry.instrumentation.instrumentor").setLevel(logging.ERROR)
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Internal instrumentation state to avoid 'global' mutation warnings
|
|
31
|
+
_INSTRUMENTATION_STATE = {"http": False, "openai": False, "threading": False}
|
|
32
|
+
_INSTRUMENTED_FRAMEWORKS: set[str] = set()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _instrument_threading() -> None:
|
|
36
|
+
if _INSTRUMENTATION_STATE["threading"]:
|
|
37
|
+
return
|
|
38
|
+
try:
|
|
39
|
+
threading_module = importlib.import_module("opentelemetry.instrumentation.threading")
|
|
40
|
+
threading_instrumentor = getattr(threading_module, "ThreadingInstrumentor")
|
|
41
|
+
threading_instrumentor().instrument()
|
|
42
|
+
_INSTRUMENTATION_STATE["threading"] = True
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.debug(f"threading instrumentation skipped: {e}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _instrument_http_clients() -> None:
|
|
48
|
+
if _INSTRUMENTATION_STATE["http"]:
|
|
49
|
+
return
|
|
50
|
+
try:
|
|
51
|
+
requests_module = importlib.import_module("opentelemetry.instrumentation.requests")
|
|
52
|
+
requests_instrumentor = getattr(requests_module, "RequestsInstrumentor")
|
|
53
|
+
requests_instrumentor().instrument()
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.debug(f"requests instrumentation skipped: {e}")
|
|
56
|
+
try:
|
|
57
|
+
aiohttp_module = importlib.import_module("opentelemetry.instrumentation.aiohttp_client")
|
|
58
|
+
aiohttp_instrumentor = getattr(aiohttp_module, "AioHttpClientInstrumentor")
|
|
59
|
+
aiohttp_instrumentor().instrument()
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.debug(f"aiohttp instrumentation skipped: {e}")
|
|
62
|
+
try:
|
|
63
|
+
httpx_module = importlib.import_module("opentelemetry.instrumentation.httpx")
|
|
64
|
+
httpx_instrumentor = getattr(httpx_module, "HTTPXClientInstrumentor")
|
|
65
|
+
httpx_instrumentor().instrument()
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.debug(f"httpx instrumentation skipped: {e}")
|
|
68
|
+
_INSTRUMENTATION_STATE["http"] = True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _instrument_openai() -> None:
|
|
72
|
+
if _INSTRUMENTATION_STATE["openai"]:
|
|
73
|
+
return
|
|
74
|
+
try:
|
|
75
|
+
openai_module = importlib.import_module("opentelemetry.instrumentation.openai")
|
|
76
|
+
openai_instrumentor = getattr(openai_module, "OpenAIInstrumentor")
|
|
77
|
+
openai_instrumentor().instrument()
|
|
78
|
+
_INSTRUMENTATION_STATE["openai"] = True
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.debug(f"openai instrumentation skipped: {e}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _instrument_framework(framework: str) -> None:
|
|
84
|
+
if framework in _INSTRUMENTED_FRAMEWORKS:
|
|
85
|
+
return
|
|
86
|
+
try:
|
|
87
|
+
if framework == "crewai":
|
|
88
|
+
crewai_module = importlib.import_module("opentelemetry.instrumentation.crewai")
|
|
89
|
+
crewai_instrumentor = getattr(crewai_module, "CrewAIInstrumentor")
|
|
90
|
+
crewai_instrumentor().instrument()
|
|
91
|
+
os.environ.setdefault("CREWAI_TESTING", "true")
|
|
92
|
+
elif framework == "langgraph":
|
|
93
|
+
# Provided by opentelemetry-instrumentation-langchain
|
|
94
|
+
langchain_module = importlib.import_module("opentelemetry.instrumentation.langchain")
|
|
95
|
+
langchain_instrumentor = getattr(langchain_module, "LangchainInstrumentor")
|
|
96
|
+
langchain_instrumentor().instrument()
|
|
97
|
+
elif framework == "llamaindex":
|
|
98
|
+
llamaindex_module = importlib.import_module("opentelemetry.instrumentation.llamaindex")
|
|
99
|
+
llamaindex_instrumentor = getattr(llamaindex_module, "LlamaIndexInstrumentor")
|
|
100
|
+
# LlamaIndex instrumentor lacks precise typing; cast to Any to avoid mypy complaints
|
|
101
|
+
cast(Any, llamaindex_instrumentor()).instrument()
|
|
102
|
+
elif framework == "nat":
|
|
103
|
+
_instrument_framework("crewai")
|
|
104
|
+
_instrument_framework("langgraph")
|
|
105
|
+
_instrument_framework("llamaindex")
|
|
106
|
+
_INSTRUMENTED_FRAMEWORKS.add(framework)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.debug(f"{framework} instrumentation skipped: {e}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def instrument(
|
|
112
|
+
framework: Literal["crewai", "langgraph", "llamaindex", "nat"] | None = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Idempotently instrument supported HTTP clients, OpenAI SDK, and optionally a framework.
|
|
115
|
+
|
|
116
|
+
Also disables telemetry for some third-party libraries to avoid duplicate/undesired tracking.
|
|
117
|
+
"""
|
|
118
|
+
# Some libraries collect telemetry data by default. Disable that.
|
|
119
|
+
os.environ.setdefault("RAGAS_DO_NOT_TRACK", "true")
|
|
120
|
+
os.environ.setdefault("DEEPEVAL_TELEMETRY_OPT_OUT", "YES")
|
|
121
|
+
|
|
122
|
+
_instrument_threading()
|
|
123
|
+
_instrument_http_clients()
|
|
124
|
+
_instrument_openai()
|
|
125
|
+
if framework:
|
|
126
|
+
_instrument_framework(framework)
|