datarobot-genai 0.2.0__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 +250 -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 +316 -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 +128 -0
- datarobot_genai/drmcp/core/dynamic_prompts/register.py +206 -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 +542 -0
- datarobot_genai/drmcp/core/mcp_server_tools.py +129 -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 +436 -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_filter.py +108 -0
- datarobot_genai/drmcp/core/utils.py +131 -0
- datarobot_genai/drmcp/server.py +19 -0
- datarobot_genai/drmcp/test_utils/__init__.py +13 -0
- datarobot_genai/drmcp/test_utils/integration_mcp_server.py +102 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +96 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +94 -0
- datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py +234 -0
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +151 -0
- datarobot_genai/drmcp/test_utils/utils.py +91 -0
- datarobot_genai/drmcp/tools/__init__.py +14 -0
- datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
- datarobot_genai/drmcp/tools/predictive/data.py +97 -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 +72 -0
- datarobot_genai/drmcp/tools/predictive/training.py +651 -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 +258 -0
- datarobot_genai/nat/datarobot_llm_clients.py +249 -0
- datarobot_genai/nat/datarobot_llm_providers.py +130 -0
- datarobot_genai/py.typed +0 -0
- datarobot_genai-0.2.0.dist-info/METADATA +139 -0
- datarobot_genai-0.2.0.dist-info/RECORD +101 -0
- datarobot_genai-0.2.0.dist-info/WHEEL +4 -0
- datarobot_genai-0.2.0.dist-info/entry_points.txt +3 -0
- datarobot_genai-0.2.0.dist-info/licenses/AUTHORS +2 -0
- datarobot_genai-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
import datarobot as dr
|
|
18
|
+
|
|
19
|
+
from datarobot_genai.drmcp.core.clients import get_api_client
|
|
20
|
+
|
|
21
|
+
# Needed SDK version (3.10.0) is not published yet. We'll reimplement simplified version of it.
|
|
22
|
+
# get_datarobot_prompt_templates = dr.genai.PromptTemplate.list()
|
|
23
|
+
# DrPrompt = dr.genai.PromptTemplate
|
|
24
|
+
# DrPromptVersion = dr.genai.PromptTemplateVersion
|
|
25
|
+
# DrVariable = dr.genai.Variable
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class DrVariable:
|
|
30
|
+
name: str
|
|
31
|
+
description: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DrPromptVersion:
|
|
36
|
+
id: str
|
|
37
|
+
prompt_template_id: str
|
|
38
|
+
version: int
|
|
39
|
+
prompt_text: str
|
|
40
|
+
variables: list[DrVariable]
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_dict(cls, d: dict) -> "DrPromptVersion":
|
|
44
|
+
variables = [
|
|
45
|
+
DrVariable(name=v["name"], description=v["description"]) for v in d["variables"]
|
|
46
|
+
]
|
|
47
|
+
return cls(
|
|
48
|
+
id=d["id"],
|
|
49
|
+
prompt_template_id=d["promptTemplateId"],
|
|
50
|
+
version=d["version"],
|
|
51
|
+
prompt_text=d["promptText"],
|
|
52
|
+
variables=variables,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class DrPrompt:
|
|
58
|
+
id: str
|
|
59
|
+
name: str
|
|
60
|
+
description: str
|
|
61
|
+
|
|
62
|
+
def get_latest_version(self) -> DrPromptVersion | None:
|
|
63
|
+
all_prompt_template_versions = get_datarobot_prompt_template_versions([self.id])
|
|
64
|
+
prompt_template_versions = all_prompt_template_versions.get(self.id)
|
|
65
|
+
|
|
66
|
+
if not prompt_template_versions:
|
|
67
|
+
return None
|
|
68
|
+
latest_version = max(prompt_template_versions, key=lambda v: v.version)
|
|
69
|
+
return latest_version
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, d: dict) -> "DrPrompt":
|
|
73
|
+
return cls(id=d["id"], name=d["name"], description=d["description"])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_datarobot_prompt_templates() -> list[DrPrompt]:
|
|
77
|
+
prompt_templates_data = dr.utils.pagination.unpaginate(
|
|
78
|
+
initial_url="genai/promptTemplates/", initial_params={}, client=get_api_client()
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return [DrPrompt.from_dict(prompt_template) for prompt_template in prompt_templates_data]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_datarobot_prompt_template_versions(
|
|
85
|
+
prompt_template_ids: list[str],
|
|
86
|
+
) -> dict[str, list[DrPromptVersion]]:
|
|
87
|
+
prompt_template_versions_data = dr.utils.pagination.unpaginate(
|
|
88
|
+
initial_url="genai/promptTemplates/versions/",
|
|
89
|
+
initial_params={
|
|
90
|
+
"promptTemplateIds": prompt_template_ids,
|
|
91
|
+
},
|
|
92
|
+
client=get_api_client(),
|
|
93
|
+
)
|
|
94
|
+
prompt_template_versions = defaultdict(list)
|
|
95
|
+
for prompt_template_version in prompt_template_versions_data:
|
|
96
|
+
prompt_template_versions[prompt_template_version["promptTemplateId"]].append(
|
|
97
|
+
DrPromptVersion.from_dict(prompt_template_version)
|
|
98
|
+
)
|
|
99
|
+
return prompt_template_versions
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_datarobot_prompt_template(prompt_template_id: str) -> DrPrompt | None:
|
|
103
|
+
api_client = get_api_client()
|
|
104
|
+
try:
|
|
105
|
+
prompt_template_response = api_client.get(
|
|
106
|
+
f"genai/promptTemplates/{prompt_template_id}/", join_endpoint=True
|
|
107
|
+
)
|
|
108
|
+
prompt_template_json = prompt_template_response.json()
|
|
109
|
+
except Exception:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
return DrPrompt.from_dict(prompt_template_json)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_datarobot_prompt_template_version(
|
|
116
|
+
prompt_template_id: str, prompt_template_version_id: str
|
|
117
|
+
) -> DrPromptVersion | None:
|
|
118
|
+
api_client = get_api_client()
|
|
119
|
+
try:
|
|
120
|
+
prompt_template_version_response = api_client.get(
|
|
121
|
+
f"genai/promptTemplates/{prompt_template_id}/versions/{prompt_template_version_id}/",
|
|
122
|
+
join_endpoint=True,
|
|
123
|
+
)
|
|
124
|
+
prompt_template_version_json = prompt_template_version_response.json()
|
|
125
|
+
except Exception:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
return DrPromptVersion.from_dict(prompt_template_version_json)
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
from fastmcp.prompts.prompt import Prompt
|
|
22
|
+
from pydantic import Field
|
|
23
|
+
|
|
24
|
+
from datarobot_genai.drmcp.core.exceptions import DynamicPromptRegistrationError
|
|
25
|
+
from datarobot_genai.drmcp.core.mcp_instance import register_prompt
|
|
26
|
+
|
|
27
|
+
from .dr_lib import DrPrompt
|
|
28
|
+
from .dr_lib import DrPromptVersion
|
|
29
|
+
from .dr_lib import DrVariable
|
|
30
|
+
from .dr_lib import get_datarobot_prompt_template_versions
|
|
31
|
+
from .dr_lib import get_datarobot_prompt_templates
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def register_prompts_from_datarobot_prompt_management() -> None:
|
|
37
|
+
"""Register prompts from DataRobot Prompt Management."""
|
|
38
|
+
prompts = get_datarobot_prompt_templates()
|
|
39
|
+
logger.info(f"Found {len(prompts)} prompts in Prompts Management.")
|
|
40
|
+
all_prompts_versions = get_datarobot_prompt_template_versions(
|
|
41
|
+
prompt_template_ids=list({prompt.id for prompt in prompts})
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Try to register each prompt, continue on failure
|
|
45
|
+
for prompt in prompts:
|
|
46
|
+
prompt_versions = all_prompts_versions.get(prompt.id)
|
|
47
|
+
if not prompt_versions:
|
|
48
|
+
logger.warning(f"Prompt template id {prompt.id} has no versions.")
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
latest_version = max(prompt_versions, key=lambda v: v.version)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
await register_prompt_from_datarobot_prompt_management(prompt, latest_version)
|
|
55
|
+
except DynamicPromptRegistrationError:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def register_prompt_from_datarobot_prompt_management(
|
|
60
|
+
prompt_template: DrPrompt, prompt_template_version: DrPromptVersion | None = None
|
|
61
|
+
) -> Prompt:
|
|
62
|
+
"""Register a single prompt.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
prompt_template: The prompt within DataRobot Prompt Management.
|
|
66
|
+
prompt_template_version: Optional prompt version within DataRobot Prompt Management.
|
|
67
|
+
If not provided -- latest version will be used
|
|
68
|
+
|
|
69
|
+
Raises
|
|
70
|
+
------
|
|
71
|
+
DynamicPromptRegistrationError: If registration fails at any step.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
The registered Prompt instance.
|
|
76
|
+
"""
|
|
77
|
+
if not prompt_template_version:
|
|
78
|
+
prompt_template_version_to_register = prompt_template.get_latest_version()
|
|
79
|
+
|
|
80
|
+
if prompt_template_version_to_register is None:
|
|
81
|
+
logger.info(
|
|
82
|
+
f"No latest version in Prompts Management for prompt id: {prompt_template.id}"
|
|
83
|
+
)
|
|
84
|
+
raise DynamicPromptRegistrationError
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
prompt_template_version_to_register = prompt_template_version
|
|
88
|
+
|
|
89
|
+
logger.info(
|
|
90
|
+
f"Found prompt: id: {prompt_template.id}, "
|
|
91
|
+
f"name: {prompt_template.name}, "
|
|
92
|
+
f"prompt version id: {prompt_template_version_to_register.id}, "
|
|
93
|
+
f"version: {prompt_template_version_to_register.version}."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
valid_fn_name = to_valid_mcp_prompt_name(prompt_template.name)
|
|
98
|
+
except ValueError as e:
|
|
99
|
+
raise DynamicPromptRegistrationError from e
|
|
100
|
+
|
|
101
|
+
prompt_fn = make_prompt_function(
|
|
102
|
+
name=valid_fn_name,
|
|
103
|
+
description=prompt_template.description,
|
|
104
|
+
prompt_text=prompt_template_version_to_register.prompt_text,
|
|
105
|
+
variables=prompt_template_version_to_register.variables,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Register using generic external tool registration with the config
|
|
110
|
+
return await register_prompt(
|
|
111
|
+
fn=prompt_fn,
|
|
112
|
+
name=prompt_template.name,
|
|
113
|
+
description=prompt_template.description,
|
|
114
|
+
meta={
|
|
115
|
+
"prompt_template_id": prompt_template.id,
|
|
116
|
+
"prompt_template_version_id": prompt_template_version_to_register.id,
|
|
117
|
+
},
|
|
118
|
+
prompt_template=(prompt_template.id, prompt_template_version_to_register.id),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
logger.error(f"Skipping prompt {prompt_template.id}. Registration failed: {exc}")
|
|
123
|
+
raise DynamicPromptRegistrationError(
|
|
124
|
+
"Registration failed. Could not create prompt."
|
|
125
|
+
) from exc
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _escape_non_ascii(s: str) -> str:
|
|
129
|
+
out = []
|
|
130
|
+
for ch in s:
|
|
131
|
+
# If its space -> change to underscore
|
|
132
|
+
if ch.isspace():
|
|
133
|
+
out.append("_")
|
|
134
|
+
# ASCII letter, digit or underscore -> keep
|
|
135
|
+
elif ch.isascii() and (ch.isalnum() or ch == "_"):
|
|
136
|
+
out.append(ch)
|
|
137
|
+
# Everything else -> encode as 'xHEX'
|
|
138
|
+
else:
|
|
139
|
+
out.append(f"x{ord(ch):x}")
|
|
140
|
+
return "".join(out)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def to_valid_mcp_prompt_name(s: str) -> str:
|
|
144
|
+
"""Convert an arbitrary string into a valid MCP prompt name."""
|
|
145
|
+
# If its ONLY numbers return "prompt_[number]"
|
|
146
|
+
if s.isdigit():
|
|
147
|
+
return f"prompt_{s}"
|
|
148
|
+
|
|
149
|
+
# First, ASCII-transliterate using hex escape for non-ASCII
|
|
150
|
+
if not s.isascii():
|
|
151
|
+
# whole string non-ascii? -> escape and prefix with prompt_
|
|
152
|
+
encoded = _escape_non_ascii(s)
|
|
153
|
+
return f"prompt_{encoded}"
|
|
154
|
+
|
|
155
|
+
# Replace any sequence of invalid characters with '_'
|
|
156
|
+
s = re.sub(r"[^0-9a-zA-Z_]+", "_", s)
|
|
157
|
+
|
|
158
|
+
# Remove leading characters that are not letters or underscores (can't start with a digit or _)
|
|
159
|
+
s = re.sub(r"^[^a-zA-Z]+", "", s)
|
|
160
|
+
|
|
161
|
+
# Remove following _
|
|
162
|
+
s = re.sub(r"_+$", "", s)
|
|
163
|
+
|
|
164
|
+
# If string is empty after cleaning, raise error
|
|
165
|
+
if not s:
|
|
166
|
+
raise ValueError(f"Cannot convert {s} to valid MCP prompt name.")
|
|
167
|
+
|
|
168
|
+
# Make sure it's a valid identifier and not a reserved keyword
|
|
169
|
+
if keyword.iskeyword(s) or not s.isidentifier():
|
|
170
|
+
s = f"{s}_prompt"
|
|
171
|
+
|
|
172
|
+
return s
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def make_prompt_function(
|
|
176
|
+
name: str, description: str, prompt_text: str, variables: list[DrVariable]
|
|
177
|
+
) -> Callable:
|
|
178
|
+
params = []
|
|
179
|
+
for v in variables:
|
|
180
|
+
if keyword.iskeyword(v.name):
|
|
181
|
+
raise ValueError(f"Variable name '{v.name}' is invalid.")
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
param = Parameter(
|
|
185
|
+
name=v.name,
|
|
186
|
+
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
|
187
|
+
default=Field(description=v.description),
|
|
188
|
+
)
|
|
189
|
+
except ValueError as e:
|
|
190
|
+
raise ValueError(f"Variable name '{v.name}' is invalid.") from e
|
|
191
|
+
|
|
192
|
+
params.append(param)
|
|
193
|
+
|
|
194
|
+
async def template_function(**kwargs) -> str: # type: ignore
|
|
195
|
+
prompt_text_correct = prompt_text.replace("{{", "{").replace("}}", "}")
|
|
196
|
+
try:
|
|
197
|
+
return prompt_text_correct.format(**kwargs)
|
|
198
|
+
except KeyError as exc:
|
|
199
|
+
raise ValueError(f"Missing variable {exc.args[0]} for prompt '{name}'") from exc
|
|
200
|
+
|
|
201
|
+
# Apply metadata
|
|
202
|
+
template_function.__name__ = name
|
|
203
|
+
template_function.__doc__ = description
|
|
204
|
+
template_function.__signature__ = Signature(params) # type: ignore
|
|
205
|
+
|
|
206
|
+
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)
|