datarobot-genai 0.2.24__py3-none-any.whl → 0.2.29__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/drmcp/core/config.py +121 -83
- datarobot_genai/drmcp/core/dr_mcp_server.py +0 -3
- datarobot_genai/drmcp/core/mcp_instance.py +37 -103
- datarobot_genai/drmcp/core/tool_config.py +17 -9
- datarobot_genai/drmcp/core/tool_filter.py +10 -1
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +68 -1
- datarobot_genai/drmcp/tools/clients/gdrive.py +127 -0
- datarobot_genai/drmcp/tools/clients/microsoft_graph.py +479 -0
- datarobot_genai/drmcp/tools/gdrive/tools.py +95 -2
- 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/data.py +11 -3
- datarobot_genai/drmcp/tools/predictive/project.py +45 -27
- datarobot_genai/drmcp/tools/predictive/training.py +1 -0
- datarobot_genai/nat/datarobot_llm_clients.py +90 -54
- datarobot_genai/nat/datarobot_mcp_client.py +47 -15
- {datarobot_genai-0.2.24.dist-info → datarobot_genai-0.2.29.dist-info}/METADATA +1 -1
- {datarobot_genai-0.2.24.dist-info → datarobot_genai-0.2.29.dist-info}/RECORD +22 -20
- datarobot_genai/drmcp/core/mcp_server_tools.py +0 -129
- {datarobot_genai-0.2.24.dist-info → datarobot_genai-0.2.29.dist-info}/WHEEL +0 -0
- {datarobot_genai-0.2.24.dist-info → datarobot_genai-0.2.29.dist-info}/entry_points.txt +0 -0
- {datarobot_genai-0.2.24.dist-info → datarobot_genai-0.2.29.dist-info}/licenses/AUTHORS +0 -0
- {datarobot_genai-0.2.24.dist-info → datarobot_genai-0.2.29.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,22 +12,18 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
15
17
|
from collections.abc import AsyncGenerator
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
16
19
|
from typing import Any
|
|
17
20
|
from typing import TypeVar
|
|
18
21
|
|
|
19
|
-
from crewai import LLM
|
|
20
|
-
from langchain_openai import ChatOpenAI
|
|
21
|
-
from llama_index.core.base.llms.types import LLMMetadata
|
|
22
|
-
from llama_index.llms.litellm import LiteLLM
|
|
23
22
|
from nat.builder.builder import Builder
|
|
24
23
|
from nat.builder.framework_enum import LLMFrameworkEnum
|
|
25
24
|
from nat.cli.register_workflow import register_llm_client
|
|
26
25
|
from nat.data_models.llm import LLMBaseConfig
|
|
27
26
|
from nat.data_models.retry_mixin import RetryMixin
|
|
28
|
-
from nat.plugins.langchain.llm import (
|
|
29
|
-
_patch_llm_based_on_config as langchain_patch_llm_based_on_config,
|
|
30
|
-
)
|
|
31
27
|
from nat.utils.exception_handlers.automatic_retries import patch_with_retry
|
|
32
28
|
|
|
33
29
|
from ..nat.datarobot_llm_providers import DataRobotLLMComponentModelConfig
|
|
@@ -35,6 +31,11 @@ from ..nat.datarobot_llm_providers import DataRobotLLMDeploymentModelConfig
|
|
|
35
31
|
from ..nat.datarobot_llm_providers import DataRobotLLMGatewayModelConfig
|
|
36
32
|
from ..nat.datarobot_llm_providers import DataRobotNIMModelConfig
|
|
37
33
|
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from crewai import LLM
|
|
36
|
+
from langchain_openai import ChatOpenAI
|
|
37
|
+
from llama_index.llms.litellm import LiteLLM
|
|
38
|
+
|
|
38
39
|
ModelType = TypeVar("ModelType")
|
|
39
40
|
|
|
40
41
|
|
|
@@ -50,42 +51,53 @@ def _patch_llm_based_on_config(client: ModelType, llm_config: LLMBaseConfig) ->
|
|
|
50
51
|
return client
|
|
51
52
|
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
endpoints with LlamaIndex agents, you must override this method to return the appropriate
|
|
80
|
-
metadata.
|
|
54
|
+
def _create_datarobot_chat_openai(config: dict[str, Any]) -> Any:
|
|
55
|
+
from langchain_openai import ChatOpenAI # noqa: PLC0415
|
|
56
|
+
|
|
57
|
+
class DataRobotChatOpenAI(ChatOpenAI):
|
|
58
|
+
def _get_request_payload( # type: ignore[override]
|
|
59
|
+
self,
|
|
60
|
+
*args: Any,
|
|
61
|
+
**kwargs: Any,
|
|
62
|
+
) -> dict:
|
|
63
|
+
# We need to default to include_usage=True for streaming but we get 400 response
|
|
64
|
+
# if stream_options is present for a non-streaming call.
|
|
65
|
+
payload = super()._get_request_payload(*args, **kwargs)
|
|
66
|
+
if not payload.get("stream"):
|
|
67
|
+
payload.pop("stream_options", None)
|
|
68
|
+
return payload
|
|
69
|
+
|
|
70
|
+
return DataRobotChatOpenAI(**config)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _create_datarobot_litellm(config: dict[str, Any]) -> Any:
|
|
74
|
+
from llama_index.core.base.llms.types import LLMMetadata # noqa: PLC0415
|
|
75
|
+
from llama_index.llms.litellm import LiteLLM # noqa: PLC0415
|
|
76
|
+
|
|
77
|
+
class DataRobotLiteLLM(LiteLLM): # type: ignore[misc]
|
|
78
|
+
"""DataRobotLiteLLM is a small LiteLLM wrapper class that makes all LiteLLM endpoints
|
|
79
|
+
compatible with the LlamaIndex library.
|
|
81
80
|
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def metadata(self) -> LLMMetadata:
|
|
84
|
+
"""Returns the metadata for the LLM.
|
|
85
|
+
|
|
86
|
+
This is required to enable the is_chat_model and is_function_calling_model, which are
|
|
87
|
+
mandatory for LlamaIndex agents. By default, LlamaIndex assumes these are false unless
|
|
88
|
+
each individual model config in LiteLLM explicitly sets them to true. To use custom LLM
|
|
89
|
+
endpoints with LlamaIndex agents, you must override this method to return the
|
|
90
|
+
appropriate metadata.
|
|
91
|
+
"""
|
|
92
|
+
return LLMMetadata(
|
|
93
|
+
context_window=128000,
|
|
94
|
+
num_output=self.max_tokens or -1,
|
|
95
|
+
is_chat_model=True,
|
|
96
|
+
is_function_calling_model=True,
|
|
97
|
+
model_name=self.model,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return DataRobotLiteLLM(**config)
|
|
89
101
|
|
|
90
102
|
|
|
91
103
|
@register_llm_client(
|
|
@@ -94,11 +106,15 @@ class DataRobotLiteLLM(LiteLLM): # type: ignore[misc]
|
|
|
94
106
|
async def datarobot_llm_gateway_langchain(
|
|
95
107
|
llm_config: DataRobotLLMGatewayModelConfig, builder: Builder
|
|
96
108
|
) -> AsyncGenerator[ChatOpenAI]:
|
|
109
|
+
from nat.plugins.langchain.llm import ( # noqa: PLC0415
|
|
110
|
+
_patch_llm_based_on_config as langchain_patch_llm_based_on_config,
|
|
111
|
+
)
|
|
112
|
+
|
|
97
113
|
config = llm_config.model_dump(exclude={"type", "thinking"}, by_alias=True, exclude_none=True)
|
|
98
114
|
config["base_url"] = config["base_url"] + "/genai/llmgw"
|
|
99
115
|
config["stream_options"] = {"include_usage": True}
|
|
100
116
|
config["model"] = config["model"].removeprefix("datarobot/")
|
|
101
|
-
client =
|
|
117
|
+
client = _create_datarobot_chat_openai(config)
|
|
102
118
|
yield langchain_patch_llm_based_on_config(client, config)
|
|
103
119
|
|
|
104
120
|
|
|
@@ -108,6 +124,8 @@ async def datarobot_llm_gateway_langchain(
|
|
|
108
124
|
async def datarobot_llm_gateway_crewai(
|
|
109
125
|
llm_config: DataRobotLLMGatewayModelConfig, builder: Builder
|
|
110
126
|
) -> AsyncGenerator[LLM]:
|
|
127
|
+
from crewai import LLM # noqa: PLC0415
|
|
128
|
+
|
|
111
129
|
config = llm_config.model_dump(exclude={"type", "thinking"}, by_alias=True, exclude_none=True)
|
|
112
130
|
if not config["model"].startswith("datarobot/"):
|
|
113
131
|
config["model"] = "datarobot/" + config["model"]
|
|
@@ -121,12 +139,12 @@ async def datarobot_llm_gateway_crewai(
|
|
|
121
139
|
)
|
|
122
140
|
async def datarobot_llm_gateway_llamaindex(
|
|
123
141
|
llm_config: DataRobotLLMGatewayModelConfig, builder: Builder
|
|
124
|
-
) -> AsyncGenerator[
|
|
142
|
+
) -> AsyncGenerator[LiteLLM]:
|
|
125
143
|
config = llm_config.model_dump(exclude={"type", "thinking"}, by_alias=True, exclude_none=True)
|
|
126
144
|
if not config["model"].startswith("datarobot/"):
|
|
127
145
|
config["model"] = "datarobot/" + config["model"]
|
|
128
146
|
config["api_base"] = config.pop("base_url").removesuffix("/api/v2")
|
|
129
|
-
client =
|
|
147
|
+
client = _create_datarobot_litellm(config)
|
|
130
148
|
yield _patch_llm_based_on_config(client, config)
|
|
131
149
|
|
|
132
150
|
|
|
@@ -136,6 +154,10 @@ async def datarobot_llm_gateway_llamaindex(
|
|
|
136
154
|
async def datarobot_llm_deployment_langchain(
|
|
137
155
|
llm_config: DataRobotLLMDeploymentModelConfig, builder: Builder
|
|
138
156
|
) -> AsyncGenerator[ChatOpenAI]:
|
|
157
|
+
from nat.plugins.langchain.llm import ( # noqa: PLC0415
|
|
158
|
+
_patch_llm_based_on_config as langchain_patch_llm_based_on_config,
|
|
159
|
+
)
|
|
160
|
+
|
|
139
161
|
config = llm_config.model_dump(
|
|
140
162
|
exclude={"type", "thinking"},
|
|
141
163
|
by_alias=True,
|
|
@@ -143,7 +165,7 @@ async def datarobot_llm_deployment_langchain(
|
|
|
143
165
|
)
|
|
144
166
|
config["stream_options"] = {"include_usage": True}
|
|
145
167
|
config["model"] = config["model"].removeprefix("datarobot/")
|
|
146
|
-
client =
|
|
168
|
+
client = _create_datarobot_chat_openai(config)
|
|
147
169
|
yield langchain_patch_llm_based_on_config(client, config)
|
|
148
170
|
|
|
149
171
|
|
|
@@ -153,6 +175,8 @@ async def datarobot_llm_deployment_langchain(
|
|
|
153
175
|
async def datarobot_llm_deployment_crewai(
|
|
154
176
|
llm_config: DataRobotLLMDeploymentModelConfig, builder: Builder
|
|
155
177
|
) -> AsyncGenerator[LLM]:
|
|
178
|
+
from crewai import LLM # noqa: PLC0415
|
|
179
|
+
|
|
156
180
|
config = llm_config.model_dump(
|
|
157
181
|
exclude={"type", "thinking"},
|
|
158
182
|
by_alias=True,
|
|
@@ -170,7 +194,7 @@ async def datarobot_llm_deployment_crewai(
|
|
|
170
194
|
)
|
|
171
195
|
async def datarobot_llm_deployment_llamaindex(
|
|
172
196
|
llm_config: DataRobotLLMDeploymentModelConfig, builder: Builder
|
|
173
|
-
) -> AsyncGenerator[
|
|
197
|
+
) -> AsyncGenerator[LiteLLM]:
|
|
174
198
|
config = llm_config.model_dump(
|
|
175
199
|
exclude={"type", "thinking"},
|
|
176
200
|
by_alias=True,
|
|
@@ -179,7 +203,7 @@ async def datarobot_llm_deployment_llamaindex(
|
|
|
179
203
|
if not config["model"].startswith("datarobot/"):
|
|
180
204
|
config["model"] = "datarobot/" + config["model"]
|
|
181
205
|
config["api_base"] = config.pop("base_url") + "/chat/completions"
|
|
182
|
-
client =
|
|
206
|
+
client = _create_datarobot_litellm(config)
|
|
183
207
|
yield _patch_llm_based_on_config(client, config)
|
|
184
208
|
|
|
185
209
|
|
|
@@ -187,6 +211,10 @@ async def datarobot_llm_deployment_llamaindex(
|
|
|
187
211
|
async def datarobot_nim_langchain(
|
|
188
212
|
llm_config: DataRobotNIMModelConfig, builder: Builder
|
|
189
213
|
) -> AsyncGenerator[ChatOpenAI]:
|
|
214
|
+
from nat.plugins.langchain.llm import ( # noqa: PLC0415
|
|
215
|
+
_patch_llm_based_on_config as langchain_patch_llm_based_on_config,
|
|
216
|
+
)
|
|
217
|
+
|
|
190
218
|
config = llm_config.model_dump(
|
|
191
219
|
exclude={"type", "thinking"},
|
|
192
220
|
by_alias=True,
|
|
@@ -194,7 +222,7 @@ async def datarobot_nim_langchain(
|
|
|
194
222
|
)
|
|
195
223
|
config["stream_options"] = {"include_usage": True}
|
|
196
224
|
config["model"] = config["model"].removeprefix("datarobot/")
|
|
197
|
-
client =
|
|
225
|
+
client = _create_datarobot_chat_openai(config)
|
|
198
226
|
yield langchain_patch_llm_based_on_config(client, config)
|
|
199
227
|
|
|
200
228
|
|
|
@@ -202,6 +230,8 @@ async def datarobot_nim_langchain(
|
|
|
202
230
|
async def datarobot_nim_crewai(
|
|
203
231
|
llm_config: DataRobotNIMModelConfig, builder: Builder
|
|
204
232
|
) -> AsyncGenerator[LLM]:
|
|
233
|
+
from crewai import LLM # noqa: PLC0415
|
|
234
|
+
|
|
205
235
|
config = llm_config.model_dump(
|
|
206
236
|
exclude={"type", "thinking", "max_retries"},
|
|
207
237
|
by_alias=True,
|
|
@@ -217,7 +247,7 @@ async def datarobot_nim_crewai(
|
|
|
217
247
|
@register_llm_client(config_type=DataRobotNIMModelConfig, wrapper_type=LLMFrameworkEnum.LLAMA_INDEX)
|
|
218
248
|
async def datarobot_nim_llamaindex(
|
|
219
249
|
llm_config: DataRobotNIMModelConfig, builder: Builder
|
|
220
|
-
) -> AsyncGenerator[
|
|
250
|
+
) -> AsyncGenerator[LiteLLM]:
|
|
221
251
|
config = llm_config.model_dump(
|
|
222
252
|
exclude={"type", "thinking"},
|
|
223
253
|
by_alias=True,
|
|
@@ -226,7 +256,7 @@ async def datarobot_nim_llamaindex(
|
|
|
226
256
|
if not config["model"].startswith("datarobot/"):
|
|
227
257
|
config["model"] = "datarobot/" + config["model"]
|
|
228
258
|
config["api_base"] = config.pop("base_url") + "/chat/completions"
|
|
229
|
-
client =
|
|
259
|
+
client = _create_datarobot_litellm(config)
|
|
230
260
|
yield _patch_llm_based_on_config(client, config)
|
|
231
261
|
|
|
232
262
|
|
|
@@ -236,13 +266,17 @@ async def datarobot_nim_llamaindex(
|
|
|
236
266
|
async def datarobot_llm_component_langchain(
|
|
237
267
|
llm_config: DataRobotLLMComponentModelConfig, builder: Builder
|
|
238
268
|
) -> AsyncGenerator[ChatOpenAI]:
|
|
269
|
+
from nat.plugins.langchain.llm import ( # noqa: PLC0415
|
|
270
|
+
_patch_llm_based_on_config as langchain_patch_llm_based_on_config,
|
|
271
|
+
)
|
|
272
|
+
|
|
239
273
|
config = llm_config.model_dump(exclude={"type", "thinking"}, by_alias=True, exclude_none=True)
|
|
240
274
|
if config["use_datarobot_llm_gateway"]:
|
|
241
275
|
config["base_url"] = config["base_url"] + "/genai/llmgw"
|
|
242
276
|
config["stream_options"] = {"include_usage": True}
|
|
243
277
|
config["model"] = config["model"].removeprefix("datarobot/")
|
|
244
278
|
config.pop("use_datarobot_llm_gateway")
|
|
245
|
-
client =
|
|
279
|
+
client = _create_datarobot_chat_openai(config)
|
|
246
280
|
yield langchain_patch_llm_based_on_config(client, config)
|
|
247
281
|
|
|
248
282
|
|
|
@@ -252,6 +286,8 @@ async def datarobot_llm_component_langchain(
|
|
|
252
286
|
async def datarobot_llm_component_crewai(
|
|
253
287
|
llm_config: DataRobotLLMComponentModelConfig, builder: Builder
|
|
254
288
|
) -> AsyncGenerator[LLM]:
|
|
289
|
+
from crewai import LLM # noqa: PLC0415
|
|
290
|
+
|
|
255
291
|
config = llm_config.model_dump(exclude={"type", "thinking"}, by_alias=True, exclude_none=True)
|
|
256
292
|
if not config["model"].startswith("datarobot/"):
|
|
257
293
|
config["model"] = "datarobot/" + config["model"]
|
|
@@ -269,7 +305,7 @@ async def datarobot_llm_component_crewai(
|
|
|
269
305
|
)
|
|
270
306
|
async def datarobot_llm_component_llamaindex(
|
|
271
307
|
llm_config: DataRobotLLMComponentModelConfig, builder: Builder
|
|
272
|
-
) -> AsyncGenerator[
|
|
308
|
+
) -> AsyncGenerator[LiteLLM]:
|
|
273
309
|
config = llm_config.model_dump(exclude={"type", "thinking"}, by_alias=True, exclude_none=True)
|
|
274
310
|
if not config["model"].startswith("datarobot/"):
|
|
275
311
|
config["model"] = "datarobot/" + config["model"]
|
|
@@ -278,5 +314,5 @@ async def datarobot_llm_component_llamaindex(
|
|
|
278
314
|
else:
|
|
279
315
|
config["api_base"] = config.pop("base_url") + "/chat/completions"
|
|
280
316
|
config.pop("use_datarobot_llm_gateway")
|
|
281
|
-
client =
|
|
317
|
+
client = _create_datarobot_litellm(config)
|
|
282
318
|
yield _patch_llm_based_on_config(client, config)
|
|
@@ -12,57 +12,83 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
15
17
|
import logging
|
|
16
18
|
from datetime import timedelta
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
17
20
|
from typing import Literal
|
|
18
21
|
|
|
19
|
-
import httpx
|
|
20
|
-
from nat.authentication.interfaces import AuthProviderBase
|
|
21
|
-
from nat.builder.builder import Builder
|
|
22
22
|
from nat.cli.register_workflow import register_function_group
|
|
23
23
|
from nat.data_models.component_ref import AuthenticationRef
|
|
24
24
|
from nat.plugins.mcp.client_base import AuthAdapter
|
|
25
|
-
from nat.plugins.mcp.client_base import MCPSSEClient
|
|
26
|
-
from nat.plugins.mcp.client_base import MCPStdioClient
|
|
27
25
|
from nat.plugins.mcp.client_base import MCPStreamableHTTPClient
|
|
28
26
|
from nat.plugins.mcp.client_config import MCPServerConfig
|
|
29
27
|
from nat.plugins.mcp.client_impl import MCPClientConfig
|
|
30
|
-
from nat.plugins.mcp.client_impl import MCPFunctionGroup
|
|
31
|
-
from nat.plugins.mcp.client_impl import mcp_apply_tool_alias_and_description
|
|
32
|
-
from nat.plugins.mcp.client_impl import mcp_session_tool_function
|
|
33
28
|
from pydantic import Field
|
|
34
29
|
from pydantic import HttpUrl
|
|
35
30
|
|
|
36
|
-
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
import httpx
|
|
33
|
+
from nat.authentication.interfaces import AuthProviderBase
|
|
34
|
+
from nat.builder.builder import Builder
|
|
35
|
+
from nat.plugins.mcp.client_impl import MCPFunctionGroup
|
|
37
36
|
|
|
38
37
|
logger = logging.getLogger(__name__)
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
|
|
40
|
+
def _default_transport() -> Literal["streamable-http", "sse", "stdio"]:
|
|
41
|
+
from datarobot_genai.core.mcp.common import MCPConfig # noqa: PLC0415
|
|
42
|
+
|
|
43
|
+
server_config = MCPConfig().server_config
|
|
44
|
+
return server_config["transport"] if server_config else "stdio"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _default_url() -> HttpUrl | None:
|
|
48
|
+
from datarobot_genai.core.mcp.common import MCPConfig # noqa: PLC0415
|
|
49
|
+
|
|
50
|
+
server_config = MCPConfig().server_config
|
|
51
|
+
return server_config["url"] if server_config else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _default_auth_provider() -> str | AuthenticationRef | None:
|
|
55
|
+
from datarobot_genai.core.mcp.common import MCPConfig # noqa: PLC0415
|
|
56
|
+
|
|
57
|
+
server_config = MCPConfig().server_config
|
|
58
|
+
return "datarobot_mcp_auth" if server_config else None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _default_command() -> str | None:
|
|
62
|
+
from datarobot_genai.core.mcp.common import MCPConfig # noqa: PLC0415
|
|
63
|
+
|
|
64
|
+
server_config = MCPConfig().server_config
|
|
65
|
+
return None if server_config else "docker"
|
|
41
66
|
|
|
42
67
|
|
|
43
68
|
class DataRobotMCPServerConfig(MCPServerConfig):
|
|
44
69
|
transport: Literal["streamable-http", "sse", "stdio"] = Field(
|
|
45
|
-
|
|
70
|
+
default_factory=_default_transport,
|
|
46
71
|
description="Transport type to connect to the MCP server (sse or streamable-http)",
|
|
47
72
|
)
|
|
48
73
|
url: HttpUrl | None = Field(
|
|
49
|
-
|
|
74
|
+
default_factory=_default_url,
|
|
50
75
|
description="URL of the MCP server (for sse or streamable-http transport)",
|
|
51
76
|
)
|
|
52
77
|
# Authentication configuration
|
|
53
78
|
auth_provider: str | AuthenticationRef | None = Field(
|
|
54
|
-
|
|
79
|
+
default_factory=_default_auth_provider,
|
|
55
80
|
description="Reference to authentication provider",
|
|
56
81
|
)
|
|
57
82
|
command: str | None = Field(
|
|
58
|
-
|
|
83
|
+
default_factory=_default_command,
|
|
59
84
|
description="Command to run for stdio transport (e.g. 'python' or 'docker')",
|
|
60
85
|
)
|
|
61
86
|
|
|
62
87
|
|
|
63
88
|
class DataRobotMCPClientConfig(MCPClientConfig, name="datarobot_mcp_client"): # type: ignore[call-arg]
|
|
64
89
|
server: DataRobotMCPServerConfig = Field(
|
|
65
|
-
|
|
90
|
+
default_factory=DataRobotMCPServerConfig,
|
|
91
|
+
description="DataRobot MCP Server configuration",
|
|
66
92
|
)
|
|
67
93
|
|
|
68
94
|
|
|
@@ -128,6 +154,12 @@ async def datarobot_mcp_client_function_group(
|
|
|
128
154
|
Returns:
|
|
129
155
|
The function group
|
|
130
156
|
"""
|
|
157
|
+
from nat.plugins.mcp.client_base import MCPSSEClient # noqa: PLC0415
|
|
158
|
+
from nat.plugins.mcp.client_base import MCPStdioClient # noqa: PLC0415
|
|
159
|
+
from nat.plugins.mcp.client_impl import MCPFunctionGroup # noqa: PLC0415
|
|
160
|
+
from nat.plugins.mcp.client_impl import mcp_apply_tool_alias_and_description # noqa: PLC0415
|
|
161
|
+
from nat.plugins.mcp.client_impl import mcp_session_tool_function # noqa: PLC0415
|
|
162
|
+
|
|
131
163
|
# Resolve auth provider if specified
|
|
132
164
|
auth_provider = None
|
|
133
165
|
if config.server.auth_provider:
|
|
@@ -27,22 +27,21 @@ datarobot_genai/drmcp/server.py,sha256=KE4kjS5f9bfdYftG14HBHrfvxDfCD4pwCXePfvl1O
|
|
|
27
27
|
datarobot_genai/drmcp/core/__init__.py,sha256=y4yapzp3KnFMzSR6HlNDS4uSuyNT7I1iPBvaCLsS0sU,577
|
|
28
28
|
datarobot_genai/drmcp/core/auth.py,sha256=E-5wrGbBFEBlD5377g6Exddrc7HsazamwX8tWr2RLXY,5815
|
|
29
29
|
datarobot_genai/drmcp/core/clients.py,sha256=y-yG8617LbmiZ_L7FWfMrk4WjIekyr76u_Q80aLqGpI,5524
|
|
30
|
-
datarobot_genai/drmcp/core/config.py,sha256=
|
|
30
|
+
datarobot_genai/drmcp/core/config.py,sha256=SWLhVKoqI4vmA-04TFKpKm1_G2yEMEN1e_8cv8d_XRM,13774
|
|
31
31
|
datarobot_genai/drmcp/core/config_utils.py,sha256=U-aieWw7MyP03cGDFIp97JH99ZUfr3vD9uuTzBzxn7w,6428
|
|
32
32
|
datarobot_genai/drmcp/core/constants.py,sha256=lUwoW_PTrbaBGqRJifKqCn3EoFacoEgdO-CpoFVrUoU,739
|
|
33
33
|
datarobot_genai/drmcp/core/credentials.py,sha256=PYEUDNMVw1BoMzZKLkPVTypNkVevEPtmk3scKnE-zYg,6706
|
|
34
|
-
datarobot_genai/drmcp/core/dr_mcp_server.py,sha256=
|
|
34
|
+
datarobot_genai/drmcp/core/dr_mcp_server.py,sha256=czcjbwhZAeW9EtG_Bys0GARPOuQulstkiU7FG48Q9bg,14118
|
|
35
35
|
datarobot_genai/drmcp/core/dr_mcp_server_logo.py,sha256=hib-nfR1SNTW6CnpFsFCkL9H_OMwa4YYyinV7VNOuLk,4708
|
|
36
36
|
datarobot_genai/drmcp/core/exceptions.py,sha256=eqsGI-lxybgvWL5w4BFhbm3XzH1eU5tetwjnhJxelpc,905
|
|
37
37
|
datarobot_genai/drmcp/core/logging.py,sha256=Y_hig4eBWiXGaVV7B_3wBcaYVRNH4ydptbEQhrP9-mY,3414
|
|
38
|
-
datarobot_genai/drmcp/core/mcp_instance.py,sha256=
|
|
39
|
-
datarobot_genai/drmcp/core/mcp_server_tools.py,sha256=odNZKozfx0VV38SLZHw9lY0C0JM_JnRI06W3BBXnyE4,4278
|
|
38
|
+
datarobot_genai/drmcp/core/mcp_instance.py,sha256=nt4gOlAQklMcqmohRIKovYcyhgLdb08NHMo28DBYmOk,18362
|
|
40
39
|
datarobot_genai/drmcp/core/routes.py,sha256=dqE2M0UzAyyN9vQjlyTjYW4rpju3LT039po5weuO__I,17936
|
|
41
40
|
datarobot_genai/drmcp/core/routes_utils.py,sha256=vSseXWlplMSnRgoJgtP_rHxWSAVYcx_tpTv4lyTpQoc,944
|
|
42
41
|
datarobot_genai/drmcp/core/server_life_cycle.py,sha256=WKGJWGxalvqxupzJ2y67Kklc_9PgpZT0uyjlv_sr5wc,3419
|
|
43
42
|
datarobot_genai/drmcp/core/telemetry.py,sha256=NEkSTC1w6uQgtukLHI-sWvR4EMgInysgATcvfQ5CplM,15378
|
|
44
|
-
datarobot_genai/drmcp/core/tool_config.py,sha256=
|
|
45
|
-
datarobot_genai/drmcp/core/tool_filter.py,sha256=
|
|
43
|
+
datarobot_genai/drmcp/core/tool_config.py,sha256=izUdM6dN3GRBzSBs-OagggM2dX5PGBnDbVv4N5bfWFI,3668
|
|
44
|
+
datarobot_genai/drmcp/core/tool_filter.py,sha256=yKQlEtzyIeXGxZJkHbK36QI19vmgQkvqmfx5cTo2pp4,3156
|
|
46
45
|
datarobot_genai/drmcp/core/utils.py,sha256=EvfpqKZ3tECMoxpIQ_tA_3rOgy6KJEYKC0lWZo_Daag,4517
|
|
47
46
|
datarobot_genai/drmcp/core/dynamic_prompts/__init__.py,sha256=y4yapzp3KnFMzSR6HlNDS4uSuyNT7I1iPBvaCLsS0sU,577
|
|
48
47
|
datarobot_genai/drmcp/core/dynamic_prompts/controllers.py,sha256=AGJlKqgHRO0Kd7Gl-Ulw9KYBgzjTTFXWBvOUF-SuKUI,5454
|
|
@@ -73,30 +72,33 @@ datarobot_genai/drmcp/test_utils/mcp_utils_ete.py,sha256=46rH0fYYmUj7ygf968iRbdS
|
|
|
73
72
|
datarobot_genai/drmcp/test_utils/mcp_utils_integration.py,sha256=sHA_BWtpgIAFp9IXiNkUeBartBMjLAauqkV9bYtCr-g,3874
|
|
74
73
|
datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py,sha256=YgyqHK09MB-PBaqT34heqvmvYYFtLpzzSJt7xuTJmDg,11224
|
|
75
74
|
datarobot_genai/drmcp/test_utils/test_interactive.py,sha256=guXvR8q2H6VUdmvIjEJcElQJCC6lQ-oTrzbD2EkHeCs,8025
|
|
76
|
-
datarobot_genai/drmcp/test_utils/tool_base_ete.py,sha256=
|
|
75
|
+
datarobot_genai/drmcp/test_utils/tool_base_ete.py,sha256=3yMfOsz3LdHYywuE5BhdJDpTUowx37HsFSsMdBTxA80,9337
|
|
77
76
|
datarobot_genai/drmcp/test_utils/utils.py,sha256=esGKFv8aO31-Qg3owayeWp32BYe1CdYOEutjjdbweCw,3048
|
|
78
77
|
datarobot_genai/drmcp/tools/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
79
78
|
datarobot_genai/drmcp/tools/clients/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
80
79
|
datarobot_genai/drmcp/tools/clients/atlassian.py,sha256=__M_uz7FrcbKCYRzeMn24DCEYD6OmFx_LuywHCxgXsA,6472
|
|
81
80
|
datarobot_genai/drmcp/tools/clients/confluence.py,sha256=h_G0By_kDnJeWDT_d-IREsaZ5-0xB5GoLXOqblYP5MA,20706
|
|
82
|
-
datarobot_genai/drmcp/tools/clients/gdrive.py,sha256=
|
|
81
|
+
datarobot_genai/drmcp/tools/clients/gdrive.py,sha256=8GztWTdpJ7Ir3NIvIoOHPzDscoR1Ui7Ct2IiKmuUzIc,26012
|
|
83
82
|
datarobot_genai/drmcp/tools/clients/jira.py,sha256=Rm91JAyrNIqxu66-9rU1YqoRXVnWbEy-Ahvy6f6HlVg,9823
|
|
83
|
+
datarobot_genai/drmcp/tools/clients/microsoft_graph.py,sha256=PASGThDPE8zkBZqach8lurJL1y47DWUPLwvf9N6uLGM,19234
|
|
84
84
|
datarobot_genai/drmcp/tools/clients/s3.py,sha256=GmwzvurFdNfvxOooA8g5S4osRysHYU0S9ypg_177Glg,953
|
|
85
85
|
datarobot_genai/drmcp/tools/confluence/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
86
86
|
datarobot_genai/drmcp/tools/confluence/tools.py,sha256=_-ws65WLK8KZP_mKkf4yJ7ZunR8qdyoiMwHQX47MSMw,12362
|
|
87
87
|
datarobot_genai/drmcp/tools/gdrive/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
88
|
-
datarobot_genai/drmcp/tools/gdrive/tools.py,sha256=
|
|
88
|
+
datarobot_genai/drmcp/tools/gdrive/tools.py,sha256=G8LlnGEINZqV83Q-b3ZliWkDjouhbozDam3w6GfA7s0,10711
|
|
89
89
|
datarobot_genai/drmcp/tools/jira/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
90
90
|
datarobot_genai/drmcp/tools/jira/tools.py,sha256=dfkqTU2HH-7n44hX80ODFacKq0p0LOchFcZtIIKFNMM,9687
|
|
91
|
+
datarobot_genai/drmcp/tools/microsoft_graph/__init__.py,sha256=CuOaMt1AJo7cHx_GuhO3s_aqxZas_wlDsoBorBsvbeU,577
|
|
92
|
+
datarobot_genai/drmcp/tools/microsoft_graph/tools.py,sha256=zJ-UA1TMhPOYcExvgWv0YBjDsSIDPA-U1SEbBrVfAc8,7744
|
|
91
93
|
datarobot_genai/drmcp/tools/predictive/__init__.py,sha256=WuOHlNNEpEmcF7gVnhckruJRKU2qtmJLE3E7zoCGLDo,1030
|
|
92
|
-
datarobot_genai/drmcp/tools/predictive/data.py,sha256=
|
|
94
|
+
datarobot_genai/drmcp/tools/predictive/data.py,sha256=sSFAmO6x0DSuolw8urhMaOj5PwfUH29oc2mEOZI3YU4,4631
|
|
93
95
|
datarobot_genai/drmcp/tools/predictive/deployment.py,sha256=lm02Ayuo11L1hP41fgi3QpR1Eyty-Wc16rM0c8SgliM,3277
|
|
94
96
|
datarobot_genai/drmcp/tools/predictive/deployment_info.py,sha256=BGEF_dmbxOBJR0n1Tt9TO2-iNTQSBTr-oQUyaxLZ0ZI,15297
|
|
95
97
|
datarobot_genai/drmcp/tools/predictive/model.py,sha256=Yih5-KedJ-1yupPLXCJsCXOdyWWi9pRvgapXDlgXWJA,4891
|
|
96
98
|
datarobot_genai/drmcp/tools/predictive/predict.py,sha256=Qoob2_t2crfWtyPzkXMRz2ITZumnczU6Dq4C7q9RBMI,9370
|
|
97
99
|
datarobot_genai/drmcp/tools/predictive/predict_realtime.py,sha256=urq6rPyZFsAP-bPyclSNzrkvb6FTamdlFau8q0IWWJ0,13472
|
|
98
|
-
datarobot_genai/drmcp/tools/predictive/project.py,sha256=
|
|
99
|
-
datarobot_genai/drmcp/tools/predictive/training.py,sha256=
|
|
100
|
+
datarobot_genai/drmcp/tools/predictive/project.py,sha256=xC52UdYvuFeNZC7Y5MfXcvzTL70WwAacQXESr6rqN6s,3255
|
|
101
|
+
datarobot_genai/drmcp/tools/predictive/training.py,sha256=LzMxbBT8wxKYDrRlVElfmTUrzpmGvwrR-mTGf6YUnIA,23998
|
|
100
102
|
datarobot_genai/langgraph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
101
103
|
datarobot_genai/langgraph/agent.py,sha256=DRnywmS9KDywyChtuIZZwNKbJs8BpC259EG_kxYbiQ8,15828
|
|
102
104
|
datarobot_genai/langgraph/mcp.py,sha256=iA2_j46mZAaNaL7ntXT-LW6C-NMJkzr3VfKDDfe7mh8,2851
|
|
@@ -107,13 +109,13 @@ datarobot_genai/llama_index/mcp.py,sha256=leXqF1C4zhuYEKFwNEfZHY4dsUuGZk3W7KArY-
|
|
|
107
109
|
datarobot_genai/nat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
108
110
|
datarobot_genai/nat/agent.py,sha256=DuGrgqt1FzvAE-cRH_P3LTFUlwuClvbVurdwA-RsbuY,11177
|
|
109
111
|
datarobot_genai/nat/datarobot_auth_provider.py,sha256=Z4NSsrHxK8hUeiqtK_lryHsUuZC74ziNo_FHbsZgtiM,4230
|
|
110
|
-
datarobot_genai/nat/datarobot_llm_clients.py,sha256
|
|
112
|
+
datarobot_genai/nat/datarobot_llm_clients.py,sha256=-_q_KlKOVQecIYJd8YRiYnS4ZNazQAiAdZBE1Zip_wQ,12684
|
|
111
113
|
datarobot_genai/nat/datarobot_llm_providers.py,sha256=aDoQcTeGI-odqydPXEX9OGGNFbzAtpqzTvHHEkmJuEQ,4963
|
|
112
|
-
datarobot_genai/nat/datarobot_mcp_client.py,sha256=
|
|
114
|
+
datarobot_genai/nat/datarobot_mcp_client.py,sha256=jL8sXb8g4gvt0VYgB2tfMGsMjpB1GV2XIbN0iv_LxVU,10701
|
|
113
115
|
datarobot_genai/nat/helpers.py,sha256=Q7E3ADZdtFfS8E6OQPyw2wgA6laQ58N3bhLj5CBWwJs,3265
|
|
114
|
-
datarobot_genai-0.2.
|
|
115
|
-
datarobot_genai-0.2.
|
|
116
|
-
datarobot_genai-0.2.
|
|
117
|
-
datarobot_genai-0.2.
|
|
118
|
-
datarobot_genai-0.2.
|
|
119
|
-
datarobot_genai-0.2.
|
|
116
|
+
datarobot_genai-0.2.29.dist-info/METADATA,sha256=lFk85PaEbHw_waws2INyPqFxo92s43jFp4vFyz8HHdc,6301
|
|
117
|
+
datarobot_genai-0.2.29.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
118
|
+
datarobot_genai-0.2.29.dist-info/entry_points.txt,sha256=jEW3WxDZ8XIK9-ISmTyt5DbmBb047rFlzQuhY09rGrM,284
|
|
119
|
+
datarobot_genai-0.2.29.dist-info/licenses/AUTHORS,sha256=isJGUXdjq1U7XZ_B_9AH8Qf0u4eX0XyQifJZ_Sxm4sA,80
|
|
120
|
+
datarobot_genai-0.2.29.dist-info/licenses/LICENSE,sha256=U2_VkLIktQoa60Nf6Tbt7E4RMlfhFSjWjcJJfVC-YCE,11341
|
|
121
|
+
datarobot_genai-0.2.29.dist-info/RECORD,,
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
# Copyright 2025 DataRobot, Inc.
|
|
2
|
-
#
|
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
# you may not use this file except in compliance with the License.
|
|
5
|
-
# You may obtain a copy of the License at
|
|
6
|
-
#
|
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
#
|
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
14
|
-
|
|
15
|
-
import logging
|
|
16
|
-
|
|
17
|
-
from .mcp_instance import dr_core_mcp_tool
|
|
18
|
-
from .mcp_instance import mcp
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dr_core_mcp_tool(tags={"mcp_server_tools", "metadata"})
|
|
24
|
-
async def get_all_available_tags() -> str:
|
|
25
|
-
"""
|
|
26
|
-
List all unique tags from all registered tools.
|
|
27
|
-
|
|
28
|
-
Returns
|
|
29
|
-
-------
|
|
30
|
-
A string containing all available tags, one per line.
|
|
31
|
-
"""
|
|
32
|
-
tags = await mcp.get_all_tags()
|
|
33
|
-
if not tags:
|
|
34
|
-
return "No tags found in any tools."
|
|
35
|
-
|
|
36
|
-
return "\n".join(sorted(tags))
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@dr_core_mcp_tool(tags={"mcp_server_tools", "metadata", "discovery"})
|
|
40
|
-
async def list_tools_by_tags(tags: list[str] | None = None, match_all: bool = False) -> str:
|
|
41
|
-
"""
|
|
42
|
-
List tools filtered by tags.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
tags: Optional list of tags to filter by. If None, returns all tools.
|
|
46
|
-
match_all: If True, tool must have all specified tags (AND logic).
|
|
47
|
-
If False, tool must have at least one tag (OR logic).
|
|
48
|
-
Only used when tags is provided.
|
|
49
|
-
|
|
50
|
-
Returns
|
|
51
|
-
-------
|
|
52
|
-
A formatted string listing tools that match the tag criteria.
|
|
53
|
-
"""
|
|
54
|
-
tools = await mcp.list_tools(tags=tags, match_all=match_all)
|
|
55
|
-
|
|
56
|
-
if not tools:
|
|
57
|
-
if tags:
|
|
58
|
-
logic = "all" if match_all else "any"
|
|
59
|
-
return f"No tools found with {logic} of the tags: {', '.join(tags)}"
|
|
60
|
-
else:
|
|
61
|
-
return "No tools found."
|
|
62
|
-
|
|
63
|
-
result = []
|
|
64
|
-
if tags:
|
|
65
|
-
logic = "all" if match_all else "any"
|
|
66
|
-
result.append(f"Tools with {logic} of the tags: {', '.join(tags)}")
|
|
67
|
-
else:
|
|
68
|
-
result.append("All available tools:")
|
|
69
|
-
|
|
70
|
-
result.append("")
|
|
71
|
-
|
|
72
|
-
for i, tool in enumerate(tools, 1):
|
|
73
|
-
tool_tags = []
|
|
74
|
-
if tool.annotations and hasattr(tool.annotations, "extra") and tool.annotations.extra:
|
|
75
|
-
tool_tags = tool.annotations.extra.get("tags", [])
|
|
76
|
-
|
|
77
|
-
result.append(f"{i}. {tool.name}")
|
|
78
|
-
result.append(f" Description: {tool.description}")
|
|
79
|
-
if tool_tags:
|
|
80
|
-
result.append(f" Tags: {', '.join(tool_tags)}")
|
|
81
|
-
result.append("")
|
|
82
|
-
|
|
83
|
-
return "\n".join(result)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@dr_core_mcp_tool(tags={"mcp_server_tools", "metadata", "discovery"})
|
|
87
|
-
async def get_tool_info_by_name(tool_name: str) -> str:
|
|
88
|
-
"""
|
|
89
|
-
Get detailed information about a specific tool by name.
|
|
90
|
-
|
|
91
|
-
Args:
|
|
92
|
-
tool_name: The name of the tool to get information about.
|
|
93
|
-
|
|
94
|
-
Returns
|
|
95
|
-
-------
|
|
96
|
-
A formatted string with detailed information about the tool.
|
|
97
|
-
"""
|
|
98
|
-
all_tools = await mcp.list_tools()
|
|
99
|
-
|
|
100
|
-
for tool in all_tools:
|
|
101
|
-
if tool.name == tool_name:
|
|
102
|
-
result = [f"Tool: {tool.name}"]
|
|
103
|
-
result.append(f"Description: {tool.description}")
|
|
104
|
-
|
|
105
|
-
# Get tags
|
|
106
|
-
tool_tags = []
|
|
107
|
-
if tool.annotations and hasattr(tool.annotations, "extra") and tool.annotations.extra:
|
|
108
|
-
tool_tags = tool.annotations.extra.get("tags", [])
|
|
109
|
-
|
|
110
|
-
if tool_tags:
|
|
111
|
-
result.append(f"Tags: {', '.join(tool_tags)}")
|
|
112
|
-
else:
|
|
113
|
-
result.append("Tags: None")
|
|
114
|
-
|
|
115
|
-
# Get input schema info
|
|
116
|
-
if (
|
|
117
|
-
tool.inputSchema
|
|
118
|
-
and hasattr(tool.inputSchema, "properties")
|
|
119
|
-
and tool.inputSchema.properties
|
|
120
|
-
):
|
|
121
|
-
result.append("Parameters:")
|
|
122
|
-
for param_name, param_info in tool.inputSchema.properties.items():
|
|
123
|
-
param_type = param_info.get("type", "unknown")
|
|
124
|
-
param_desc = param_info.get("description", "No description")
|
|
125
|
-
result.append(f" - {param_name} ({param_type}): {param_desc}")
|
|
126
|
-
|
|
127
|
-
return "\n".join(result)
|
|
128
|
-
|
|
129
|
-
return f"Tool '{tool_name}' not found."
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|