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,130 @@
|
|
|
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 fastmcp.prompts.prompt import Prompt
|
|
18
|
+
|
|
19
|
+
from datarobot_genai.drmcp.core.dynamic_prompts.dr_lib import get_datarobot_prompt_template
|
|
20
|
+
from datarobot_genai.drmcp.core.dynamic_prompts.dr_lib import get_datarobot_prompt_template_version
|
|
21
|
+
from datarobot_genai.drmcp.core.dynamic_prompts.dr_lib import get_datarobot_prompt_template_versions
|
|
22
|
+
from datarobot_genai.drmcp.core.dynamic_prompts.dr_lib import get_datarobot_prompt_templates
|
|
23
|
+
from datarobot_genai.drmcp.core.dynamic_prompts.register import (
|
|
24
|
+
register_prompt_from_datarobot_prompt_management,
|
|
25
|
+
)
|
|
26
|
+
from datarobot_genai.drmcp.core.exceptions import DynamicPromptRegistrationError
|
|
27
|
+
from datarobot_genai.drmcp.core.mcp_instance import mcp
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def register_prompt_from_prompt_template_id_and_version(
|
|
33
|
+
prompt_template_id: str, prompt_template_version_id: str | None
|
|
34
|
+
) -> Prompt:
|
|
35
|
+
"""Register a Prompt for a specific prompt template ID and version.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
prompt_template_id: The ID of the DataRobot prompt template.
|
|
39
|
+
prompt_template_version_id: Optional ID of the DataRobot prompt template version.
|
|
40
|
+
If not provided latest will be used
|
|
41
|
+
|
|
42
|
+
Raises
|
|
43
|
+
------
|
|
44
|
+
DynamicPromptRegistrationError: If registration fails at any step.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
The registered Prompt instance.
|
|
49
|
+
"""
|
|
50
|
+
prompt_template = get_datarobot_prompt_template(prompt_template_id)
|
|
51
|
+
|
|
52
|
+
if not prompt_template:
|
|
53
|
+
raise DynamicPromptRegistrationError("Registration failed. Could not find prompt template.")
|
|
54
|
+
|
|
55
|
+
if not prompt_template_version_id:
|
|
56
|
+
return await register_prompt_from_datarobot_prompt_management(
|
|
57
|
+
prompt_template=prompt_template
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
prompt_template_version = get_datarobot_prompt_template_version(
|
|
61
|
+
prompt_template_id, prompt_template_version_id
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if not prompt_template_version:
|
|
65
|
+
raise DynamicPromptRegistrationError(
|
|
66
|
+
"Registration failed. Could not find prompt template version."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return await register_prompt_from_datarobot_prompt_management(
|
|
70
|
+
prompt_template=prompt_template, prompt_template_version=prompt_template_version
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def delete_registered_prompt_template(prompt_template_id: str) -> bool:
|
|
75
|
+
"""Delete the prompt registered for the prompt template id in the MCP instance."""
|
|
76
|
+
prompt_templates_mappings = await mcp.get_prompt_mapping()
|
|
77
|
+
if prompt_template_id not in prompt_templates_mappings:
|
|
78
|
+
logger.debug(f"No prompt registered for prompt template id {prompt_template_id}")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
prompt_template_version_id, prompt_name = prompt_templates_mappings[prompt_template_id]
|
|
82
|
+
await mcp.remove_prompt_mapping(prompt_template_id, prompt_template_version_id)
|
|
83
|
+
logger.info(
|
|
84
|
+
f"Deleted prompt name {prompt_name} for prompt template id {prompt_template_id}, "
|
|
85
|
+
f"version {prompt_template_version_id}"
|
|
86
|
+
)
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def refresh_registered_prompt_template() -> None:
|
|
91
|
+
"""Refresh all registered prompt templates in the MCP instance."""
|
|
92
|
+
prompt_templates = get_datarobot_prompt_templates()
|
|
93
|
+
prompt_templates_ids = {p.id for p in prompt_templates}
|
|
94
|
+
prompt_templates_versions = get_datarobot_prompt_template_versions(list(prompt_templates_ids))
|
|
95
|
+
|
|
96
|
+
mcp_prompt_templates_mappings = await mcp.get_prompt_mapping()
|
|
97
|
+
|
|
98
|
+
for prompt_template in prompt_templates:
|
|
99
|
+
prompt_template_versions = prompt_templates_versions.get(prompt_template.id)
|
|
100
|
+
if not prompt_template_versions:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
latest_version = max(prompt_template_versions, key=lambda v: v.version)
|
|
104
|
+
|
|
105
|
+
if prompt_template.id not in mcp_prompt_templates_mappings:
|
|
106
|
+
# New prompt template -> add
|
|
107
|
+
await register_prompt_from_datarobot_prompt_management(
|
|
108
|
+
prompt_template=prompt_template, prompt_template_version=latest_version
|
|
109
|
+
)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
mcp_prompt_template_version, mcp_prompt = mcp_prompt_templates_mappings[prompt_template.id]
|
|
113
|
+
|
|
114
|
+
if mcp_prompt_template_version != latest_version:
|
|
115
|
+
# Current version saved in MCP is not the latest one => update it
|
|
116
|
+
await register_prompt_from_datarobot_prompt_management(
|
|
117
|
+
prompt_template=prompt_template, prompt_template_version=latest_version
|
|
118
|
+
)
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# Else => mcp_prompt_template_version == latest_version
|
|
122
|
+
# For now it means nothing changed as there's no possibility to edit promp template version.
|
|
123
|
+
|
|
124
|
+
for mcp_prompt_template_id, (
|
|
125
|
+
mcp_prompt_template_version_id,
|
|
126
|
+
_,
|
|
127
|
+
) in mcp_prompt_templates_mappings.items():
|
|
128
|
+
if mcp_prompt_template_id not in prompt_templates_ids:
|
|
129
|
+
# We need to also delete prompt templates that are
|
|
130
|
+
await mcp.remove_prompt_mapping(mcp_prompt_template_id, mcp_prompt_template_version_id)
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
from collections import defaultdict
|
|
15
|
+
|
|
16
|
+
import datarobot as dr
|
|
17
|
+
|
|
18
|
+
from datarobot_genai.drmcp.core.clients import get_api_client
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_datarobot_prompt_templates() -> list[dr.genai.PromptTemplate]:
|
|
22
|
+
try:
|
|
23
|
+
return dr.genai.PromptTemplate.list()
|
|
24
|
+
except Exception:
|
|
25
|
+
return []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_datarobot_prompt_template_versions(
|
|
29
|
+
prompt_template_ids: list[str],
|
|
30
|
+
) -> dict[str, list[dr.genai.PromptTemplateVersion]]:
|
|
31
|
+
# Still missing in SDK
|
|
32
|
+
prompt_template_versions_data = dr.utils.pagination.unpaginate(
|
|
33
|
+
initial_url="genai/promptTemplates/versions/",
|
|
34
|
+
initial_params={
|
|
35
|
+
"promptTemplateIds": prompt_template_ids,
|
|
36
|
+
},
|
|
37
|
+
client=get_api_client(),
|
|
38
|
+
)
|
|
39
|
+
prompt_template_versions = defaultdict(list)
|
|
40
|
+
for prompt_template_version in prompt_template_versions_data:
|
|
41
|
+
prompt_template_versions[prompt_template_version["promptTemplateId"]].append(
|
|
42
|
+
dr.genai.PromptTemplateVersion(
|
|
43
|
+
id=prompt_template_version["id"],
|
|
44
|
+
prompt_template_id=prompt_template_version["promptTemplateId"],
|
|
45
|
+
prompt_text=prompt_template_version["promptText"],
|
|
46
|
+
commit_comment=prompt_template_version["commitComment"],
|
|
47
|
+
version=prompt_template_version["version"],
|
|
48
|
+
variables=prompt_template_version["variables"],
|
|
49
|
+
creation_date=prompt_template_version["creationDate"],
|
|
50
|
+
creation_user_id=prompt_template_version["creationUserId"],
|
|
51
|
+
user_name=prompt_template_version["userName"],
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
return prompt_template_versions
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_datarobot_prompt_template(prompt_template_id: str) -> dr.genai.PromptTemplate | None:
|
|
58
|
+
try:
|
|
59
|
+
return dr.genai.PromptTemplate.get(prompt_template_id)
|
|
60
|
+
except Exception:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_datarobot_prompt_template_version(
|
|
65
|
+
prompt_template_id: str, prompt_template_version_id: str
|
|
66
|
+
) -> dr.genai.PromptTemplateVersion | None:
|
|
67
|
+
try:
|
|
68
|
+
return dr.genai.PromptTemplateVersion.get(prompt_template_id, prompt_template_version_id)
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
import keyword
|
|
15
|
+
import logging
|
|
16
|
+
import re
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from inspect import Parameter
|
|
19
|
+
from inspect import Signature
|
|
20
|
+
|
|
21
|
+
import datarobot as dr
|
|
22
|
+
from fastmcp.prompts.prompt import Prompt
|
|
23
|
+
from pydantic import Field
|
|
24
|
+
|
|
25
|
+
from datarobot_genai.drmcp.core.exceptions import DynamicPromptRegistrationError
|
|
26
|
+
from datarobot_genai.drmcp.core.mcp_instance import register_prompt
|
|
27
|
+
|
|
28
|
+
from .dr_lib import get_datarobot_prompt_template_versions
|
|
29
|
+
from .dr_lib import get_datarobot_prompt_templates
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def register_prompts_from_datarobot_prompt_management() -> None:
|
|
35
|
+
"""Register prompts from DataRobot Prompt Management."""
|
|
36
|
+
prompts = get_datarobot_prompt_templates()
|
|
37
|
+
logger.info(f"Found {len(prompts)} prompts in Prompts Management.")
|
|
38
|
+
all_prompts_versions = get_datarobot_prompt_template_versions(
|
|
39
|
+
prompt_template_ids=list({prompt.id for prompt in prompts})
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Try to register each prompt, continue on failure
|
|
43
|
+
for prompt in prompts:
|
|
44
|
+
prompt_versions = all_prompts_versions.get(prompt.id)
|
|
45
|
+
if not prompt_versions:
|
|
46
|
+
logger.warning(f"Prompt template id {prompt.id} has no versions.")
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
latest_version = max(prompt_versions, key=lambda v: v.version)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
await register_prompt_from_datarobot_prompt_management(prompt, latest_version)
|
|
53
|
+
except DynamicPromptRegistrationError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def register_prompt_from_datarobot_prompt_management(
|
|
58
|
+
prompt_template: dr.genai.PromptTemplate,
|
|
59
|
+
prompt_template_version: dr.genai.PromptTemplateVersion | None = None,
|
|
60
|
+
) -> Prompt:
|
|
61
|
+
"""Register a single prompt.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
prompt_template: The prompt within DataRobot Prompt Management.
|
|
65
|
+
prompt_template_version: Optional prompt version within DataRobot Prompt Management.
|
|
66
|
+
If not provided -- latest version will be used
|
|
67
|
+
|
|
68
|
+
Raises
|
|
69
|
+
------
|
|
70
|
+
DynamicPromptRegistrationError: If registration fails at any step.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
The registered Prompt instance.
|
|
75
|
+
"""
|
|
76
|
+
if not prompt_template_version:
|
|
77
|
+
prompt_template_version_to_register = prompt_template.get_latest_version()
|
|
78
|
+
|
|
79
|
+
if prompt_template_version_to_register is None:
|
|
80
|
+
logger.info(
|
|
81
|
+
f"No latest version in Prompts Management for prompt id: {prompt_template.id}"
|
|
82
|
+
)
|
|
83
|
+
raise DynamicPromptRegistrationError
|
|
84
|
+
|
|
85
|
+
else:
|
|
86
|
+
prompt_template_version_to_register = prompt_template_version
|
|
87
|
+
|
|
88
|
+
logger.info(
|
|
89
|
+
f"Found prompt: id: {prompt_template.id}, "
|
|
90
|
+
f"name: {prompt_template.name}, "
|
|
91
|
+
f"prompt version id: {prompt_template_version_to_register.id}, "
|
|
92
|
+
f"version: {prompt_template_version_to_register.version}."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
valid_fn_name = to_valid_mcp_prompt_name(prompt_template.name)
|
|
97
|
+
except ValueError as e:
|
|
98
|
+
raise DynamicPromptRegistrationError from e
|
|
99
|
+
|
|
100
|
+
prompt_fn = make_prompt_function(
|
|
101
|
+
name=valid_fn_name,
|
|
102
|
+
description=prompt_template.description,
|
|
103
|
+
prompt_text=prompt_template_version_to_register.prompt_text,
|
|
104
|
+
variables=prompt_template_version_to_register.variables,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
# Register using generic external tool registration with the config
|
|
109
|
+
return await register_prompt(
|
|
110
|
+
fn=prompt_fn,
|
|
111
|
+
name=prompt_template.name,
|
|
112
|
+
description=prompt_template.description,
|
|
113
|
+
meta={
|
|
114
|
+
"prompt_template_id": prompt_template.id,
|
|
115
|
+
"prompt_template_version_id": prompt_template_version_to_register.id,
|
|
116
|
+
},
|
|
117
|
+
prompt_template=(prompt_template.id, prompt_template_version_to_register.id),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
logger.error(f"Skipping prompt {prompt_template.id}. Registration failed: {exc}")
|
|
122
|
+
raise DynamicPromptRegistrationError(
|
|
123
|
+
"Registration failed. Could not create prompt."
|
|
124
|
+
) from exc
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _escape_non_ascii(s: str) -> str:
|
|
128
|
+
out = []
|
|
129
|
+
for ch in s:
|
|
130
|
+
# If its space -> change to underscore
|
|
131
|
+
if ch.isspace():
|
|
132
|
+
out.append("_")
|
|
133
|
+
# ASCII letter, digit or underscore -> keep
|
|
134
|
+
elif ch.isascii() and (ch.isalnum() or ch == "_"):
|
|
135
|
+
out.append(ch)
|
|
136
|
+
# Everything else -> encode as 'xHEX'
|
|
137
|
+
else:
|
|
138
|
+
out.append(f"x{ord(ch):x}")
|
|
139
|
+
return "".join(out)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def to_valid_mcp_prompt_name(s: str) -> str:
|
|
143
|
+
"""Convert an arbitrary string into a valid MCP prompt name."""
|
|
144
|
+
# If its ONLY numbers return "prompt_[number]"
|
|
145
|
+
if s.isdigit():
|
|
146
|
+
return f"prompt_{s}"
|
|
147
|
+
|
|
148
|
+
# First, ASCII-transliterate using hex escape for non-ASCII
|
|
149
|
+
if not s.isascii():
|
|
150
|
+
# whole string non-ascii? -> escape and prefix with prompt_
|
|
151
|
+
encoded = _escape_non_ascii(s)
|
|
152
|
+
return f"prompt_{encoded}"
|
|
153
|
+
|
|
154
|
+
# Replace any sequence of invalid characters with '_'
|
|
155
|
+
s = re.sub(r"[^0-9a-zA-Z_]+", "_", s)
|
|
156
|
+
|
|
157
|
+
# Remove leading characters that are not letters or underscores (can't start with a digit or _)
|
|
158
|
+
s = re.sub(r"^[^a-zA-Z]+", "", s)
|
|
159
|
+
|
|
160
|
+
# Remove following _
|
|
161
|
+
s = re.sub(r"_+$", "", s)
|
|
162
|
+
|
|
163
|
+
# If string is empty after cleaning, raise error
|
|
164
|
+
if not s:
|
|
165
|
+
raise ValueError(f"Cannot convert {s} to valid MCP prompt name.")
|
|
166
|
+
|
|
167
|
+
# Make sure it's a valid identifier and not a reserved keyword
|
|
168
|
+
if keyword.iskeyword(s) or not s.isidentifier():
|
|
169
|
+
s = f"{s}_prompt"
|
|
170
|
+
|
|
171
|
+
return s
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def make_prompt_function(
|
|
175
|
+
name: str, description: str, prompt_text: str, variables: list[dr.genai.Variable]
|
|
176
|
+
) -> Callable:
|
|
177
|
+
params = []
|
|
178
|
+
for v in variables:
|
|
179
|
+
if keyword.iskeyword(v.name):
|
|
180
|
+
raise ValueError(f"Variable name '{v.name}' is invalid.")
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
param = Parameter(
|
|
184
|
+
name=v.name,
|
|
185
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
186
|
+
default=Field(description=v.description),
|
|
187
|
+
)
|
|
188
|
+
except ValueError as e:
|
|
189
|
+
raise ValueError(f"Variable name '{v.name}' is invalid.") from e
|
|
190
|
+
|
|
191
|
+
params.append(param)
|
|
192
|
+
|
|
193
|
+
async def template_function(**kwargs) -> str: # type: ignore
|
|
194
|
+
prompt_text_correct = prompt_text.replace("{{", "{").replace("}}", "}")
|
|
195
|
+
try:
|
|
196
|
+
return prompt_text_correct.format(**kwargs)
|
|
197
|
+
except KeyError as exc:
|
|
198
|
+
raise ValueError(f"Missing variable {exc.args[0]} for prompt '{name}'") from exc
|
|
199
|
+
|
|
200
|
+
# Apply metadata
|
|
201
|
+
template_function.__name__ = name
|
|
202
|
+
template_function.__doc__ = description
|
|
203
|
+
template_function.__signature__ = Signature(params) # type: ignore
|
|
204
|
+
|
|
205
|
+
return template_function
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
from fastmcp import FastMCP
|
|
17
|
+
from fastmcp.exceptions import NotFoundError
|
|
18
|
+
|
|
19
|
+
_SUFFIX_LENGTH: int = 4
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def get_prompt_name_no_duplicate(mcp: FastMCP, prompt_name: str) -> str:
|
|
23
|
+
"""Handle prompt name duplicate.
|
|
24
|
+
|
|
25
|
+
We're working optimistic here -- we're keeping default names unless there's collision
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
prompt = await mcp.get_prompt(prompt_name)
|
|
29
|
+
except NotFoundError:
|
|
30
|
+
return prompt_name
|
|
31
|
+
|
|
32
|
+
prompt_name_suffix = str(uuid4())[:_SUFFIX_LENGTH]
|
|
33
|
+
return f"{prompt.name} ({prompt_name_suffix})"
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
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 abc import ABC
|
|
16
|
+
from abc import abstractmethod
|
|
17
|
+
from typing import Any
|
|
18
|
+
from typing import Literal
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MetadataBase(ABC):
|
|
22
|
+
@property
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
"""The name of the tool, for the LLM to identify and call the tool."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def description(self) -> str:
|
|
31
|
+
"""The description of the tool, for the LLM to understand its purpose
|
|
32
|
+
and all additional instructions and context about the tool, which can
|
|
33
|
+
help the LLM to better utilize the tool in the right context.
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def method(self) -> Literal["GET", "POST", "PATCH", "PUT", "DELETE"]:
|
|
40
|
+
"""HTTP method to use when calling the tool, e.g. `POST` or `GET` etc."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def endpoint(self) -> str:
|
|
46
|
+
"""The endpoint path of the tool, e.g. `/weather/{city}/forecast`."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def input_schema(self) -> dict[str, Any]:
|
|
52
|
+
"""The JSON schema defining the input parameters for the tool.
|
|
53
|
+
|
|
54
|
+
Structure:
|
|
55
|
+
{
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"path_params": {...}, # Optional: path parameter schemas
|
|
59
|
+
"query_params": {...}, # Optional: query parameter schemas
|
|
60
|
+
"data": {...}, # Optional: form/body data schema
|
|
61
|
+
"json": {...} # Optional: JSON body schema
|
|
62
|
+
},
|
|
63
|
+
"required": [...] # Optional: required properties
|
|
64
|
+
}
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def headers(self) -> dict[str, str]:
|
|
71
|
+
"""Optional HTTP headers to include when calling the tool."""
|
|
72
|
+
pass
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
from typing import Any
|
|
15
|
+
from typing import Literal
|
|
16
|
+
from typing import cast
|
|
17
|
+
|
|
18
|
+
from .base import MetadataBase
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Metadata(MetadataBase):
|
|
22
|
+
"""Default adapter for external deployment metadata."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, metadata: dict[str, Any]) -> None:
|
|
25
|
+
self.metadata = metadata
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def name(self) -> str:
|
|
29
|
+
return str(self.metadata.get("name", ""))
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def description(self) -> str:
|
|
33
|
+
return str(self.metadata.get("description", ""))
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def base_url(self) -> str:
|
|
37
|
+
base_url = self.metadata.get("base_url")
|
|
38
|
+
if not base_url or not isinstance(base_url, str):
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"Deployment missing required 'base_url' field in /info/ metadata. "
|
|
41
|
+
"This field is required for MCP Server to route tool requests correctly."
|
|
42
|
+
)
|
|
43
|
+
return str(base_url)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def endpoint(self) -> str:
|
|
47
|
+
endpoint = self.metadata.get("endpoint")
|
|
48
|
+
if not endpoint or not isinstance(endpoint, str):
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"Deployment missing required 'endpoint' field in /info/ metadata. "
|
|
51
|
+
"This field is required for MCP Server to route tool requests correctly."
|
|
52
|
+
)
|
|
53
|
+
return str(endpoint)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def input_schema(self) -> dict[str, Any]:
|
|
57
|
+
input_schema = self.metadata.get("input_schema")
|
|
58
|
+
if not input_schema or not isinstance(input_schema, dict):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"Deployment missing required 'inputSchema' field in /info/ metadata. "
|
|
61
|
+
"This field is required for MCP Server to route tool requests correctly."
|
|
62
|
+
)
|
|
63
|
+
return dict(input_schema)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def method(self) -> Literal["GET", "POST", "PATCH", "PUT", "DELETE"]:
|
|
67
|
+
method = self.metadata.get("method", "").upper()
|
|
68
|
+
if not method or not isinstance(method, str):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"Deployment missing required 'method' field in /info/ metadata. "
|
|
71
|
+
"This field is required for MCP Server to route tool requests correctly."
|
|
72
|
+
)
|
|
73
|
+
if method not in ("GET", "POST", "PATCH", "PUT", "DELETE"):
|
|
74
|
+
raise ValueError(f"Deployment metadata is invalid, unsupported `method`: {method}.")
|
|
75
|
+
return cast(Literal["GET", "POST", "PATCH", "PUT", "DELETE"], method)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def headers(self) -> dict[str, str]:
|
|
79
|
+
headers = self.metadata.get("headers", {})
|
|
80
|
+
if not isinstance(headers, dict):
|
|
81
|
+
raise ValueError("Deployment metadata 'headers' field must be a dictionary.")
|
|
82
|
+
return dict(headers)
|