datarobot-genai 0.2.26__py3-none-any.whl → 0.2.34__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/core/cli/agent_kernel.py +4 -1
- datarobot_genai/drmcp/__init__.py +2 -2
- datarobot_genai/drmcp/core/config.py +121 -83
- datarobot_genai/drmcp/core/exceptions.py +0 -4
- datarobot_genai/drmcp/core/logging.py +2 -2
- datarobot_genai/drmcp/core/tool_config.py +17 -9
- 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/{openai_llm_mcp_client.py → clients/base.py} +38 -40
- 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/mcp_utils_ete.py +20 -0
- datarobot_genai/drmcp/test_utils/test_interactive.py +16 -16
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +69 -2
- datarobot_genai/drmcp/test_utils/utils.py +1 -1
- datarobot_genai/drmcp/tools/clients/gdrive.py +314 -1
- datarobot_genai/drmcp/tools/clients/microsoft_graph.py +479 -0
- datarobot_genai/drmcp/tools/gdrive/tools.py +273 -4
- 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/data.py +16 -8
- datarobot_genai/drmcp/tools/predictive/model.py +87 -52
- datarobot_genai/drmcp/tools/predictive/project.py +2 -2
- datarobot_genai/drmcp/tools/predictive/training.py +15 -14
- datarobot_genai/nat/datarobot_llm_clients.py +90 -54
- datarobot_genai/nat/datarobot_mcp_client.py +47 -15
- {datarobot_genai-0.2.26.dist-info → datarobot_genai-0.2.34.dist-info}/METADATA +1 -1
- {datarobot_genai-0.2.26.dist-info → datarobot_genai-0.2.34.dist-info}/RECORD +32 -25
- {datarobot_genai-0.2.26.dist-info → datarobot_genai-0.2.34.dist-info}/WHEEL +0 -0
- {datarobot_genai-0.2.26.dist-info → datarobot_genai-0.2.34.dist-info}/entry_points.txt +0 -0
- {datarobot_genai-0.2.26.dist-info → datarobot_genai-0.2.34.dist-info}/licenses/AUTHORS +0 -0
- {datarobot_genai-0.2.26.dist-info → datarobot_genai-0.2.34.dist-info}/licenses/LICENSE +0 -0
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
18
|
from typing import Annotated
|
|
19
|
+
from typing import Literal
|
|
19
20
|
|
|
20
21
|
from fastmcp.exceptions import ToolError
|
|
21
22
|
from fastmcp.tools.tool import ToolResult
|
|
22
23
|
|
|
23
24
|
from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
|
|
25
|
+
from datarobot_genai.drmcp.tools.clients.gdrive import GOOGLE_DRIVE_FOLDER_MIME
|
|
24
26
|
from datarobot_genai.drmcp.tools.clients.gdrive import LIMIT
|
|
25
27
|
from datarobot_genai.drmcp.tools.clients.gdrive import MAX_PAGE_SIZE
|
|
26
28
|
from datarobot_genai.drmcp.tools.clients.gdrive import SUPPORTED_FIELDS
|
|
@@ -32,7 +34,9 @@ from datarobot_genai.drmcp.tools.clients.gdrive import get_gdrive_access_token
|
|
|
32
34
|
logger = logging.getLogger(__name__)
|
|
33
35
|
|
|
34
36
|
|
|
35
|
-
@dr_mcp_tool(
|
|
37
|
+
@dr_mcp_tool(
|
|
38
|
+
tags={"google", "gdrive", "list", "search", "files", "find", "contents"}, enabled=False
|
|
39
|
+
)
|
|
36
40
|
async def gdrive_find_contents(
|
|
37
41
|
*,
|
|
38
42
|
page_size: Annotated[
|
|
@@ -60,7 +64,7 @@ async def gdrive_find_contents(
|
|
|
60
64
|
"Optional list of metadata fields to include. Ex. id, name, mimeType. "
|
|
61
65
|
f"Default = {SUPPORTED_FIELDS_STR}",
|
|
62
66
|
] = None,
|
|
63
|
-
) -> ToolResult
|
|
67
|
+
) -> ToolResult:
|
|
64
68
|
"""
|
|
65
69
|
Search or list files in the user's Google Drive with pagination and filtering support.
|
|
66
70
|
Use this tool to discover file names and IDs for use with other tools.
|
|
@@ -121,7 +125,7 @@ async def gdrive_read_content(
|
|
|
121
125
|
"(e.g., 'text/markdown' for Docs, 'text/csv' for Sheets). "
|
|
122
126
|
"If not specified, uses sensible defaults. Has no effect on regular files.",
|
|
123
127
|
] = None,
|
|
124
|
-
) -> ToolResult
|
|
128
|
+
) -> ToolResult:
|
|
125
129
|
"""
|
|
126
130
|
Retrieve the content of a specific file by its ID. Google Workspace files are
|
|
127
131
|
automatically exported to LLM-readable formats (Push-Down).
|
|
@@ -163,7 +167,6 @@ async def gdrive_read_content(
|
|
|
163
167
|
f"An unexpected error occurred while reading Google Drive file content: {str(e)}"
|
|
164
168
|
)
|
|
165
169
|
|
|
166
|
-
# Provide helpful context about the conversion
|
|
167
170
|
export_info = ""
|
|
168
171
|
if file_content.was_exported:
|
|
169
172
|
export_info = f" (exported from {file_content.original_mime_type})"
|
|
@@ -175,3 +178,269 @@ async def gdrive_read_content(
|
|
|
175
178
|
),
|
|
176
179
|
structured_content=file_content.as_flat_dict(),
|
|
177
180
|
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dr_mcp_tool(tags={"google", "gdrive", "create", "write", "file", "folder"}, enabled=False)
|
|
184
|
+
async def gdrive_create_file(
|
|
185
|
+
*,
|
|
186
|
+
name: Annotated[str, "The name for the new file or folder."],
|
|
187
|
+
mime_type: Annotated[
|
|
188
|
+
str,
|
|
189
|
+
"The MIME type of the file (e.g., 'text/plain', "
|
|
190
|
+
"'application/vnd.google-apps.document', 'application/vnd.google-apps.folder').",
|
|
191
|
+
],
|
|
192
|
+
parent_id: Annotated[
|
|
193
|
+
str | None, "The ID of the parent folder where the file should be created."
|
|
194
|
+
] = None,
|
|
195
|
+
initial_content: Annotated[
|
|
196
|
+
str | None, "Text content to populate the new file, if applicable."
|
|
197
|
+
] = None,
|
|
198
|
+
) -> ToolResult:
|
|
199
|
+
"""
|
|
200
|
+
Create a new file or folder in Google Drive.
|
|
201
|
+
|
|
202
|
+
This tool is essential for an AI agent to generate new output (like reports or
|
|
203
|
+
documentation) directly into the Drive structure.
|
|
204
|
+
|
|
205
|
+
Usage:
|
|
206
|
+
- Create empty file: gdrive_create_file(name="report.txt", mime_type="text/plain")
|
|
207
|
+
- Create Google Doc: gdrive_create_file(
|
|
208
|
+
name="My Report",
|
|
209
|
+
mime_type="application/vnd.google-apps.document",
|
|
210
|
+
initial_content="# Report Title"
|
|
211
|
+
)
|
|
212
|
+
- Create folder: gdrive_create_file(
|
|
213
|
+
name="Reports",
|
|
214
|
+
mime_type="application/vnd.google-apps.folder"
|
|
215
|
+
)
|
|
216
|
+
- Create in subfolder: gdrive_create_file(
|
|
217
|
+
name="file.txt",
|
|
218
|
+
mime_type="text/plain",
|
|
219
|
+
parent_id="folder_id_here",
|
|
220
|
+
initial_content="File content"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
Supported MIME types:
|
|
224
|
+
- text/plain: Plain text file
|
|
225
|
+
- application/vnd.google-apps.document: Google Doc (content auto-converted)
|
|
226
|
+
- application/vnd.google-apps.spreadsheet: Google Sheet (CSV content works best)
|
|
227
|
+
- application/vnd.google-apps.folder: Folder (initial_content is ignored)
|
|
228
|
+
|
|
229
|
+
Note: For Google Workspace files, the Drive API automatically converts plain text
|
|
230
|
+
content to the appropriate format.
|
|
231
|
+
"""
|
|
232
|
+
if not name or not name.strip():
|
|
233
|
+
raise ToolError("Argument validation error: 'name' cannot be empty.")
|
|
234
|
+
|
|
235
|
+
if not mime_type or not mime_type.strip():
|
|
236
|
+
raise ToolError("Argument validation error: 'mime_type' cannot be empty.")
|
|
237
|
+
|
|
238
|
+
access_token = await get_gdrive_access_token()
|
|
239
|
+
if isinstance(access_token, ToolError):
|
|
240
|
+
raise access_token
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
async with GoogleDriveClient(access_token) as client:
|
|
244
|
+
created_file = await client.create_file(
|
|
245
|
+
name=name,
|
|
246
|
+
mime_type=mime_type,
|
|
247
|
+
parent_id=parent_id,
|
|
248
|
+
initial_content=initial_content,
|
|
249
|
+
)
|
|
250
|
+
except GoogleDriveError as e:
|
|
251
|
+
logger.error(f"Google Drive error creating file: {e}")
|
|
252
|
+
raise ToolError(str(e))
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.error(f"Unexpected error creating Google Drive file: {e}")
|
|
255
|
+
raise ToolError(f"An unexpected error occurred while creating Google Drive file: {str(e)}")
|
|
256
|
+
|
|
257
|
+
file_type = "folder" if mime_type == GOOGLE_DRIVE_FOLDER_MIME else "file"
|
|
258
|
+
content_info = ""
|
|
259
|
+
if initial_content and mime_type != GOOGLE_DRIVE_FOLDER_MIME:
|
|
260
|
+
content_info = " with initial content"
|
|
261
|
+
|
|
262
|
+
return ToolResult(
|
|
263
|
+
content=f"Successfully created {file_type} '{created_file.name}'{content_info}.",
|
|
264
|
+
structured_content=created_file.as_flat_dict(),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dr_mcp_tool(
|
|
269
|
+
tags={"google", "gdrive", "update", "metadata", "rename", "star", "trash"}, enabled=False
|
|
270
|
+
)
|
|
271
|
+
async def gdrive_update_metadata(
|
|
272
|
+
*,
|
|
273
|
+
file_id: Annotated[str, "The ID of the file or folder to update."],
|
|
274
|
+
new_name: Annotated[str | None, "A new name to rename the file."] = None,
|
|
275
|
+
starred: Annotated[bool | None, "Set to True to star the file or False to unstar it."] = None,
|
|
276
|
+
trash: Annotated[bool | None, "Set to True to trash the file or False to restore it."] = None,
|
|
277
|
+
) -> ToolResult:
|
|
278
|
+
"""
|
|
279
|
+
Update non-content metadata fields of a Google Drive file or folder.
|
|
280
|
+
|
|
281
|
+
This tool allows you to:
|
|
282
|
+
- Rename files and folders by setting new_name
|
|
283
|
+
- Star or unstar files (per-user preference) with starred
|
|
284
|
+
- Move files to trash or restore them with trash
|
|
285
|
+
|
|
286
|
+
Usage:
|
|
287
|
+
- Rename: gdrive_update_metadata(file_id="1ABC...", new_name="New Name.txt")
|
|
288
|
+
- Star: gdrive_update_metadata(file_id="1ABC...", starred=True)
|
|
289
|
+
- Unstar: gdrive_update_metadata(file_id="1ABC...", starred=False)
|
|
290
|
+
- Trash: gdrive_update_metadata(file_id="1ABC...", trash=True)
|
|
291
|
+
- Restore: gdrive_update_metadata(file_id="1ABC...", trash=False)
|
|
292
|
+
- Multiple: gdrive_update_metadata(file_id="1ABC...", new_name="New.txt", starred=True)
|
|
293
|
+
|
|
294
|
+
Note:
|
|
295
|
+
- At least one of new_name, starred, or trash must be provided.
|
|
296
|
+
- Starring is per-user: starring a shared file only affects your view.
|
|
297
|
+
- Trashing a folder trashes all contents recursively.
|
|
298
|
+
- Trashing requires permissions (owner for My Drive, organizer for Shared Drives).
|
|
299
|
+
"""
|
|
300
|
+
if not file_id or not file_id.strip():
|
|
301
|
+
raise ToolError("Argument validation error: 'file_id' cannot be empty.")
|
|
302
|
+
|
|
303
|
+
if new_name is None and starred is None and trash is None:
|
|
304
|
+
raise ToolError(
|
|
305
|
+
"Argument validation error: at least one of 'new_name', 'starred', or 'trash' "
|
|
306
|
+
"must be provided."
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if new_name is not None and not new_name.strip():
|
|
310
|
+
raise ToolError("Argument validation error: 'new_name' cannot be empty or whitespace.")
|
|
311
|
+
|
|
312
|
+
access_token = await get_gdrive_access_token()
|
|
313
|
+
if isinstance(access_token, ToolError):
|
|
314
|
+
raise access_token
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
async with GoogleDriveClient(access_token) as client:
|
|
318
|
+
updated_file = await client.update_file_metadata(
|
|
319
|
+
file_id=file_id,
|
|
320
|
+
new_name=new_name,
|
|
321
|
+
starred=starred,
|
|
322
|
+
trashed=trash,
|
|
323
|
+
)
|
|
324
|
+
except GoogleDriveError as e:
|
|
325
|
+
logger.error(f"Google Drive error updating file metadata: {e}")
|
|
326
|
+
raise ToolError(str(e))
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.error(f"Unexpected error updating Google Drive file metadata: {e}")
|
|
329
|
+
raise ToolError(
|
|
330
|
+
f"An unexpected error occurred while updating Google Drive file metadata: {str(e)}"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
changes: list[str] = []
|
|
334
|
+
if new_name is not None:
|
|
335
|
+
changes.append(f"renamed to '{new_name}'")
|
|
336
|
+
if starred is True:
|
|
337
|
+
changes.append("starred")
|
|
338
|
+
elif starred is False:
|
|
339
|
+
changes.append("unstarred")
|
|
340
|
+
if trash is True:
|
|
341
|
+
changes.append("moved to trash")
|
|
342
|
+
elif trash is False:
|
|
343
|
+
changes.append("restored from trash")
|
|
344
|
+
|
|
345
|
+
changes_description = ", ".join(changes)
|
|
346
|
+
|
|
347
|
+
return ToolResult(
|
|
348
|
+
content=f"Successfully updated file '{updated_file.name}': {changes_description}.",
|
|
349
|
+
structured_content=updated_file.as_flat_dict(),
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@dr_mcp_tool(tags={"google", "gdrive", "manage", "access", "acl"})
|
|
354
|
+
async def gdrive_manage_access(
|
|
355
|
+
*,
|
|
356
|
+
file_id: Annotated[str, "The ID of the file or folder."],
|
|
357
|
+
action: Annotated[Literal["add", "update", "remove"], "The operation to perform."],
|
|
358
|
+
role: Annotated[
|
|
359
|
+
Literal["reader", "commenter", "writer", "fileOrganizer", "organizer", "owner"] | None,
|
|
360
|
+
"The access level.",
|
|
361
|
+
] = None,
|
|
362
|
+
email_address: Annotated[
|
|
363
|
+
str | None, "The email of the user or group (required for 'add')."
|
|
364
|
+
] = None,
|
|
365
|
+
permission_id: Annotated[
|
|
366
|
+
str | None, "The specific permission ID (required for 'update' or 'remove')."
|
|
367
|
+
] = None,
|
|
368
|
+
transfer_ownership: Annotated[
|
|
369
|
+
bool, "Whether to transfer ownership (only for 'update' to 'owner' role)."
|
|
370
|
+
] = False,
|
|
371
|
+
) -> ToolResult:
|
|
372
|
+
"""
|
|
373
|
+
Consolidated tool for sharing files and managing permissions.
|
|
374
|
+
Pushes all logic to the Google Drive API permissions resource (create, update, delete).
|
|
375
|
+
|
|
376
|
+
Usage:
|
|
377
|
+
- Add role: gdrive_manage_access(
|
|
378
|
+
file_id="SomeFileId",
|
|
379
|
+
action="add",
|
|
380
|
+
role="reader",
|
|
381
|
+
email_address="dummy@user.com"
|
|
382
|
+
)
|
|
383
|
+
- Update role: gdrive_manage_access(
|
|
384
|
+
file_id="SomeFileId",
|
|
385
|
+
action="update",
|
|
386
|
+
role="reader",
|
|
387
|
+
permission_id="SomePermissionId"
|
|
388
|
+
)
|
|
389
|
+
- Remove permission: gdrive_manage_access(
|
|
390
|
+
file_id="SomeFileId",
|
|
391
|
+
action="remove",
|
|
392
|
+
permission_id="SomePermissionId"
|
|
393
|
+
)
|
|
394
|
+
"""
|
|
395
|
+
if not file_id or not file_id.strip():
|
|
396
|
+
raise ToolError("Argument validation error: 'file_id' cannot be empty.")
|
|
397
|
+
|
|
398
|
+
if action == "add" and not email_address:
|
|
399
|
+
raise ToolError("'email_address' is required for action 'add'.")
|
|
400
|
+
|
|
401
|
+
if action in ("update", "remove") and not permission_id:
|
|
402
|
+
raise ToolError("'permission_id' is required for action 'update' or 'remove'.")
|
|
403
|
+
|
|
404
|
+
if action != "remove" and not role:
|
|
405
|
+
raise ToolError("'role' is required for action 'add' or 'update'.")
|
|
406
|
+
|
|
407
|
+
access_token = await get_gdrive_access_token()
|
|
408
|
+
if isinstance(access_token, ToolError):
|
|
409
|
+
raise access_token
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
async with GoogleDriveClient(access_token) as client:
|
|
413
|
+
permission_id = await client.manage_access(
|
|
414
|
+
file_id=file_id,
|
|
415
|
+
action=action,
|
|
416
|
+
role=role,
|
|
417
|
+
email_address=email_address,
|
|
418
|
+
permission_id=permission_id,
|
|
419
|
+
transfer_ownership=transfer_ownership,
|
|
420
|
+
)
|
|
421
|
+
except GoogleDriveError as e:
|
|
422
|
+
logger.error(f"Google Drive permission operation failed: {e}")
|
|
423
|
+
raise ToolError(str(e))
|
|
424
|
+
except Exception as e:
|
|
425
|
+
logger.error(f"Unexpected error changing permissions for Google Drive file {file_id}: {e}")
|
|
426
|
+
raise ToolError(
|
|
427
|
+
f"Unexpected error changing permissions for Google Drive file {file_id}: {str(e)}"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Build response
|
|
431
|
+
structured_content = {"affectedFileId": file_id}
|
|
432
|
+
if action == "add":
|
|
433
|
+
content = (
|
|
434
|
+
f"Successfully added role '{role}' for '{email_address}' for gdrive file '{file_id}'. "
|
|
435
|
+
f"New permission id '{permission_id}'."
|
|
436
|
+
)
|
|
437
|
+
structured_content["newPermissionId"] = permission_id
|
|
438
|
+
elif action == "update":
|
|
439
|
+
content = (
|
|
440
|
+
f"Successfully updated role '{role}' (permission '{permission_id}') "
|
|
441
|
+
f"for gdrive file '{file_id}'."
|
|
442
|
+
)
|
|
443
|
+
else: # action == "remove":
|
|
444
|
+
content = f"Successfully removed permission '{permission_id}' for gdrive file '{file_id}'."
|
|
445
|
+
|
|
446
|
+
return ToolResult(content=content, structured_content=structured_content)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2026 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.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Copyright 2026 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
|
+
"""Microsoft Graph MCP tools for searching SharePoint and OneDrive content."""
|
|
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.microsoft_graph import MicrosoftGraphClient
|
|
25
|
+
from datarobot_genai.drmcp.tools.clients.microsoft_graph import MicrosoftGraphError
|
|
26
|
+
from datarobot_genai.drmcp.tools.clients.microsoft_graph import get_microsoft_graph_access_token
|
|
27
|
+
from datarobot_genai.drmcp.tools.clients.microsoft_graph import validate_site_url
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dr_mcp_tool(
|
|
33
|
+
tags={
|
|
34
|
+
"microsoft",
|
|
35
|
+
"graph api",
|
|
36
|
+
"sharepoint",
|
|
37
|
+
"drive",
|
|
38
|
+
"list",
|
|
39
|
+
"search",
|
|
40
|
+
"files",
|
|
41
|
+
"find",
|
|
42
|
+
"contents",
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
async def microsoft_graph_search_content(
|
|
46
|
+
*,
|
|
47
|
+
search_query: Annotated[str, "The search string to find files, folders, or list items."],
|
|
48
|
+
site_url: Annotated[
|
|
49
|
+
str | None,
|
|
50
|
+
"Optional SharePoint site URL to scope the search "
|
|
51
|
+
"(e.g., https://tenant.sharepoint.com/sites/sitename). "
|
|
52
|
+
"If not provided, searches across all accessible sites.",
|
|
53
|
+
] = None,
|
|
54
|
+
site_id: Annotated[
|
|
55
|
+
str | None,
|
|
56
|
+
"Optional ID of the site to scope the search. If provided, takes precedence over site_url.",
|
|
57
|
+
] = None,
|
|
58
|
+
from_offset: Annotated[
|
|
59
|
+
int,
|
|
60
|
+
"The zero-based index of the first result to return. Use this for pagination. "
|
|
61
|
+
"Default: 0 (start from the beginning). To get the next page, increment by the size "
|
|
62
|
+
"value (e.g., first page: from=0 size=250, second page: from=250 size=250, "
|
|
63
|
+
"third page: from=500 size=250).",
|
|
64
|
+
] = 0,
|
|
65
|
+
size: Annotated[
|
|
66
|
+
int,
|
|
67
|
+
"Maximum number of results to return in this request. Default is 250, max is 250. "
|
|
68
|
+
"The LLM should control pagination by making multiple calls with different 'from' values.",
|
|
69
|
+
] = 250,
|
|
70
|
+
entity_types: Annotated[
|
|
71
|
+
list[str] | None,
|
|
72
|
+
"Optional list of entity types to search. Valid values: 'driveItem', 'listItem', "
|
|
73
|
+
"'site', 'list', 'drive'. Default: ['driveItem', 'listItem']. "
|
|
74
|
+
"Multiple types can be specified.",
|
|
75
|
+
] = None,
|
|
76
|
+
filters: Annotated[
|
|
77
|
+
list[str] | None,
|
|
78
|
+
"Optional list of KQL filter expressions to refine search results "
|
|
79
|
+
"(e.g., ['fileType:docx', 'size>1000']).",
|
|
80
|
+
] = None,
|
|
81
|
+
include_hidden_content: Annotated[
|
|
82
|
+
bool,
|
|
83
|
+
"Whether to include hidden content in search results. Only works with delegated "
|
|
84
|
+
"permissions, not application permissions. Default: False.",
|
|
85
|
+
] = False,
|
|
86
|
+
region: Annotated[
|
|
87
|
+
str | None,
|
|
88
|
+
"Optional region code for application permissions (e.g., 'NAM', 'EUR', 'APC'). "
|
|
89
|
+
"Required when using application permissions to search SharePoint content in "
|
|
90
|
+
"specific regions.",
|
|
91
|
+
] = None,
|
|
92
|
+
) -> ToolResult | ToolError:
|
|
93
|
+
"""
|
|
94
|
+
Search for SharePoint and OneDrive content using Microsoft Graph Search API.
|
|
95
|
+
|
|
96
|
+
Search Scope:
|
|
97
|
+
- When site_url or site_id is provided: searches within the specified SharePoint site
|
|
98
|
+
- When neither is provided: searches across all accessible SharePoint sites and OneDrive
|
|
99
|
+
|
|
100
|
+
Supported Entity Types:
|
|
101
|
+
- driveItem: Files and folders in document libraries and OneDrive
|
|
102
|
+
- listItem: Items in SharePoint lists
|
|
103
|
+
- site: SharePoint sites
|
|
104
|
+
- list: SharePoint lists
|
|
105
|
+
- drive: Document libraries/drives
|
|
106
|
+
|
|
107
|
+
Filtering:
|
|
108
|
+
- Filters use KQL (Keyword Query Language) syntax
|
|
109
|
+
- Multiple filters are combined with AND operators
|
|
110
|
+
- Examples: ['fileType:docx', 'size>1000', 'lastModifiedTime>2024-01-01']
|
|
111
|
+
- Filters are applied in addition to the search query
|
|
112
|
+
|
|
113
|
+
Pagination:
|
|
114
|
+
- Controlled via from_offset (zero-based index) and size parameters
|
|
115
|
+
- Maximum size per request: 250 results
|
|
116
|
+
- To paginate: increment from_offset by size value for each subsequent page
|
|
117
|
+
- Example pagination sequence:
|
|
118
|
+
* Page 1: from_offset=0, size=250 (returns results 0-249)
|
|
119
|
+
* Page 2: from_offset=250, size=250 (returns results 250-499)
|
|
120
|
+
* Page 3: from_offset=500, size=250 (returns results 500-749)
|
|
121
|
+
|
|
122
|
+
API Reference:
|
|
123
|
+
- Endpoint: POST /search/query
|
|
124
|
+
- Documentation: https://learn.microsoft.com/en-us/graph/api/search-query
|
|
125
|
+
- Search concepts: https://learn.microsoft.com/en-us/graph/search-concept-files
|
|
126
|
+
|
|
127
|
+
Permissions:
|
|
128
|
+
- Requires Sites.Read.All or Sites.Search.All permission
|
|
129
|
+
- include_hidden_content only works with delegated permissions
|
|
130
|
+
- region parameter is required for application permissions in multi-region environments
|
|
131
|
+
"""
|
|
132
|
+
if not search_query:
|
|
133
|
+
raise ToolError("Argument validation error: 'search_query' cannot be empty.")
|
|
134
|
+
|
|
135
|
+
# Validate site_url if provided
|
|
136
|
+
if site_url:
|
|
137
|
+
validation_error = validate_site_url(site_url)
|
|
138
|
+
if validation_error:
|
|
139
|
+
raise ToolError(validation_error)
|
|
140
|
+
|
|
141
|
+
access_token = await get_microsoft_graph_access_token()
|
|
142
|
+
if isinstance(access_token, ToolError):
|
|
143
|
+
raise access_token
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
async with MicrosoftGraphClient(access_token=access_token, site_url=site_url) as client:
|
|
147
|
+
items = await client.search_content(
|
|
148
|
+
search_query=search_query,
|
|
149
|
+
site_id=site_id,
|
|
150
|
+
from_offset=from_offset,
|
|
151
|
+
size=size,
|
|
152
|
+
entity_types=entity_types,
|
|
153
|
+
filters=filters,
|
|
154
|
+
include_hidden_content=include_hidden_content,
|
|
155
|
+
region=region,
|
|
156
|
+
)
|
|
157
|
+
except MicrosoftGraphError as e:
|
|
158
|
+
logger.error(f"Microsoft Graph error searching content: {e}")
|
|
159
|
+
raise ToolError(str(e))
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"Unexpected error searching Microsoft Graph content: {e}", exc_info=True)
|
|
162
|
+
raise ToolError(
|
|
163
|
+
f"An unexpected error occurred while searching Microsoft Graph content: {str(e)}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
results = []
|
|
167
|
+
for item in items:
|
|
168
|
+
result_dict = {
|
|
169
|
+
"id": item.id, # Unique ID of the file, folder, or list item
|
|
170
|
+
"name": item.name,
|
|
171
|
+
"webUrl": item.web_url,
|
|
172
|
+
"size": item.size,
|
|
173
|
+
"createdDateTime": item.created_datetime,
|
|
174
|
+
"lastModifiedDateTime": item.last_modified_datetime,
|
|
175
|
+
"isFolder": item.is_folder,
|
|
176
|
+
"mimeType": item.mime_type,
|
|
177
|
+
# Document library/drive ID (driveId in Microsoft Graph API)
|
|
178
|
+
"documentLibraryId": item.drive_id,
|
|
179
|
+
"parentFolderId": item.parent_folder_id, # Parent folder ID
|
|
180
|
+
}
|
|
181
|
+
results.append(result_dict)
|
|
182
|
+
|
|
183
|
+
n = len(results)
|
|
184
|
+
return ToolResult(
|
|
185
|
+
content=(
|
|
186
|
+
f"Successfully searched Microsoft Graph and retrieved {n} result(s) for "
|
|
187
|
+
f"'{search_query}' (from={from_offset}, size={size})."
|
|
188
|
+
),
|
|
189
|
+
structured_content={
|
|
190
|
+
"query": search_query,
|
|
191
|
+
"siteUrl": site_url,
|
|
192
|
+
"siteId": site_id,
|
|
193
|
+
"from": from_offset,
|
|
194
|
+
"size": size,
|
|
195
|
+
"results": results,
|
|
196
|
+
"count": n,
|
|
197
|
+
},
|
|
198
|
+
)
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
import json
|
|
15
16
|
import logging
|
|
16
17
|
import os
|
|
17
18
|
from typing import Annotated
|
|
@@ -28,14 +29,15 @@ logger = logging.getLogger(__name__)
|
|
|
28
29
|
|
|
29
30
|
@dr_mcp_tool(tags={"predictive", "data", "write", "upload", "catalog"})
|
|
30
31
|
async def upload_dataset_to_ai_catalog(
|
|
32
|
+
*,
|
|
31
33
|
file_path: Annotated[str, "The path to the dataset file to upload."] | None = None,
|
|
32
34
|
file_url: Annotated[str, "The URL to the dataset file to upload."] | None = None,
|
|
33
35
|
) -> ToolError | ToolResult:
|
|
34
36
|
"""Upload a dataset to the DataRobot AI Catalog / Data Registry."""
|
|
35
37
|
if not file_path and not file_url:
|
|
36
|
-
|
|
38
|
+
raise ToolError("Either file_path or file_url must be provided.")
|
|
37
39
|
if file_path and file_url:
|
|
38
|
-
|
|
40
|
+
raise ToolError("Please provide either file_path or file_url, not both.")
|
|
39
41
|
|
|
40
42
|
# Get client
|
|
41
43
|
client = get_sdk_client()
|
|
@@ -45,17 +47,17 @@ async def upload_dataset_to_ai_catalog(
|
|
|
45
47
|
# Does file exist?
|
|
46
48
|
if not os.path.exists(file_path):
|
|
47
49
|
logger.error("File not found: %s", file_path)
|
|
48
|
-
|
|
50
|
+
raise ToolError(f"File not found: {file_path}")
|
|
49
51
|
catalog_item = client.Dataset.create_from_file(file_path)
|
|
50
52
|
else:
|
|
51
53
|
# Does URL exist?
|
|
52
54
|
if file_url is None or not is_valid_url(file_url):
|
|
53
55
|
logger.error("Invalid file URL: %s", file_url)
|
|
54
|
-
|
|
56
|
+
raise ToolError(f"Invalid file URL: {file_url}")
|
|
55
57
|
catalog_item = client.Dataset.create_from_url(file_url)
|
|
56
58
|
|
|
57
59
|
if not catalog_item:
|
|
58
|
-
|
|
60
|
+
raise ToolError("Failed to upload dataset.")
|
|
59
61
|
|
|
60
62
|
return ToolResult(
|
|
61
63
|
content=f"Successfully uploaded dataset: {catalog_item.id}",
|
|
@@ -80,11 +82,17 @@ async def list_ai_catalog_items() -> ToolResult:
|
|
|
80
82
|
structured_content={"datasets": []},
|
|
81
83
|
)
|
|
82
84
|
|
|
85
|
+
datasets_dict = {ds.id: ds.name for ds in datasets}
|
|
86
|
+
datasets_count = len(datasets)
|
|
87
|
+
|
|
83
88
|
return ToolResult(
|
|
84
|
-
content=
|
|
89
|
+
content=(
|
|
90
|
+
f"Found {datasets_count} AI Catalog items, here are the details:\n"
|
|
91
|
+
f"{json.dumps(datasets_dict, indent=2)}"
|
|
92
|
+
),
|
|
85
93
|
structured_content={
|
|
86
|
-
"datasets":
|
|
87
|
-
"count":
|
|
94
|
+
"datasets": datasets_dict,
|
|
95
|
+
"count": datasets_count,
|
|
88
96
|
},
|
|
89
97
|
)
|
|
90
98
|
|