datarobot-genai 0.2.39__py3-none-any.whl → 0.3.1__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/agents/__init__.py +1 -1
- datarobot_genai/core/agents/base.py +5 -2
- datarobot_genai/core/chat/responses.py +6 -1
- datarobot_genai/core/utils/auth.py +188 -31
- datarobot_genai/crewai/__init__.py +1 -4
- datarobot_genai/crewai/agent.py +150 -17
- datarobot_genai/crewai/events.py +11 -4
- datarobot_genai/drmcp/__init__.py +4 -2
- datarobot_genai/drmcp/core/config.py +21 -1
- datarobot_genai/drmcp/core/mcp_instance.py +5 -49
- datarobot_genai/drmcp/core/routes.py +108 -13
- datarobot_genai/drmcp/core/tool_config.py +16 -0
- datarobot_genai/drmcp/core/utils.py +110 -0
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +41 -26
- datarobot_genai/drmcp/tools/clients/gdrive.py +2 -0
- datarobot_genai/drmcp/tools/clients/microsoft_graph.py +96 -0
- datarobot_genai/drmcp/tools/clients/perplexity.py +173 -0
- datarobot_genai/drmcp/tools/clients/tavily.py +199 -0
- datarobot_genai/drmcp/tools/confluence/tools.py +0 -5
- datarobot_genai/drmcp/tools/gdrive/tools.py +12 -59
- datarobot_genai/drmcp/tools/jira/tools.py +4 -8
- datarobot_genai/drmcp/tools/microsoft_graph/tools.py +135 -19
- datarobot_genai/drmcp/tools/perplexity/__init__.py +0 -0
- datarobot_genai/drmcp/tools/perplexity/tools.py +117 -0
- datarobot_genai/drmcp/tools/predictive/data.py +1 -9
- datarobot_genai/drmcp/tools/predictive/deployment.py +0 -8
- datarobot_genai/drmcp/tools/predictive/deployment_info.py +0 -19
- datarobot_genai/drmcp/tools/predictive/model.py +0 -21
- datarobot_genai/drmcp/tools/predictive/predict_realtime.py +3 -0
- datarobot_genai/drmcp/tools/predictive/project.py +3 -19
- datarobot_genai/drmcp/tools/predictive/training.py +1 -19
- datarobot_genai/drmcp/tools/tavily/__init__.py +13 -0
- datarobot_genai/drmcp/tools/tavily/tools.py +141 -0
- datarobot_genai/langgraph/agent.py +10 -2
- datarobot_genai/llama_index/__init__.py +1 -1
- datarobot_genai/llama_index/agent.py +284 -5
- datarobot_genai/nat/agent.py +17 -6
- {datarobot_genai-0.2.39.dist-info → datarobot_genai-0.3.1.dist-info}/METADATA +3 -1
- {datarobot_genai-0.2.39.dist-info → datarobot_genai-0.3.1.dist-info}/RECORD +43 -40
- datarobot_genai/crewai/base.py +0 -159
- datarobot_genai/drmcp/core/tool_filter.py +0 -117
- datarobot_genai/llama_index/base.py +0 -299
- {datarobot_genai-0.2.39.dist-info → datarobot_genai-0.3.1.dist-info}/WHEEL +0 -0
- {datarobot_genai-0.2.39.dist-info → datarobot_genai-0.3.1.dist-info}/entry_points.txt +0 -0
- {datarobot_genai-0.2.39.dist-info → datarobot_genai-0.3.1.dist-info}/licenses/AUTHORS +0 -0
- {datarobot_genai-0.2.39.dist-info → datarobot_genai-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -25,7 +25,6 @@ from fastmcp.prompts.prompt import Prompt
|
|
|
25
25
|
from fastmcp.server.dependencies import get_context
|
|
26
26
|
from fastmcp.tools import Tool
|
|
27
27
|
from mcp.types import AnyFunction
|
|
28
|
-
from mcp.types import Tool as MCPTool
|
|
29
28
|
from mcp.types import ToolAnnotations
|
|
30
29
|
from typing_extensions import Unpack
|
|
31
30
|
|
|
@@ -36,8 +35,6 @@ from .logging import log_execution
|
|
|
36
35
|
from .memory_management.manager import MemoryManager
|
|
37
36
|
from .memory_management.manager import get_memory_manager
|
|
38
37
|
from .telemetry import trace_execution
|
|
39
|
-
from .tool_filter import filter_tools_by_tags
|
|
40
|
-
from .tool_filter import list_all_tags
|
|
41
38
|
|
|
42
39
|
logger = logging.getLogger(__name__)
|
|
43
40
|
|
|
@@ -80,10 +77,8 @@ async def get_agent_and_storage_ids(
|
|
|
80
77
|
return agent_id, storage_id
|
|
81
78
|
|
|
82
79
|
|
|
83
|
-
class
|
|
84
|
-
"""Extended FastMCP that supports
|
|
85
|
-
tool decorator.
|
|
86
|
-
"""
|
|
80
|
+
class DataRobotMCP(FastMCP):
|
|
81
|
+
"""Extended FastMCP that supports DataRobot specific features like deployments and prompts."""
|
|
87
82
|
|
|
88
83
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
89
84
|
super().__init__(*args, **kwargs)
|
|
@@ -118,45 +113,6 @@ class TaggedFastMCP(FastMCP):
|
|
|
118
113
|
"In stateless mode, clients will see changes on next request."
|
|
119
114
|
)
|
|
120
115
|
|
|
121
|
-
async def list_tools(
|
|
122
|
-
self, tags: list[str] | None = None, match_all: bool = False
|
|
123
|
-
) -> list[MCPTool]:
|
|
124
|
-
"""
|
|
125
|
-
List all available tools, optionally filtered by tags.
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
tags: Optional list of tags to filter by. If None, returns all tools.
|
|
129
|
-
match_all: If True, tool must have all specified tags (AND logic).
|
|
130
|
-
If False, tool must have at least one tag (OR logic).
|
|
131
|
-
Only used when tags is provided.
|
|
132
|
-
|
|
133
|
-
Returns
|
|
134
|
-
-------
|
|
135
|
-
List of MCPTool objects that match the tag criteria.
|
|
136
|
-
"""
|
|
137
|
-
# Get all tools from the parent class
|
|
138
|
-
all_tools = await super()._list_tools_mcp()
|
|
139
|
-
|
|
140
|
-
# If no tags specified, return all tools
|
|
141
|
-
if not tags:
|
|
142
|
-
return all_tools
|
|
143
|
-
|
|
144
|
-
# Filter tools by tags
|
|
145
|
-
filtered_tools = filter_tools_by_tags(list(all_tools), tags, match_all)
|
|
146
|
-
|
|
147
|
-
return filtered_tools # type: ignore[return-value]
|
|
148
|
-
|
|
149
|
-
async def get_all_tags(self) -> list[str]:
|
|
150
|
-
"""
|
|
151
|
-
Get all unique tags from all registered tools.
|
|
152
|
-
|
|
153
|
-
Returns
|
|
154
|
-
-------
|
|
155
|
-
List of all unique tags sorted alphabetically.
|
|
156
|
-
"""
|
|
157
|
-
all_tools = await self._list_tools_mcp()
|
|
158
|
-
return list_all_tags(list(all_tools))
|
|
159
|
-
|
|
160
116
|
async def get_deployment_mapping(self) -> dict[str, str]:
|
|
161
117
|
"""
|
|
162
118
|
Get the list of deployment IDs for all registered dynamic tools.
|
|
@@ -279,10 +235,10 @@ class TaggedFastMCP(FastMCP):
|
|
|
279
235
|
)
|
|
280
236
|
|
|
281
237
|
|
|
282
|
-
# Create the
|
|
238
|
+
# Create the DataRobot MCP instance
|
|
283
239
|
mcp_server_configs: MCPServerConfig = get_config()
|
|
284
240
|
|
|
285
|
-
mcp =
|
|
241
|
+
mcp = DataRobotMCP(
|
|
286
242
|
name=mcp_server_configs.mcp_server_name,
|
|
287
243
|
on_duplicate_tools=mcp_server_configs.tool_registration_duplicate_behavior,
|
|
288
244
|
on_duplicate_prompts=mcp_server_configs.prompt_registration_duplicate_behavior,
|
|
@@ -446,7 +402,7 @@ async def register_tools(
|
|
|
446
402
|
await mcp.set_deployment_mapping(deployment_id, tool_name)
|
|
447
403
|
|
|
448
404
|
# Verify tool is registered
|
|
449
|
-
tools = await mcp.
|
|
405
|
+
tools = await mcp._list_tools_mcp()
|
|
450
406
|
if not any(tool.name == tool_name for tool in tools):
|
|
451
407
|
raise RuntimeError(f"Tool {tool_name} was not registered successfully")
|
|
452
408
|
logger.info(f"Registered tools: {len(tools)}")
|
|
@@ -18,20 +18,28 @@ from botocore.exceptions import ClientError
|
|
|
18
18
|
from starlette.requests import Request
|
|
19
19
|
from starlette.responses import JSONResponse
|
|
20
20
|
|
|
21
|
+
from datarobot_genai import __version__
|
|
22
|
+
|
|
23
|
+
from .config import get_config
|
|
21
24
|
from .dynamic_prompts.controllers import delete_registered_prompt_template
|
|
22
25
|
from .dynamic_prompts.controllers import refresh_registered_prompt_template
|
|
23
26
|
from .dynamic_prompts.controllers import register_prompt_from_prompt_template_id_and_version
|
|
24
27
|
from .dynamic_tools.deployment.controllers import delete_registered_tool_deployment
|
|
25
28
|
from .dynamic_tools.deployment.controllers import get_registered_tool_deployments
|
|
26
29
|
from .dynamic_tools.deployment.controllers import register_tool_for_deployment_id
|
|
27
|
-
from .mcp_instance import
|
|
30
|
+
from .mcp_instance import DataRobotMCP
|
|
28
31
|
from .memory_management.manager import get_memory_manager
|
|
29
32
|
from .routes_utils import prefix_mount_path
|
|
33
|
+
from .tool_config import TOOL_CONFIGS
|
|
34
|
+
from .tool_config import ToolType
|
|
35
|
+
from .utils import get_prompt_tags
|
|
36
|
+
from .utils import get_resource_tags
|
|
37
|
+
from .utils import get_tool_tags
|
|
30
38
|
|
|
31
39
|
logger = getLogger(__name__)
|
|
32
40
|
|
|
33
41
|
|
|
34
|
-
def register_routes(mcp:
|
|
42
|
+
def register_routes(mcp: DataRobotMCP) -> None:
|
|
35
43
|
"""Register all routes with the MCP server."""
|
|
36
44
|
|
|
37
45
|
@mcp.custom_route(prefix_mount_path("/"), methods=["GET"])
|
|
@@ -44,26 +52,113 @@ def register_routes(mcp: TaggedFastMCP) -> None:
|
|
|
44
52
|
},
|
|
45
53
|
)
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
@mcp.custom_route(prefix_mount_path("/metadata"), methods=["GET"])
|
|
56
|
+
async def get_metadata(_: Request) -> JSONResponse:
|
|
57
|
+
"""Get metadata about tools, prompts, resources, and system configuration."""
|
|
50
58
|
try:
|
|
51
|
-
#
|
|
52
|
-
|
|
59
|
+
# Get tools with tags
|
|
60
|
+
tools = await mcp._list_tools_mcp()
|
|
61
|
+
tools_metadata = [
|
|
62
|
+
{
|
|
63
|
+
"name": tool.name,
|
|
64
|
+
"tags": sorted(list(get_tool_tags(tool))),
|
|
65
|
+
}
|
|
66
|
+
for tool in tools
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Get prompts with tags
|
|
70
|
+
prompts = await mcp._list_prompts_mcp()
|
|
71
|
+
prompts_metadata = [
|
|
72
|
+
{
|
|
73
|
+
"name": prompt.name,
|
|
74
|
+
"tags": sorted(list(get_prompt_tags(prompt))),
|
|
75
|
+
}
|
|
76
|
+
for prompt in prompts
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# Get resources with tags
|
|
80
|
+
resources = await mcp._list_resources_mcp()
|
|
81
|
+
resources_metadata = [
|
|
82
|
+
{
|
|
83
|
+
"name": resource.name,
|
|
84
|
+
"tags": sorted(list(get_resource_tags(resource))),
|
|
85
|
+
}
|
|
86
|
+
for resource in resources
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
# Get safe configuration details
|
|
90
|
+
config = get_config()
|
|
91
|
+
|
|
92
|
+
# Build tool config status
|
|
93
|
+
tool_config_status = {}
|
|
94
|
+
for tool_type in ToolType:
|
|
95
|
+
tool_config = TOOL_CONFIGS[tool_type]
|
|
96
|
+
is_enabled = getattr(config.tool_config, tool_config["config_field_name"], False)
|
|
97
|
+
oauth_check_fn = tool_config["oauth_check"]
|
|
98
|
+
oauth_required = oauth_check_fn is not None
|
|
99
|
+
oauth_configured = None
|
|
100
|
+
if oauth_required and oauth_check_fn is not None:
|
|
101
|
+
oauth_configured = oauth_check_fn(config)
|
|
102
|
+
|
|
103
|
+
tool_config_status[tool_type.value] = {
|
|
104
|
+
"enabled": is_enabled,
|
|
105
|
+
"oauth_required": oauth_required,
|
|
106
|
+
"oauth_configured": oauth_configured,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Safe config details (excluding sensitive information)
|
|
110
|
+
safe_config = {
|
|
111
|
+
"server": {
|
|
112
|
+
"name": config.mcp_server_name,
|
|
113
|
+
"port": config.mcp_server_port,
|
|
114
|
+
"log_level": config.mcp_server_log_level,
|
|
115
|
+
"app_log_level": config.app_log_level,
|
|
116
|
+
"mount_path": config.mount_path,
|
|
117
|
+
"drmcp_genai_version": __version__,
|
|
118
|
+
},
|
|
119
|
+
"features": {
|
|
120
|
+
"register_dynamic_tools_on_startup": (
|
|
121
|
+
config.mcp_server_register_dynamic_tools_on_startup
|
|
122
|
+
),
|
|
123
|
+
"register_dynamic_prompts_on_startup": (
|
|
124
|
+
config.mcp_server_register_dynamic_prompts_on_startup
|
|
125
|
+
),
|
|
126
|
+
"tool_registration_allow_empty_schema": (
|
|
127
|
+
config.tool_registration_allow_empty_schema
|
|
128
|
+
),
|
|
129
|
+
"tool_registration_duplicate_behavior": (
|
|
130
|
+
config.tool_registration_duplicate_behavior
|
|
131
|
+
),
|
|
132
|
+
"prompt_registration_duplicate_behavior": (
|
|
133
|
+
config.prompt_registration_duplicate_behavior
|
|
134
|
+
),
|
|
135
|
+
},
|
|
136
|
+
"tool_config": tool_config_status,
|
|
137
|
+
}
|
|
138
|
+
|
|
53
139
|
return JSONResponse(
|
|
54
140
|
status_code=HTTPStatus.OK,
|
|
55
141
|
content={
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
142
|
+
"tools": {
|
|
143
|
+
"items": tools_metadata,
|
|
144
|
+
"count": len(tools_metadata),
|
|
145
|
+
},
|
|
146
|
+
"prompts": {
|
|
147
|
+
"items": prompts_metadata,
|
|
148
|
+
"count": len(prompts_metadata),
|
|
149
|
+
},
|
|
150
|
+
"resources": {
|
|
151
|
+
"items": resources_metadata,
|
|
152
|
+
"count": len(resources_metadata),
|
|
153
|
+
},
|
|
154
|
+
"config": safe_config,
|
|
59
155
|
},
|
|
60
156
|
)
|
|
61
157
|
except Exception as e:
|
|
158
|
+
logger.exception("Failed to retrieve metadata")
|
|
62
159
|
return JSONResponse(
|
|
63
160
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
64
|
-
content={
|
|
65
|
-
"error": f"Failed to retrieve tags: {str(e)}",
|
|
66
|
-
},
|
|
161
|
+
content={"error": f"Failed to retrieve metadata: {str(e)}"},
|
|
67
162
|
)
|
|
68
163
|
|
|
69
164
|
memory_manager = get_memory_manager()
|
|
@@ -31,6 +31,8 @@ class ToolType(str, Enum):
|
|
|
31
31
|
CONFLUENCE = "confluence"
|
|
32
32
|
GDRIVE = "gdrive"
|
|
33
33
|
MICROSOFT_GRAPH = "microsoft_graph"
|
|
34
|
+
PERPLEXITY = "perplexity"
|
|
35
|
+
TAVILY = "tavily"
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
class ToolConfig(TypedDict):
|
|
@@ -80,6 +82,20 @@ TOOL_CONFIGS: dict[ToolType, ToolConfig] = {
|
|
|
80
82
|
package_prefix="datarobot_genai.drmcp.tools.microsoft_graph",
|
|
81
83
|
config_field_name="enable_microsoft_graph_tools",
|
|
82
84
|
),
|
|
85
|
+
ToolType.PERPLEXITY: ToolConfig(
|
|
86
|
+
name="perplexity",
|
|
87
|
+
oauth_check=None, # OAuth for Perplexity is not supported
|
|
88
|
+
directory="perplexity",
|
|
89
|
+
package_prefix="datarobot_genai.drmcp.tools.perplexity",
|
|
90
|
+
config_field_name="enable_perplexity_tools",
|
|
91
|
+
),
|
|
92
|
+
ToolType.TAVILY: ToolConfig(
|
|
93
|
+
name="tavily",
|
|
94
|
+
oauth_check=None,
|
|
95
|
+
directory="tavily",
|
|
96
|
+
package_prefix="datarobot_genai.drmcp.tools.tavily",
|
|
97
|
+
config_field_name="enable_tavily_tools",
|
|
98
|
+
),
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
|
|
@@ -18,7 +18,11 @@ from urllib.parse import urlparse
|
|
|
18
18
|
|
|
19
19
|
import boto3
|
|
20
20
|
from fastmcp.resources import HttpResource
|
|
21
|
+
from fastmcp.tools import Tool
|
|
21
22
|
from fastmcp.tools.tool import ToolResult
|
|
23
|
+
from mcp.types import Prompt as MCPPrompt
|
|
24
|
+
from mcp.types import Resource as MCPResource
|
|
25
|
+
from mcp.types import Tool as MCPTool
|
|
22
26
|
from pydantic import BaseModel
|
|
23
27
|
|
|
24
28
|
from .constants import MAX_INLINE_SIZE
|
|
@@ -136,3 +140,109 @@ def is_valid_url(url: str) -> bool:
|
|
|
136
140
|
"""Check if a URL is valid."""
|
|
137
141
|
result = urlparse(url)
|
|
138
142
|
return all([result.scheme, result.netloc])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_prompt_tags(prompt: MCPPrompt) -> set[str]:
|
|
146
|
+
"""
|
|
147
|
+
Extract tags from a prompt.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
prompt: MCP protocol Prompt
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
Set of tag strings, empty set if no tags found
|
|
155
|
+
"""
|
|
156
|
+
# MCPPrompt has tags in meta._fastmcp.tags (as a list)
|
|
157
|
+
if not (prompt.meta and isinstance(prompt.meta, dict)):
|
|
158
|
+
return set()
|
|
159
|
+
|
|
160
|
+
fastmcp_meta = prompt.meta.get("_fastmcp")
|
|
161
|
+
if not (fastmcp_meta and isinstance(fastmcp_meta, dict)):
|
|
162
|
+
return set()
|
|
163
|
+
|
|
164
|
+
tags = fastmcp_meta.get("tags")
|
|
165
|
+
return set(tags) if tags else set()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_resource_tags(resource: MCPResource) -> set[str]:
|
|
169
|
+
"""
|
|
170
|
+
Extract tags from a resource.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
resource: MCP protocol Resource
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
Set of tag strings, empty set if no tags found
|
|
178
|
+
"""
|
|
179
|
+
# MCPResource has tags in meta._fastmcp.tags (as a list)
|
|
180
|
+
if not (resource.meta and isinstance(resource.meta, dict)):
|
|
181
|
+
return set()
|
|
182
|
+
|
|
183
|
+
fastmcp_meta = resource.meta.get("_fastmcp")
|
|
184
|
+
if not (fastmcp_meta and isinstance(fastmcp_meta, dict)):
|
|
185
|
+
return set()
|
|
186
|
+
|
|
187
|
+
tags = fastmcp_meta.get("tags")
|
|
188
|
+
return set(tags) if tags else set()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_tool_tags(tool: Tool | MCPTool) -> set[str]:
|
|
192
|
+
"""
|
|
193
|
+
Extract tags from a tool, handling both FastMCP Tool and MCP protocol Tool types.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
tool: Either a FastMCP Tool or MCP protocol Tool
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
Set of tag strings, empty set if no tags found
|
|
201
|
+
"""
|
|
202
|
+
if isinstance(tool, Tool):
|
|
203
|
+
# FastMCP Tool has tags directly as a set
|
|
204
|
+
return getattr(tool, "tags", None) or set()
|
|
205
|
+
|
|
206
|
+
# MCPTool has tags in meta._fastmcp.tags (as a list)
|
|
207
|
+
if not (tool.meta and isinstance(tool.meta, dict)):
|
|
208
|
+
return set()
|
|
209
|
+
|
|
210
|
+
fastmcp_meta = tool.meta.get("_fastmcp")
|
|
211
|
+
if not (fastmcp_meta and isinstance(fastmcp_meta, dict)):
|
|
212
|
+
return set()
|
|
213
|
+
|
|
214
|
+
tags = fastmcp_meta.get("tags")
|
|
215
|
+
return set(tags) if tags else set()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def filter_tools_by_tags(
|
|
219
|
+
*,
|
|
220
|
+
tools: list[Tool | MCPTool],
|
|
221
|
+
tags: list[str] | None = None,
|
|
222
|
+
match_all: bool = False,
|
|
223
|
+
) -> list[Tool | MCPTool]:
|
|
224
|
+
"""
|
|
225
|
+
Filter tools by tags.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
tools: List of tools to filter
|
|
229
|
+
tags: List of tags to filter by. If None, returns all tools
|
|
230
|
+
match_all: If True, tool must have all specified tags. If False, tool must have at least
|
|
231
|
+
one tag.
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
List of tools that match the tag criteria
|
|
236
|
+
"""
|
|
237
|
+
if not tags:
|
|
238
|
+
return tools
|
|
239
|
+
|
|
240
|
+
# Convert tags to set for O(1) lookup instead of O(n)
|
|
241
|
+
tags_set = set(tags)
|
|
242
|
+
|
|
243
|
+
return [
|
|
244
|
+
tool
|
|
245
|
+
for tool in tools
|
|
246
|
+
if (tool_tags := get_tool_tags(tool))
|
|
247
|
+
and (tags_set.issubset(tool_tags) if match_all else tags_set & tool_tags)
|
|
248
|
+
]
|
|
@@ -39,6 +39,17 @@ class ETETestExpectations(BaseModel):
|
|
|
39
39
|
SHOULD_NOT_BE_EMPTY = "SHOULD_NOT_BE_EMPTY"
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def _extract_content_when_structured_empty(tool_result: str) -> dict[str, str] | None:
|
|
43
|
+
r"""When Content is present but Structured content is empty, return {"error": content}."""
|
|
44
|
+
if "Content: " not in tool_result:
|
|
45
|
+
return None
|
|
46
|
+
content_part = tool_result.split("Content: ", 1)[1]
|
|
47
|
+
if "\nStructured content: " in content_part:
|
|
48
|
+
content_part = content_part.split("\nStructured content: ", 1)[0]
|
|
49
|
+
content_part = content_part.strip()
|
|
50
|
+
return {"error": content_part} if content_part else None
|
|
51
|
+
|
|
52
|
+
|
|
42
53
|
def _extract_structured_content(tool_result: str) -> Any:
|
|
43
54
|
r"""
|
|
44
55
|
Extract and parse structured content from tool result string.
|
|
@@ -49,7 +60,9 @@ def _extract_structured_content(tool_result: str) -> Any:
|
|
|
49
60
|
Structured content can be:
|
|
50
61
|
1. A JSON object with a "result" key: {"result": "..."} or {"result": "{...}"}
|
|
51
62
|
2. A direct JSON object: {"key": "value", ...}
|
|
52
|
-
3. Empty or missing
|
|
63
|
+
3. Empty or missing — when Content is present but Structured content is empty
|
|
64
|
+
(e.g. tool errors), returns {"error": content} so dict expectations can validate.
|
|
65
|
+
4. None if neither valid structured content nor Content is available
|
|
53
66
|
|
|
54
67
|
Args:
|
|
55
68
|
tool_result: The tool result string
|
|
@@ -58,33 +71,35 @@ def _extract_structured_content(tool_result: str) -> Any:
|
|
|
58
71
|
-------
|
|
59
72
|
Parsed structured content, or None if not available
|
|
60
73
|
"""
|
|
61
|
-
|
|
62
|
-
if not tool_result
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
try:
|
|
70
|
-
structured_data = json.loads(structured_part)
|
|
71
|
-
except json.JSONDecodeError:
|
|
72
|
-
return None
|
|
73
|
-
|
|
74
|
-
# If structured data has a "result" key, extract and parse that
|
|
75
|
-
if isinstance(structured_data, dict) and "result" in structured_data:
|
|
76
|
-
result_value = structured_data["result"]
|
|
77
|
-
# If result is a JSON string (starts with { or [), try to parse it
|
|
78
|
-
if isinstance(result_value, str) and result_value.strip().startswith(("{", "[")):
|
|
74
|
+
result: Any = None
|
|
75
|
+
if not tool_result:
|
|
76
|
+
pass
|
|
77
|
+
elif "Structured content: " in tool_result:
|
|
78
|
+
structured_part = tool_result.split("Structured content: ", 1)[1].strip()
|
|
79
|
+
if not structured_part:
|
|
80
|
+
result = _extract_content_when_structured_empty(tool_result)
|
|
81
|
+
else:
|
|
79
82
|
try:
|
|
80
|
-
|
|
83
|
+
structured_data = json.loads(structured_part)
|
|
81
84
|
except json.JSONDecodeError:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
pass
|
|
86
|
+
else:
|
|
87
|
+
if isinstance(structured_data, dict) and "result" in structured_data:
|
|
88
|
+
result_value = structured_data["result"]
|
|
89
|
+
if isinstance(result_value, str) and result_value.strip().startswith(
|
|
90
|
+
("{", "[")
|
|
91
|
+
):
|
|
92
|
+
try:
|
|
93
|
+
result = json.loads(result_value)
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
result = result_value
|
|
96
|
+
else:
|
|
97
|
+
result = result_value
|
|
98
|
+
else:
|
|
99
|
+
result = structured_data
|
|
100
|
+
else:
|
|
101
|
+
result = _extract_content_when_structured_empty(tool_result)
|
|
102
|
+
return result
|
|
88
103
|
|
|
89
104
|
|
|
90
105
|
def _check_dict_has_keys(
|
|
@@ -586,6 +586,102 @@ class MicrosoftGraphClient:
|
|
|
586
586
|
f"Microsoft Graph API error {response.status_code}: {response.text}"
|
|
587
587
|
)
|
|
588
588
|
|
|
589
|
+
async def update_item_metadata(
|
|
590
|
+
self,
|
|
591
|
+
item_id: str,
|
|
592
|
+
fields_to_update: dict[str, Any],
|
|
593
|
+
site_id: str | None = None,
|
|
594
|
+
list_id: str | None = None,
|
|
595
|
+
drive_id: str | None = None,
|
|
596
|
+
) -> dict[str, Any]:
|
|
597
|
+
"""Update metadata on a SharePoint list item or OneDrive/SharePoint drive item.
|
|
598
|
+
|
|
599
|
+
For SharePoint list items: Updates custom column values via the fields endpoint.
|
|
600
|
+
For drive items: Updates properties like name and description.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
item_id: The ID of the item to update.
|
|
604
|
+
fields_to_update: Key-value pairs of metadata fields to modify.
|
|
605
|
+
site_id: For SharePoint list items - the site ID.
|
|
606
|
+
list_id: For SharePoint list items - the list ID.
|
|
607
|
+
drive_id: For OneDrive/drive items - the drive ID.
|
|
608
|
+
|
|
609
|
+
Returns
|
|
610
|
+
-------
|
|
611
|
+
The API response containing updated item data.
|
|
612
|
+
|
|
613
|
+
Raises
|
|
614
|
+
------
|
|
615
|
+
MicrosoftGraphError: If validation fails or the API request fails.
|
|
616
|
+
"""
|
|
617
|
+
if not item_id or not item_id.strip():
|
|
618
|
+
raise MicrosoftGraphError("item_id cannot be empty")
|
|
619
|
+
if not fields_to_update:
|
|
620
|
+
raise MicrosoftGraphError("fields_to_update cannot be empty")
|
|
621
|
+
|
|
622
|
+
# Determine the endpoint based on provided parameters
|
|
623
|
+
has_sharepoint_context = site_id is not None and list_id is not None
|
|
624
|
+
has_drive_context = drive_id is not None
|
|
625
|
+
|
|
626
|
+
if has_sharepoint_context and has_drive_context:
|
|
627
|
+
raise MicrosoftGraphError(
|
|
628
|
+
"Cannot specify both SharePoint (site_id + list_id) and OneDrive "
|
|
629
|
+
"(document_library_id) context. Choose one."
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
if not has_sharepoint_context and not has_drive_context:
|
|
633
|
+
raise MicrosoftGraphError(
|
|
634
|
+
"Must specify either SharePoint context (site_id + list_id) or "
|
|
635
|
+
"OneDrive context (document_library_id)."
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
try:
|
|
639
|
+
if has_sharepoint_context:
|
|
640
|
+
# PATCH /sites/{site-id}/lists/{list-id}/items/{item-id}/fields
|
|
641
|
+
url = f"{GRAPH_API_BASE}/sites/{site_id}/lists/{list_id}/items/{item_id}/fields"
|
|
642
|
+
response = await self._client.patch(url, json=fields_to_update)
|
|
643
|
+
else:
|
|
644
|
+
# Drive item: PATCH /drives/{drive-id}/items/{item-id}
|
|
645
|
+
url = f"{GRAPH_API_BASE}/drives/{drive_id}/items/{item_id}"
|
|
646
|
+
response = await self._client.patch(url, json=fields_to_update)
|
|
647
|
+
|
|
648
|
+
response.raise_for_status()
|
|
649
|
+
return response.json()
|
|
650
|
+
except httpx.HTTPStatusError as e:
|
|
651
|
+
raise self._handle_update_metadata_error(e, item_id) from e
|
|
652
|
+
|
|
653
|
+
def _handle_update_metadata_error(
|
|
654
|
+
self,
|
|
655
|
+
error: httpx.HTTPStatusError,
|
|
656
|
+
item_id: str,
|
|
657
|
+
) -> MicrosoftGraphError:
|
|
658
|
+
"""Handle HTTP errors for metadata updates and return appropriate MicrosoftGraphError."""
|
|
659
|
+
status_code = error.response.status_code
|
|
660
|
+
error_msg = f"Failed to update metadata: HTTP {status_code}"
|
|
661
|
+
|
|
662
|
+
if status_code == 400:
|
|
663
|
+
try:
|
|
664
|
+
error_data = error.response.json()
|
|
665
|
+
api_message = error_data.get("error", {}).get("message", "Invalid request")
|
|
666
|
+
error_msg = f"Bad request updating metadata: {api_message}"
|
|
667
|
+
except Exception:
|
|
668
|
+
error_msg = "Bad request: invalid field names or values."
|
|
669
|
+
elif status_code == 401:
|
|
670
|
+
error_msg = "Authentication failed. Access token may be expired or invalid."
|
|
671
|
+
elif status_code == 403:
|
|
672
|
+
error_msg = (
|
|
673
|
+
f"Permission denied: you don't have permission to update item '{item_id}'. "
|
|
674
|
+
"Requires Sites.ReadWrite.All or Files.ReadWrite.All permission."
|
|
675
|
+
)
|
|
676
|
+
elif status_code == 404:
|
|
677
|
+
error_msg = f"Item '{item_id}' not found."
|
|
678
|
+
elif status_code == 409:
|
|
679
|
+
error_msg = f"Conflict: item '{item_id}' may have been modified concurrently."
|
|
680
|
+
elif status_code == 429:
|
|
681
|
+
error_msg = "Rate limit exceeded. Please try again later."
|
|
682
|
+
|
|
683
|
+
return MicrosoftGraphError(error_msg)
|
|
684
|
+
|
|
589
685
|
async def __aenter__(self) -> "MicrosoftGraphClient":
|
|
590
686
|
"""Async context manager entry."""
|
|
591
687
|
return self
|