datarobot-genai 0.2.31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. datarobot_genai/__init__.py +19 -0
  2. datarobot_genai/core/__init__.py +0 -0
  3. datarobot_genai/core/agents/__init__.py +43 -0
  4. datarobot_genai/core/agents/base.py +195 -0
  5. datarobot_genai/core/chat/__init__.py +19 -0
  6. datarobot_genai/core/chat/auth.py +146 -0
  7. datarobot_genai/core/chat/client.py +178 -0
  8. datarobot_genai/core/chat/responses.py +297 -0
  9. datarobot_genai/core/cli/__init__.py +18 -0
  10. datarobot_genai/core/cli/agent_environment.py +47 -0
  11. datarobot_genai/core/cli/agent_kernel.py +211 -0
  12. datarobot_genai/core/custom_model.py +141 -0
  13. datarobot_genai/core/mcp/__init__.py +0 -0
  14. datarobot_genai/core/mcp/common.py +218 -0
  15. datarobot_genai/core/telemetry_agent.py +126 -0
  16. datarobot_genai/core/utils/__init__.py +3 -0
  17. datarobot_genai/core/utils/auth.py +234 -0
  18. datarobot_genai/core/utils/urls.py +64 -0
  19. datarobot_genai/crewai/__init__.py +24 -0
  20. datarobot_genai/crewai/agent.py +42 -0
  21. datarobot_genai/crewai/base.py +159 -0
  22. datarobot_genai/crewai/events.py +117 -0
  23. datarobot_genai/crewai/mcp.py +59 -0
  24. datarobot_genai/drmcp/__init__.py +78 -0
  25. datarobot_genai/drmcp/core/__init__.py +13 -0
  26. datarobot_genai/drmcp/core/auth.py +165 -0
  27. datarobot_genai/drmcp/core/clients.py +180 -0
  28. datarobot_genai/drmcp/core/config.py +364 -0
  29. datarobot_genai/drmcp/core/config_utils.py +174 -0
  30. datarobot_genai/drmcp/core/constants.py +18 -0
  31. datarobot_genai/drmcp/core/credentials.py +190 -0
  32. datarobot_genai/drmcp/core/dr_mcp_server.py +350 -0
  33. datarobot_genai/drmcp/core/dr_mcp_server_logo.py +136 -0
  34. datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +13 -0
  35. datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +130 -0
  36. datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +70 -0
  37. datarobot_genai/drmcp/core/dynamic_prompts/register.py +205 -0
  38. datarobot_genai/drmcp/core/dynamic_prompts/utils.py +33 -0
  39. datarobot_genai/drmcp/core/dynamic_tools/__init__.py +14 -0
  40. datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
  41. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +14 -0
  42. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +72 -0
  43. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +82 -0
  44. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +238 -0
  45. datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +228 -0
  46. datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +63 -0
  47. datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +162 -0
  48. datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +87 -0
  49. datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +36 -0
  50. datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +10 -0
  51. datarobot_genai/drmcp/core/dynamic_tools/register.py +254 -0
  52. datarobot_genai/drmcp/core/dynamic_tools/schema.py +532 -0
  53. datarobot_genai/drmcp/core/exceptions.py +25 -0
  54. datarobot_genai/drmcp/core/logging.py +98 -0
  55. datarobot_genai/drmcp/core/mcp_instance.py +515 -0
  56. datarobot_genai/drmcp/core/memory_management/__init__.py +13 -0
  57. datarobot_genai/drmcp/core/memory_management/manager.py +820 -0
  58. datarobot_genai/drmcp/core/memory_management/memory_tools.py +201 -0
  59. datarobot_genai/drmcp/core/routes.py +439 -0
  60. datarobot_genai/drmcp/core/routes_utils.py +30 -0
  61. datarobot_genai/drmcp/core/server_life_cycle.py +107 -0
  62. datarobot_genai/drmcp/core/telemetry.py +424 -0
  63. datarobot_genai/drmcp/core/tool_config.py +111 -0
  64. datarobot_genai/drmcp/core/tool_filter.py +117 -0
  65. datarobot_genai/drmcp/core/utils.py +138 -0
  66. datarobot_genai/drmcp/server.py +19 -0
  67. datarobot_genai/drmcp/test_utils/__init__.py +13 -0
  68. datarobot_genai/drmcp/test_utils/clients/__init__.py +0 -0
  69. datarobot_genai/drmcp/test_utils/clients/anthropic.py +68 -0
  70. datarobot_genai/drmcp/test_utils/clients/base.py +300 -0
  71. datarobot_genai/drmcp/test_utils/clients/dr_gateway.py +58 -0
  72. datarobot_genai/drmcp/test_utils/clients/openai.py +68 -0
  73. datarobot_genai/drmcp/test_utils/elicitation_test_tool.py +89 -0
  74. datarobot_genai/drmcp/test_utils/integration_mcp_server.py +109 -0
  75. datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +133 -0
  76. datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +107 -0
  77. datarobot_genai/drmcp/test_utils/test_interactive.py +205 -0
  78. datarobot_genai/drmcp/test_utils/tool_base_ete.py +220 -0
  79. datarobot_genai/drmcp/test_utils/utils.py +91 -0
  80. datarobot_genai/drmcp/tools/__init__.py +14 -0
  81. datarobot_genai/drmcp/tools/clients/__init__.py +14 -0
  82. datarobot_genai/drmcp/tools/clients/atlassian.py +188 -0
  83. datarobot_genai/drmcp/tools/clients/confluence.py +584 -0
  84. datarobot_genai/drmcp/tools/clients/gdrive.py +832 -0
  85. datarobot_genai/drmcp/tools/clients/jira.py +334 -0
  86. datarobot_genai/drmcp/tools/clients/microsoft_graph.py +479 -0
  87. datarobot_genai/drmcp/tools/clients/s3.py +28 -0
  88. datarobot_genai/drmcp/tools/confluence/__init__.py +14 -0
  89. datarobot_genai/drmcp/tools/confluence/tools.py +321 -0
  90. datarobot_genai/drmcp/tools/gdrive/__init__.py +0 -0
  91. datarobot_genai/drmcp/tools/gdrive/tools.py +347 -0
  92. datarobot_genai/drmcp/tools/jira/__init__.py +14 -0
  93. datarobot_genai/drmcp/tools/jira/tools.py +243 -0
  94. datarobot_genai/drmcp/tools/microsoft_graph/__init__.py +13 -0
  95. datarobot_genai/drmcp/tools/microsoft_graph/tools.py +198 -0
  96. datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
  97. datarobot_genai/drmcp/tools/predictive/data.py +133 -0
  98. datarobot_genai/drmcp/tools/predictive/deployment.py +91 -0
  99. datarobot_genai/drmcp/tools/predictive/deployment_info.py +392 -0
  100. datarobot_genai/drmcp/tools/predictive/model.py +148 -0
  101. datarobot_genai/drmcp/tools/predictive/predict.py +254 -0
  102. datarobot_genai/drmcp/tools/predictive/predict_realtime.py +307 -0
  103. datarobot_genai/drmcp/tools/predictive/project.py +90 -0
  104. datarobot_genai/drmcp/tools/predictive/training.py +661 -0
  105. datarobot_genai/langgraph/__init__.py +0 -0
  106. datarobot_genai/langgraph/agent.py +341 -0
  107. datarobot_genai/langgraph/mcp.py +73 -0
  108. datarobot_genai/llama_index/__init__.py +16 -0
  109. datarobot_genai/llama_index/agent.py +50 -0
  110. datarobot_genai/llama_index/base.py +299 -0
  111. datarobot_genai/llama_index/mcp.py +79 -0
  112. datarobot_genai/nat/__init__.py +0 -0
  113. datarobot_genai/nat/agent.py +275 -0
  114. datarobot_genai/nat/datarobot_auth_provider.py +110 -0
  115. datarobot_genai/nat/datarobot_llm_clients.py +318 -0
  116. datarobot_genai/nat/datarobot_llm_providers.py +130 -0
  117. datarobot_genai/nat/datarobot_mcp_client.py +266 -0
  118. datarobot_genai/nat/helpers.py +87 -0
  119. datarobot_genai/py.typed +0 -0
  120. datarobot_genai-0.2.31.dist-info/METADATA +145 -0
  121. datarobot_genai-0.2.31.dist-info/RECORD +125 -0
  122. datarobot_genai-0.2.31.dist-info/WHEEL +4 -0
  123. datarobot_genai-0.2.31.dist-info/entry_points.txt +5 -0
  124. datarobot_genai-0.2.31.dist-info/licenses/AUTHORS +2 -0
  125. datarobot_genai-0.2.31.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,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)
@@ -0,0 +1,3 @@
1
+ from .urls import get_api_base
2
+
3
+ __all__ = ["get_api_base"]