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,321 @@
|
|
|
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
|
+
"""Confluence MCP tools for interacting with Confluence Cloud."""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Annotated
|
|
19
|
+
|
|
20
|
+
from fastmcp.exceptions import ToolError
|
|
21
|
+
from fastmcp.tools.tool import ToolResult
|
|
22
|
+
|
|
23
|
+
from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
|
|
24
|
+
from datarobot_genai.drmcp.tools.clients.atlassian import get_atlassian_access_token
|
|
25
|
+
from datarobot_genai.drmcp.tools.clients.confluence import ConfluenceClient
|
|
26
|
+
from datarobot_genai.drmcp.tools.clients.confluence import ConfluenceError
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dr_mcp_tool(tags={"confluence", "read", "get", "page"})
|
|
32
|
+
async def confluence_get_page(
|
|
33
|
+
*,
|
|
34
|
+
page_id_or_title: Annotated[str, "The ID or the exact title of the Confluence page."],
|
|
35
|
+
space_key: Annotated[
|
|
36
|
+
str | None,
|
|
37
|
+
"Required if identifying the page by title. The space key (e.g., 'PROJ').",
|
|
38
|
+
] = None,
|
|
39
|
+
) -> ToolResult | ToolError:
|
|
40
|
+
"""Retrieve the content of a specific Confluence page.
|
|
41
|
+
|
|
42
|
+
Use this tool to fetch Confluence pages by their numeric ID or by title.
|
|
43
|
+
Returns page content in HTML storage format.
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
- By ID: page_id_or_title="856391684"
|
|
47
|
+
- By title: page_id_or_title="Meeting Notes", space_key="TEAM"
|
|
48
|
+
|
|
49
|
+
When using a page title, the space_key parameter is required.
|
|
50
|
+
"""
|
|
51
|
+
if not page_id_or_title:
|
|
52
|
+
raise ToolError("Argument validation error: 'page_id_or_title' cannot be empty.")
|
|
53
|
+
|
|
54
|
+
access_token = await get_atlassian_access_token()
|
|
55
|
+
if isinstance(access_token, ToolError):
|
|
56
|
+
raise access_token
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
async with ConfluenceClient(access_token) as client:
|
|
60
|
+
if page_id_or_title.isdigit():
|
|
61
|
+
page_response = await client.get_page_by_id(page_id_or_title)
|
|
62
|
+
else:
|
|
63
|
+
if not space_key:
|
|
64
|
+
raise ToolError(
|
|
65
|
+
"Argument validation error: "
|
|
66
|
+
"'space_key' is required when identifying a page by title."
|
|
67
|
+
)
|
|
68
|
+
page_response = await client.get_page_by_title(page_id_or_title, space_key)
|
|
69
|
+
except ConfluenceError as e:
|
|
70
|
+
logger.error(f"Confluence error getting page: {e}")
|
|
71
|
+
raise ToolError(str(e))
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Unexpected error getting Confluence page: {e}")
|
|
74
|
+
raise ToolError(
|
|
75
|
+
f"An unexpected error occurred while getting Confluence page "
|
|
76
|
+
f"'{page_id_or_title}': {str(e)}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return ToolResult(
|
|
80
|
+
content=f"Successfully retrieved page '{page_response.title}'.",
|
|
81
|
+
structured_content=page_response.as_flat_dict(),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dr_mcp_tool(tags={"confluence", "write", "create", "page"})
|
|
86
|
+
async def confluence_create_page(
|
|
87
|
+
*,
|
|
88
|
+
space_key: Annotated[str, "The key of the Confluence space where the new page should live."],
|
|
89
|
+
title: Annotated[str, "The title of the new page."],
|
|
90
|
+
body_content: Annotated[
|
|
91
|
+
str,
|
|
92
|
+
"The content of the page, typically in Confluence Storage Format (XML) or raw text.",
|
|
93
|
+
],
|
|
94
|
+
parent_id: Annotated[
|
|
95
|
+
int | None,
|
|
96
|
+
"The ID of the parent page, used to create a child page.",
|
|
97
|
+
] = None,
|
|
98
|
+
) -> ToolResult:
|
|
99
|
+
"""Create a new documentation page in a specified Confluence space.
|
|
100
|
+
|
|
101
|
+
Use this tool to create new Confluence pages with content in storage format.
|
|
102
|
+
The page will be created at the root level of the space unless a parent_id
|
|
103
|
+
is provided, in which case it will be created as a child page.
|
|
104
|
+
|
|
105
|
+
Usage:
|
|
106
|
+
- Root page: space_key="PROJ", title="New Page", body_content="<p>Content</p>"
|
|
107
|
+
- Child page: space_key="PROJ", title="Sub Page", body_content="<p>Content</p>",
|
|
108
|
+
parent_id=123456
|
|
109
|
+
"""
|
|
110
|
+
if not all([space_key, title, body_content]):
|
|
111
|
+
raise ToolError(
|
|
112
|
+
"Argument validation error: space_key, title, and body_content are required fields."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
access_token = await get_atlassian_access_token()
|
|
116
|
+
if isinstance(access_token, ToolError):
|
|
117
|
+
raise access_token
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
async with ConfluenceClient(access_token) as client:
|
|
121
|
+
page_response = await client.create_page(
|
|
122
|
+
space_key=space_key,
|
|
123
|
+
title=title,
|
|
124
|
+
body_content=body_content,
|
|
125
|
+
parent_id=parent_id,
|
|
126
|
+
)
|
|
127
|
+
except ConfluenceError as e:
|
|
128
|
+
logger.error(f"Confluence error creating page: {e}")
|
|
129
|
+
raise ToolError(str(e))
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Unexpected error creating Confluence page: {e}")
|
|
132
|
+
raise ToolError(
|
|
133
|
+
f"An unexpected error occurred while creating Confluence page "
|
|
134
|
+
f"'{title}' in space '{space_key}': {str(e)}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return ToolResult(
|
|
138
|
+
content=f"New page '{title}' created successfully in space '{space_key}'.",
|
|
139
|
+
structured_content={"new_page_id": page_response.page_id, "title": page_response.title},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dr_mcp_tool(tags={"confluence", "write", "add", "comment"})
|
|
144
|
+
async def confluence_add_comment(
|
|
145
|
+
*,
|
|
146
|
+
page_id: Annotated[str, "The numeric ID of the page where the comment will be added."],
|
|
147
|
+
comment_body: Annotated[str, "The text content of the comment."],
|
|
148
|
+
) -> ToolResult:
|
|
149
|
+
"""Add a new comment to a specified Confluence page for collaboration.
|
|
150
|
+
|
|
151
|
+
Use this tool to add comments to Confluence pages to facilitate collaboration
|
|
152
|
+
and discussion. Comments are added at the page level.
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
- Add comment: page_id="856391684", comment_body="Great work on this documentation!"
|
|
156
|
+
"""
|
|
157
|
+
if not page_id:
|
|
158
|
+
raise ToolError("Argument validation error: 'page_id' cannot be empty.")
|
|
159
|
+
|
|
160
|
+
if not comment_body:
|
|
161
|
+
raise ToolError("Argument validation error: 'comment_body' cannot be empty.")
|
|
162
|
+
|
|
163
|
+
access_token = await get_atlassian_access_token()
|
|
164
|
+
if isinstance(access_token, ToolError):
|
|
165
|
+
raise access_token
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
async with ConfluenceClient(access_token) as client:
|
|
169
|
+
comment_response = await client.add_comment(
|
|
170
|
+
page_id=page_id,
|
|
171
|
+
comment_body=comment_body,
|
|
172
|
+
)
|
|
173
|
+
except ConfluenceError as e:
|
|
174
|
+
logger.error(f"Confluence error adding comment: {e}")
|
|
175
|
+
raise ToolError(str(e))
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Unexpected error adding comment to Confluence page: {e}")
|
|
178
|
+
raise ToolError(
|
|
179
|
+
f"An unexpected error occurred while adding comment to page '{page_id}': {str(e)}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return ToolResult(
|
|
183
|
+
content=f"Comment added successfully to page ID {page_id}.",
|
|
184
|
+
structured_content={
|
|
185
|
+
"comment_id": comment_response.comment_id,
|
|
186
|
+
"page_id": page_id,
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dr_mcp_tool(tags={"confluence", "search", "content"})
|
|
192
|
+
async def confluence_search(
|
|
193
|
+
*,
|
|
194
|
+
cql_query: Annotated[
|
|
195
|
+
str,
|
|
196
|
+
"The CQL (Confluence Query Language) string used to filter content, "
|
|
197
|
+
"e.g., 'type=page and space=DOC'.",
|
|
198
|
+
],
|
|
199
|
+
max_results: Annotated[int, "Maximum number of content items to return. Default is 10."] = 10,
|
|
200
|
+
include_body: Annotated[
|
|
201
|
+
bool,
|
|
202
|
+
"If True, fetch full page body content for each result (slower, "
|
|
203
|
+
"makes additional API calls). Default is False, which returns only excerpts.",
|
|
204
|
+
] = False,
|
|
205
|
+
) -> ToolResult:
|
|
206
|
+
"""
|
|
207
|
+
Search Confluence pages and content efficiently using a CQL query string.
|
|
208
|
+
This pushes the search logic to the Confluence API (Push-Down).
|
|
209
|
+
|
|
210
|
+
Refer to Confluence documentation for advanced searching using CQL:
|
|
211
|
+
https://developer.atlassian.com/cloud/confluence/advanced-searching-using-cql/
|
|
212
|
+
"""
|
|
213
|
+
if not cql_query:
|
|
214
|
+
raise ToolError("Argument validation error: 'cql_query' cannot be empty.")
|
|
215
|
+
|
|
216
|
+
if max_results < 1 or max_results > 100:
|
|
217
|
+
raise ToolError("Argument validation error: 'max_results' must be between 1 and 100.")
|
|
218
|
+
|
|
219
|
+
access_token = await get_atlassian_access_token()
|
|
220
|
+
if isinstance(access_token, ToolError):
|
|
221
|
+
raise access_token
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
async with ConfluenceClient(access_token) as client:
|
|
225
|
+
results = await client.search_confluence_content(
|
|
226
|
+
cql_query=cql_query, max_results=max_results
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# If include_body is True, fetch full content for each page
|
|
230
|
+
if include_body and results:
|
|
231
|
+
data = []
|
|
232
|
+
for result in results:
|
|
233
|
+
flat = result.as_flat_dict()
|
|
234
|
+
try:
|
|
235
|
+
page = await client.get_page_by_id(result.id)
|
|
236
|
+
flat["body"] = page.body
|
|
237
|
+
except ConfluenceError:
|
|
238
|
+
flat["body"] = None # Keep excerpt if page fetch fails
|
|
239
|
+
data.append(flat)
|
|
240
|
+
else:
|
|
241
|
+
data = [result.as_flat_dict() for result in results]
|
|
242
|
+
|
|
243
|
+
except ConfluenceError as e:
|
|
244
|
+
logger.error(f"Confluence error searching content: {e}")
|
|
245
|
+
raise ToolError(str(e))
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"Unexpected error searching Confluence content: {e}")
|
|
248
|
+
raise ToolError(f"An unexpected error occurred while searching Confluence: {str(e)}")
|
|
249
|
+
|
|
250
|
+
n = len(results)
|
|
251
|
+
return ToolResult(
|
|
252
|
+
content=f"Successfully executed CQL query and retrieved {n} result(s).",
|
|
253
|
+
structured_content={"data": data, "count": n},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dr_mcp_tool(tags={"confluence", "write", "update", "page"})
|
|
258
|
+
async def confluence_update_page(
|
|
259
|
+
*,
|
|
260
|
+
page_id: Annotated[str, "The ID of the Confluence page to update."],
|
|
261
|
+
new_body_content: Annotated[
|
|
262
|
+
str,
|
|
263
|
+
"The full updated content of the page in Confluence Storage Format (XML) or raw text.",
|
|
264
|
+
],
|
|
265
|
+
version_number: Annotated[
|
|
266
|
+
int,
|
|
267
|
+
"The current version number of the page, required to prevent update conflicts. "
|
|
268
|
+
"Get this from the confluence_get_page tool.",
|
|
269
|
+
],
|
|
270
|
+
) -> ToolResult:
|
|
271
|
+
"""Update the content of an existing Confluence page.
|
|
272
|
+
|
|
273
|
+
Requires the current version number to ensure atomic updates.
|
|
274
|
+
Use this tool to update the body content of an existing Confluence page.
|
|
275
|
+
The version_number is required for optimistic locking - it prevents overwriting
|
|
276
|
+
changes made by others since you last fetched the page.
|
|
277
|
+
|
|
278
|
+
Usage:
|
|
279
|
+
page_id="856391684", new_body_content="<p>New content</p>", version_number=5
|
|
280
|
+
|
|
281
|
+
Important: Always fetch the page first using confluence_get_page to get the
|
|
282
|
+
current version number before updating.
|
|
283
|
+
"""
|
|
284
|
+
if not page_id:
|
|
285
|
+
raise ToolError("Argument validation error: 'page_id' cannot be empty.")
|
|
286
|
+
|
|
287
|
+
if not new_body_content:
|
|
288
|
+
raise ToolError("Argument validation error: 'new_body_content' cannot be empty.")
|
|
289
|
+
|
|
290
|
+
if version_number < 1:
|
|
291
|
+
raise ToolError(
|
|
292
|
+
"Argument validation error: 'version_number' must be a positive integer (>= 1)."
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
access_token = await get_atlassian_access_token()
|
|
296
|
+
if isinstance(access_token, ToolError):
|
|
297
|
+
raise access_token
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
async with ConfluenceClient(access_token) as client:
|
|
301
|
+
page_response = await client.update_page(
|
|
302
|
+
page_id=page_id,
|
|
303
|
+
new_body_content=new_body_content,
|
|
304
|
+
version_number=version_number,
|
|
305
|
+
)
|
|
306
|
+
except ConfluenceError as e:
|
|
307
|
+
logger.error(f"Confluence error updating page: {e}")
|
|
308
|
+
raise ToolError(str(e))
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Unexpected error updating Confluence page: {e}")
|
|
311
|
+
raise ToolError(
|
|
312
|
+
f"An unexpected error occurred while updating Confluence page '{page_id}': {str(e)}"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return ToolResult(
|
|
316
|
+
content=f"Page ID {page_id} updated successfully to version {page_response.version}.",
|
|
317
|
+
structured_content={
|
|
318
|
+
"updated_page_id": page_response.page_id,
|
|
319
|
+
"new_version": page_response.version,
|
|
320
|
+
},
|
|
321
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,347 @@
|
|
|
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
|
+
"""Google Drive MCP tools for interacting with Google Drive API."""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Annotated
|
|
19
|
+
|
|
20
|
+
from fastmcp.exceptions import ToolError
|
|
21
|
+
from fastmcp.tools.tool import ToolResult
|
|
22
|
+
|
|
23
|
+
from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
|
|
24
|
+
from datarobot_genai.drmcp.tools.clients.gdrive import GOOGLE_DRIVE_FOLDER_MIME
|
|
25
|
+
from datarobot_genai.drmcp.tools.clients.gdrive import LIMIT
|
|
26
|
+
from datarobot_genai.drmcp.tools.clients.gdrive import MAX_PAGE_SIZE
|
|
27
|
+
from datarobot_genai.drmcp.tools.clients.gdrive import SUPPORTED_FIELDS
|
|
28
|
+
from datarobot_genai.drmcp.tools.clients.gdrive import SUPPORTED_FIELDS_STR
|
|
29
|
+
from datarobot_genai.drmcp.tools.clients.gdrive import GoogleDriveClient
|
|
30
|
+
from datarobot_genai.drmcp.tools.clients.gdrive import GoogleDriveError
|
|
31
|
+
from datarobot_genai.drmcp.tools.clients.gdrive import get_gdrive_access_token
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dr_mcp_tool(tags={"google", "gdrive", "list", "search", "files", "find", "contents"})
|
|
37
|
+
async def gdrive_find_contents(
|
|
38
|
+
*,
|
|
39
|
+
page_size: Annotated[
|
|
40
|
+
int, f"Maximum number of files to return per page (max {MAX_PAGE_SIZE})."
|
|
41
|
+
] = 10,
|
|
42
|
+
limit: Annotated[int, f"Total maximum number of files to return (max {LIMIT})."] = 50,
|
|
43
|
+
page_token: Annotated[
|
|
44
|
+
str | None, "The token for the next page of results, retrieved from a previous call."
|
|
45
|
+
] = None,
|
|
46
|
+
query: Annotated[
|
|
47
|
+
str | None, "Optional filter to narrow results (e.g., 'trashed = false')."
|
|
48
|
+
] = None,
|
|
49
|
+
folder_id: Annotated[
|
|
50
|
+
str | None,
|
|
51
|
+
"The ID of a specific folder to list or search within. "
|
|
52
|
+
"If omitted, searches the entire Drive.",
|
|
53
|
+
] = None,
|
|
54
|
+
recursive: Annotated[
|
|
55
|
+
bool,
|
|
56
|
+
"If True, searches all subfolders. "
|
|
57
|
+
"If False and folder_id is provided, only lists immediate children.",
|
|
58
|
+
] = False,
|
|
59
|
+
fields: Annotated[
|
|
60
|
+
list[str] | None,
|
|
61
|
+
"Optional list of metadata fields to include. Ex. id, name, mimeType. "
|
|
62
|
+
f"Default = {SUPPORTED_FIELDS_STR}",
|
|
63
|
+
] = None,
|
|
64
|
+
) -> ToolResult:
|
|
65
|
+
"""
|
|
66
|
+
Search or list files in the user's Google Drive with pagination and filtering support.
|
|
67
|
+
Use this tool to discover file names and IDs for use with other tools.
|
|
68
|
+
|
|
69
|
+
Limit must be bigger than or equal to page size and it must be multiplication of page size.
|
|
70
|
+
Ex.
|
|
71
|
+
page size = 10 limit = 50
|
|
72
|
+
page size = 3 limit = 3
|
|
73
|
+
page size = 12 limit = 36
|
|
74
|
+
"""
|
|
75
|
+
access_token = await get_gdrive_access_token()
|
|
76
|
+
if isinstance(access_token, ToolError):
|
|
77
|
+
raise access_token
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
async with GoogleDriveClient(access_token) as client:
|
|
81
|
+
data = await client.list_files(
|
|
82
|
+
page_size=page_size,
|
|
83
|
+
page_token=page_token,
|
|
84
|
+
query=query,
|
|
85
|
+
limit=limit,
|
|
86
|
+
folder_id=folder_id,
|
|
87
|
+
recursive=recursive,
|
|
88
|
+
)
|
|
89
|
+
except GoogleDriveError as e:
|
|
90
|
+
logger.error(f"Google Drive error listing files: {e}")
|
|
91
|
+
raise ToolError(str(e))
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Unexpected error listing Google Drive files: {e}")
|
|
94
|
+
raise ToolError(f"An unexpected error occurred while listing Google Drive files: {str(e)}")
|
|
95
|
+
|
|
96
|
+
filtered_fields = set(fields).intersection(SUPPORTED_FIELDS) if fields else SUPPORTED_FIELDS
|
|
97
|
+
number_of_files = len(data.files)
|
|
98
|
+
next_page_info = (
|
|
99
|
+
f"Next page token needed to fetch more data: {data.next_page_token}"
|
|
100
|
+
if data.next_page_token
|
|
101
|
+
else "There're no more pages."
|
|
102
|
+
)
|
|
103
|
+
return ToolResult(
|
|
104
|
+
content=f"Successfully listed {number_of_files} files. {next_page_info}",
|
|
105
|
+
structured_content={
|
|
106
|
+
"files": [
|
|
107
|
+
file.model_dump(by_alias=True, include=filtered_fields) for file in data.files
|
|
108
|
+
],
|
|
109
|
+
"count": number_of_files,
|
|
110
|
+
"nextPageToken": data.next_page_token,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dr_mcp_tool(tags={"google", "gdrive", "read", "content", "file", "download"})
|
|
116
|
+
async def gdrive_read_content(
|
|
117
|
+
*,
|
|
118
|
+
file_id: Annotated[str, "The ID of the file to read."],
|
|
119
|
+
target_format: Annotated[
|
|
120
|
+
str | None,
|
|
121
|
+
"The preferred output format for Google Workspace files "
|
|
122
|
+
"(e.g., 'text/markdown' for Docs, 'text/csv' for Sheets). "
|
|
123
|
+
"If not specified, uses sensible defaults. Has no effect on regular files.",
|
|
124
|
+
] = None,
|
|
125
|
+
) -> ToolResult:
|
|
126
|
+
"""
|
|
127
|
+
Retrieve the content of a specific file by its ID. Google Workspace files are
|
|
128
|
+
automatically exported to LLM-readable formats (Push-Down).
|
|
129
|
+
|
|
130
|
+
Usage:
|
|
131
|
+
- Basic: gdrive_read_content(file_id="1ABC123def456")
|
|
132
|
+
- Custom format: gdrive_read_content(file_id="1ABC...", target_format="text/plain")
|
|
133
|
+
- First use gdrive_find_contents to discover file IDs
|
|
134
|
+
|
|
135
|
+
Supported conversions (defaults):
|
|
136
|
+
- Google Docs -> Markdown (text/markdown)
|
|
137
|
+
- Google Sheets -> CSV (text/csv)
|
|
138
|
+
- Google Slides -> Plain text (text/plain)
|
|
139
|
+
- PDF files -> Extracted text (text/plain)
|
|
140
|
+
- Other text files -> Downloaded as-is
|
|
141
|
+
|
|
142
|
+
Note: Binary files (images, videos, etc.) are not supported and will return an error.
|
|
143
|
+
Large Google Workspace files (>10MB) may fail to export due to API limits.
|
|
144
|
+
|
|
145
|
+
Refer to Google Drive export formats documentation:
|
|
146
|
+
https://developers.google.com/workspace/drive/api/guides/ref-export-formats
|
|
147
|
+
"""
|
|
148
|
+
if not file_id or not file_id.strip():
|
|
149
|
+
raise ToolError("Argument validation error: 'file_id' cannot be empty.")
|
|
150
|
+
|
|
151
|
+
access_token = await get_gdrive_access_token()
|
|
152
|
+
if isinstance(access_token, ToolError):
|
|
153
|
+
raise access_token
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
async with GoogleDriveClient(access_token) as client:
|
|
157
|
+
file_content = await client.read_file_content(file_id, target_format)
|
|
158
|
+
except GoogleDriveError as e:
|
|
159
|
+
logger.error(f"Google Drive error reading file content: {e}")
|
|
160
|
+
raise ToolError(str(e))
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(f"Unexpected error reading Google Drive file content: {e}")
|
|
163
|
+
raise ToolError(
|
|
164
|
+
f"An unexpected error occurred while reading Google Drive file content: {str(e)}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
export_info = ""
|
|
168
|
+
if file_content.was_exported:
|
|
169
|
+
export_info = f" (exported from {file_content.original_mime_type})"
|
|
170
|
+
|
|
171
|
+
return ToolResult(
|
|
172
|
+
content=(
|
|
173
|
+
f"Successfully retrieved content of '{file_content.name}' "
|
|
174
|
+
f"({file_content.mime_type}){export_info}."
|
|
175
|
+
),
|
|
176
|
+
structured_content=file_content.as_flat_dict(),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dr_mcp_tool(tags={"google", "gdrive", "create", "write", "file", "folder"}, enabled=False)
|
|
181
|
+
async def gdrive_create_file(
|
|
182
|
+
*,
|
|
183
|
+
name: Annotated[str, "The name for the new file or folder."],
|
|
184
|
+
mime_type: Annotated[
|
|
185
|
+
str,
|
|
186
|
+
"The MIME type of the file (e.g., 'text/plain', "
|
|
187
|
+
"'application/vnd.google-apps.document', 'application/vnd.google-apps.folder').",
|
|
188
|
+
],
|
|
189
|
+
parent_id: Annotated[
|
|
190
|
+
str | None, "The ID of the parent folder where the file should be created."
|
|
191
|
+
] = None,
|
|
192
|
+
initial_content: Annotated[
|
|
193
|
+
str | None, "Text content to populate the new file, if applicable."
|
|
194
|
+
] = None,
|
|
195
|
+
) -> ToolResult:
|
|
196
|
+
"""
|
|
197
|
+
Create a new file or folder in Google Drive.
|
|
198
|
+
|
|
199
|
+
This tool is essential for an AI agent to generate new output (like reports or
|
|
200
|
+
documentation) directly into the Drive structure.
|
|
201
|
+
|
|
202
|
+
Usage:
|
|
203
|
+
- Create empty file: gdrive_create_file(name="report.txt", mime_type="text/plain")
|
|
204
|
+
- Create Google Doc: gdrive_create_file(
|
|
205
|
+
name="My Report",
|
|
206
|
+
mime_type="application/vnd.google-apps.document",
|
|
207
|
+
initial_content="# Report Title"
|
|
208
|
+
)
|
|
209
|
+
- Create folder: gdrive_create_file(
|
|
210
|
+
name="Reports",
|
|
211
|
+
mime_type="application/vnd.google-apps.folder"
|
|
212
|
+
)
|
|
213
|
+
- Create in subfolder: gdrive_create_file(
|
|
214
|
+
name="file.txt",
|
|
215
|
+
mime_type="text/plain",
|
|
216
|
+
parent_id="folder_id_here",
|
|
217
|
+
initial_content="File content"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
Supported MIME types:
|
|
221
|
+
- text/plain: Plain text file
|
|
222
|
+
- application/vnd.google-apps.document: Google Doc (content auto-converted)
|
|
223
|
+
- application/vnd.google-apps.spreadsheet: Google Sheet (CSV content works best)
|
|
224
|
+
- application/vnd.google-apps.folder: Folder (initial_content is ignored)
|
|
225
|
+
|
|
226
|
+
Note: For Google Workspace files, the Drive API automatically converts plain text
|
|
227
|
+
content to the appropriate format.
|
|
228
|
+
"""
|
|
229
|
+
if not name or not name.strip():
|
|
230
|
+
raise ToolError("Argument validation error: 'name' cannot be empty.")
|
|
231
|
+
|
|
232
|
+
if not mime_type or not mime_type.strip():
|
|
233
|
+
raise ToolError("Argument validation error: 'mime_type' cannot be empty.")
|
|
234
|
+
|
|
235
|
+
access_token = await get_gdrive_access_token()
|
|
236
|
+
if isinstance(access_token, ToolError):
|
|
237
|
+
raise access_token
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
async with GoogleDriveClient(access_token) as client:
|
|
241
|
+
created_file = await client.create_file(
|
|
242
|
+
name=name,
|
|
243
|
+
mime_type=mime_type,
|
|
244
|
+
parent_id=parent_id,
|
|
245
|
+
initial_content=initial_content,
|
|
246
|
+
)
|
|
247
|
+
except GoogleDriveError as e:
|
|
248
|
+
logger.error(f"Google Drive error creating file: {e}")
|
|
249
|
+
raise ToolError(str(e))
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.error(f"Unexpected error creating Google Drive file: {e}")
|
|
252
|
+
raise ToolError(f"An unexpected error occurred while creating Google Drive file: {str(e)}")
|
|
253
|
+
|
|
254
|
+
file_type = "folder" if mime_type == GOOGLE_DRIVE_FOLDER_MIME else "file"
|
|
255
|
+
content_info = ""
|
|
256
|
+
if initial_content and mime_type != GOOGLE_DRIVE_FOLDER_MIME:
|
|
257
|
+
content_info = " with initial content"
|
|
258
|
+
|
|
259
|
+
return ToolResult(
|
|
260
|
+
content=f"Successfully created {file_type} '{created_file.name}'{content_info}.",
|
|
261
|
+
structured_content=created_file.as_flat_dict(),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dr_mcp_tool(
|
|
266
|
+
tags={"google", "gdrive", "update", "metadata", "rename", "star", "trash"}, enabled=False
|
|
267
|
+
)
|
|
268
|
+
async def gdrive_update_metadata(
|
|
269
|
+
*,
|
|
270
|
+
file_id: Annotated[str, "The ID of the file or folder to update."],
|
|
271
|
+
new_name: Annotated[str | None, "A new name to rename the file."] = None,
|
|
272
|
+
starred: Annotated[bool | None, "Set to True to star the file or False to unstar it."] = None,
|
|
273
|
+
trash: Annotated[bool | None, "Set to True to trash the file or False to restore it."] = None,
|
|
274
|
+
) -> ToolResult:
|
|
275
|
+
"""
|
|
276
|
+
Update non-content metadata fields of a Google Drive file or folder.
|
|
277
|
+
|
|
278
|
+
This tool allows you to:
|
|
279
|
+
- Rename files and folders by setting new_name
|
|
280
|
+
- Star or unstar files (per-user preference) with starred
|
|
281
|
+
- Move files to trash or restore them with trash
|
|
282
|
+
|
|
283
|
+
Usage:
|
|
284
|
+
- Rename: gdrive_update_metadata(file_id="1ABC...", new_name="New Name.txt")
|
|
285
|
+
- Star: gdrive_update_metadata(file_id="1ABC...", starred=True)
|
|
286
|
+
- Unstar: gdrive_update_metadata(file_id="1ABC...", starred=False)
|
|
287
|
+
- Trash: gdrive_update_metadata(file_id="1ABC...", trash=True)
|
|
288
|
+
- Restore: gdrive_update_metadata(file_id="1ABC...", trash=False)
|
|
289
|
+
- Multiple: gdrive_update_metadata(file_id="1ABC...", new_name="New.txt", starred=True)
|
|
290
|
+
|
|
291
|
+
Note:
|
|
292
|
+
- At least one of new_name, starred, or trash must be provided.
|
|
293
|
+
- Starring is per-user: starring a shared file only affects your view.
|
|
294
|
+
- Trashing a folder trashes all contents recursively.
|
|
295
|
+
- Trashing requires permissions (owner for My Drive, organizer for Shared Drives).
|
|
296
|
+
"""
|
|
297
|
+
if not file_id or not file_id.strip():
|
|
298
|
+
raise ToolError("Argument validation error: 'file_id' cannot be empty.")
|
|
299
|
+
|
|
300
|
+
if new_name is None and starred is None and trash is None:
|
|
301
|
+
raise ToolError(
|
|
302
|
+
"Argument validation error: at least one of 'new_name', 'starred', or 'trash' "
|
|
303
|
+
"must be provided."
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if new_name is not None and not new_name.strip():
|
|
307
|
+
raise ToolError("Argument validation error: 'new_name' cannot be empty or whitespace.")
|
|
308
|
+
|
|
309
|
+
access_token = await get_gdrive_access_token()
|
|
310
|
+
if isinstance(access_token, ToolError):
|
|
311
|
+
raise access_token
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
async with GoogleDriveClient(access_token) as client:
|
|
315
|
+
updated_file = await client.update_file_metadata(
|
|
316
|
+
file_id=file_id,
|
|
317
|
+
new_name=new_name,
|
|
318
|
+
starred=starred,
|
|
319
|
+
trashed=trash,
|
|
320
|
+
)
|
|
321
|
+
except GoogleDriveError as e:
|
|
322
|
+
logger.error(f"Google Drive error updating file metadata: {e}")
|
|
323
|
+
raise ToolError(str(e))
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.error(f"Unexpected error updating Google Drive file metadata: {e}")
|
|
326
|
+
raise ToolError(
|
|
327
|
+
f"An unexpected error occurred while updating Google Drive file metadata: {str(e)}"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
changes: list[str] = []
|
|
331
|
+
if new_name is not None:
|
|
332
|
+
changes.append(f"renamed to '{new_name}'")
|
|
333
|
+
if starred is True:
|
|
334
|
+
changes.append("starred")
|
|
335
|
+
elif starred is False:
|
|
336
|
+
changes.append("unstarred")
|
|
337
|
+
if trash is True:
|
|
338
|
+
changes.append("moved to trash")
|
|
339
|
+
elif trash is False:
|
|
340
|
+
changes.append("restored from trash")
|
|
341
|
+
|
|
342
|
+
changes_description = ", ".join(changes)
|
|
343
|
+
|
|
344
|
+
return ToolResult(
|
|
345
|
+
content=f"Successfully updated file '{updated_file.name}': {changes_description}.",
|
|
346
|
+
structured_content=updated_file.as_flat_dict(),
|
|
347
|
+
)
|
|
@@ -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
|
+
|