datarobot-genai 0.2.31__py3-none-any.whl

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