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.
Files changed (101) 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 +250 -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 +316 -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 +128 -0
  37. datarobot_genai/drmcp/core/dynamic_prompts/register.py +206 -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 +542 -0
  56. datarobot_genai/drmcp/core/mcp_server_tools.py +129 -0
  57. datarobot_genai/drmcp/core/memory_management/__init__.py +13 -0
  58. datarobot_genai/drmcp/core/memory_management/manager.py +820 -0
  59. datarobot_genai/drmcp/core/memory_management/memory_tools.py +201 -0
  60. datarobot_genai/drmcp/core/routes.py +436 -0
  61. datarobot_genai/drmcp/core/routes_utils.py +30 -0
  62. datarobot_genai/drmcp/core/server_life_cycle.py +107 -0
  63. datarobot_genai/drmcp/core/telemetry.py +424 -0
  64. datarobot_genai/drmcp/core/tool_filter.py +108 -0
  65. datarobot_genai/drmcp/core/utils.py +131 -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/integration_mcp_server.py +102 -0
  69. datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +96 -0
  70. datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +94 -0
  71. datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py +234 -0
  72. datarobot_genai/drmcp/test_utils/tool_base_ete.py +151 -0
  73. datarobot_genai/drmcp/test_utils/utils.py +91 -0
  74. datarobot_genai/drmcp/tools/__init__.py +14 -0
  75. datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
  76. datarobot_genai/drmcp/tools/predictive/data.py +97 -0
  77. datarobot_genai/drmcp/tools/predictive/deployment.py +91 -0
  78. datarobot_genai/drmcp/tools/predictive/deployment_info.py +392 -0
  79. datarobot_genai/drmcp/tools/predictive/model.py +148 -0
  80. datarobot_genai/drmcp/tools/predictive/predict.py +254 -0
  81. datarobot_genai/drmcp/tools/predictive/predict_realtime.py +307 -0
  82. datarobot_genai/drmcp/tools/predictive/project.py +72 -0
  83. datarobot_genai/drmcp/tools/predictive/training.py +651 -0
  84. datarobot_genai/langgraph/__init__.py +0 -0
  85. datarobot_genai/langgraph/agent.py +341 -0
  86. datarobot_genai/langgraph/mcp.py +73 -0
  87. datarobot_genai/llama_index/__init__.py +16 -0
  88. datarobot_genai/llama_index/agent.py +50 -0
  89. datarobot_genai/llama_index/base.py +299 -0
  90. datarobot_genai/llama_index/mcp.py +79 -0
  91. datarobot_genai/nat/__init__.py +0 -0
  92. datarobot_genai/nat/agent.py +258 -0
  93. datarobot_genai/nat/datarobot_llm_clients.py +249 -0
  94. datarobot_genai/nat/datarobot_llm_providers.py +130 -0
  95. datarobot_genai/py.typed +0 -0
  96. datarobot_genai-0.2.0.dist-info/METADATA +139 -0
  97. datarobot_genai-0.2.0.dist-info/RECORD +101 -0
  98. datarobot_genai-0.2.0.dist-info/WHEEL +4 -0
  99. datarobot_genai-0.2.0.dist-info/entry_points.txt +3 -0
  100. datarobot_genai-0.2.0.dist-info/licenses/AUTHORS +2 -0
  101. 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
+
@@ -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)