datarobot-genai 0.2.34__py3-none-any.whl → 0.2.39__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.
@@ -16,13 +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
24
25
  from datarobot_genai.drmcp.tools.clients.microsoft_graph import MicrosoftGraphClient
25
- from datarobot_genai.drmcp.tools.clients.microsoft_graph import MicrosoftGraphError
26
26
  from datarobot_genai.drmcp.tools.clients.microsoft_graph import get_microsoft_graph_access_token
27
27
  from datarobot_genai.drmcp.tools.clients.microsoft_graph import validate_site_url
28
28
 
@@ -142,25 +142,16 @@ async def microsoft_graph_search_content(
142
142
  if isinstance(access_token, ToolError):
143
143
  raise access_token
144
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)}"
145
+ async with MicrosoftGraphClient(access_token=access_token, site_url=site_url) as client:
146
+ items = await client.search_content(
147
+ search_query=search_query,
148
+ site_id=site_id,
149
+ from_offset=from_offset,
150
+ size=size,
151
+ entity_types=entity_types,
152
+ filters=filters,
153
+ include_hidden_content=include_hidden_content,
154
+ region=region,
164
155
  )
165
156
 
166
157
  results = []
@@ -196,3 +187,144 @@ async def microsoft_graph_search_content(
196
187
  "count": n,
197
188
  },
198
189
  )
190
+
191
+
192
+ @dr_mcp_tool(tags={"microsoft", "graph api", "sharepoint", "onedrive", "share"}, enabled=False)
193
+ async def microsoft_graph_share_item(
194
+ *,
195
+ file_id: Annotated[str, "The ID of the file or folder to share."],
196
+ document_library_id: Annotated[str, "The ID of the document library containing the item."],
197
+ recipient_emails: Annotated[list[str], "A list of email addresses to invite."],
198
+ role: Annotated[Literal["read", "write"], "The role to assign: 'read' or 'write'."] = "read",
199
+ send_invitation: Annotated[
200
+ bool, "Flag determining if recipients should be notified. Default False"
201
+ ] = False,
202
+ ) -> ToolResult | ToolError:
203
+ """
204
+ Share a SharePoint or Onedrive file or folder with one or more users.
205
+ It works with internal users or existing guest users in the
206
+ tenant. It does NOT create new guest accounts and does NOT use the tenant-level
207
+ /invitations endpoint.
208
+
209
+ Microsoft Graph API is treating OneDrive and SharePoint resources as driveItem.
210
+
211
+ API Reference:
212
+ - DriveItem Resource Type: https://learn.microsoft.com/en-us/graph/api/resources/driveitem
213
+ - API Documentation: https://learn.microsoft.com/en-us/graph/api/driveitem-invite
214
+ """
215
+ if not file_id or not file_id.strip():
216
+ raise ToolError("Argument validation error: 'file_id' cannot be empty.")
217
+
218
+ if not document_library_id or not document_library_id.strip():
219
+ raise ToolError("Argument validation error: 'document_library_id' cannot be empty.")
220
+
221
+ if not recipient_emails:
222
+ raise ToolError("Argument validation error: you must provide at least one 'recipient'.")
223
+
224
+ access_token = await get_microsoft_graph_access_token()
225
+ if isinstance(access_token, ToolError):
226
+ raise access_token
227
+
228
+ async with MicrosoftGraphClient(access_token=access_token) as client:
229
+ await client.share_item(
230
+ file_id=file_id,
231
+ document_library_id=document_library_id,
232
+ recipient_emails=recipient_emails,
233
+ role=role,
234
+ send_invitation=send_invitation,
235
+ )
236
+
237
+ n = len(recipient_emails)
238
+ return ToolResult(
239
+ content=(
240
+ f"Successfully shared file {file_id} "
241
+ f"from document library {document_library_id} "
242
+ f"with {n} recipients with '{role}' role."
243
+ ),
244
+ structured_content={
245
+ "fileId": file_id,
246
+ "documentLibraryId": document_library_id,
247
+ "recipientEmails": recipient_emails,
248
+ "n": n,
249
+ "role": role,
250
+ },
251
+ )
252
+
253
+
254
+ @dr_mcp_tool(
255
+ tags={
256
+ "microsoft",
257
+ "graph api",
258
+ "sharepoint",
259
+ "onedrive",
260
+ "document library",
261
+ "create",
262
+ "file",
263
+ "write",
264
+ }
265
+ )
266
+ async def microsoft_create_file(
267
+ *,
268
+ file_name: Annotated[str, "The name of the file to create (e.g., 'report.txt')."],
269
+ content_text: Annotated[str, "The raw text content to be stored in the file."],
270
+ document_library_id: Annotated[
271
+ str | None,
272
+ "The ID of the document library (Drive). If not provided, saves to personal OneDrive.",
273
+ ] = None,
274
+ parent_folder_id: Annotated[
275
+ str | None,
276
+ "ID of the parent folder. Defaults to 'root' (root of the drive).",
277
+ ] = "root",
278
+ ) -> ToolResult | ToolError:
279
+ """
280
+ Create a new text file in SharePoint or OneDrive.
281
+
282
+ **Personal OneDrive:** Just provide file_name and content_text.
283
+ The file saves to your personal OneDrive root folder.
284
+
285
+ **SharePoint:** Provide document_library_id to save to a specific
286
+ SharePoint site. Get the ID from microsoft_graph_search_content
287
+ results ('documentLibraryId' field).
288
+
289
+ **Conflict Resolution:** If a file with the same name exists,
290
+ it will be automatically renamed (e.g., 'report (1).txt').
291
+ """
292
+ if not file_name or not file_name.strip():
293
+ raise ToolError("Error: file_name is required.")
294
+ if not content_text:
295
+ raise ToolError("Error: content_text is required.")
296
+
297
+ access_token = await get_microsoft_graph_access_token()
298
+ if isinstance(access_token, ToolError):
299
+ raise access_token
300
+
301
+ folder_id = parent_folder_id if parent_folder_id else "root"
302
+
303
+ async with MicrosoftGraphClient(access_token=access_token) as client:
304
+ # Auto-fetch personal OneDrive if no library specified
305
+ if document_library_id is None:
306
+ drive_id = await client.get_personal_drive_id()
307
+ is_personal_onedrive = True
308
+ else:
309
+ drive_id = document_library_id
310
+ is_personal_onedrive = False
311
+
312
+ created_file = await client.create_file(
313
+ drive_id=drive_id,
314
+ file_name=file_name.strip(),
315
+ content=content_text,
316
+ parent_folder_id=folder_id,
317
+ conflict_behavior="rename",
318
+ )
319
+
320
+ return ToolResult(
321
+ content=f"File '{created_file.name}' created successfully.",
322
+ structured_content={
323
+ "file_name": created_file.name,
324
+ "destination": "onedrive" if is_personal_onedrive else "sharepoint",
325
+ "driveId": drive_id,
326
+ "id": created_file.id,
327
+ "webUrl": created_file.web_url,
328
+ "parentFolderId": created_file.parent_folder_id,
329
+ },
330
+ )
@@ -14,6 +14,10 @@
14
14
 
15
15
  import json
16
16
  import logging
17
+ from typing import Annotated
18
+
19
+ from fastmcp.exceptions import ToolError
20
+ from fastmcp.tools.tool import ToolResult
17
21
 
18
22
  from datarobot_genai.drmcp.core.clients import get_sdk_client
19
23
  from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
@@ -21,71 +25,73 @@ from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
21
25
  logger = logging.getLogger(__name__)
22
26
 
23
27
 
24
- @dr_mcp_tool(tags={"deployment", "management", "list"})
25
- async def list_deployments() -> str:
26
- """
27
- List all DataRobot deployments for the authenticated user.
28
-
29
- Returns
30
- -------
31
- A string summary of the user's DataRobot deployments.
32
- """
28
+ @dr_mcp_tool(tags={"predictive", "deployment", "read", "management", "list"})
29
+ async def list_deployments() -> ToolResult:
30
+ """List all DataRobot deployments for the authenticated user."""
33
31
  client = get_sdk_client()
34
32
  deployments = client.Deployment.list()
35
33
  if not deployments:
36
- logger.info("No deployments found")
37
- return "No deployments found."
38
- result = "\n".join(f"{d.id}: {d.label}" for d in deployments)
39
- logger.info(f"Found {len(deployments)} deployments")
40
- return result
41
-
34
+ return ToolResult(
35
+ content="No deployments found.",
36
+ structured_content={"deployments": []},
37
+ )
38
+ deployments_dict = {d.id: d.label for d in deployments}
39
+ return ToolResult(
40
+ content="\n".join(f"{d.id}: {d.label}" for d in deployments),
41
+ structured_content={"deployments": deployments_dict},
42
+ )
42
43
 
43
- @dr_mcp_tool(tags={"deployment", "model", "info"})
44
- async def get_model_info_from_deployment(deployment_id: str) -> str:
45
- """
46
- Get model info associated with a given deployment ID.
47
44
 
48
- Args:
49
- deployment_id: The ID of the DataRobot deployment.
45
+ @dr_mcp_tool(tags={"predictive", "deployment", "read", "model", "info"})
46
+ async def get_model_info_from_deployment(
47
+ *,
48
+ deployment_id: Annotated[str, "The ID of the DataRobot deployment"] | None = None,
49
+ ) -> ToolError | ToolResult:
50
+ """Retrieve model info associated with a given deployment ID."""
51
+ if not deployment_id:
52
+ raise ToolError("Deployment ID must be provided")
50
53
 
51
- Returns
52
- -------
53
- The model info associated with the deployment as a JSON string.
54
- """
55
54
  client = get_sdk_client()
56
55
  deployment = client.Deployment.get(deployment_id)
57
- logger.info(f"Retrieved model info for deployment {deployment_id}")
58
- return json.dumps(deployment.model, indent=2)
59
-
56
+ return ToolResult(
57
+ content=(
58
+ f"Retrieved model info for deployment {deployment_id}, here are the details:\n"
59
+ f"{json.dumps(deployment.model, indent=2)}"
60
+ ),
61
+ structured_content=deployment.model,
62
+ )
60
63
 
61
- @dr_mcp_tool(tags={"deployment", "model", "create"})
62
- async def deploy_model(model_id: str, label: str, description: str = "") -> str:
63
- """
64
- Deploy a model by creating a new DataRobot deployment.
65
64
 
66
- Args:
67
- model_id: The ID of the DataRobot model to deploy.
68
- label: The label/name for the deployment.
69
- description: Optional description for the deployment.
65
+ @dr_mcp_tool(tags={"predictive", "deployment", "write", "model", "create"})
66
+ async def deploy_model(
67
+ *,
68
+ model_id: Annotated[str, "The ID of the DataRobot model to deploy"] | None = None,
69
+ label: Annotated[str, "The label/name for the deployment"] | None = None,
70
+ description: Annotated[str, "Optional description for the deployment"] | None = None,
71
+ ) -> ToolError | ToolResult:
72
+ """Deploy a model by creating a new DataRobot deployment."""
73
+ if not model_id:
74
+ raise ToolError("Model ID must be provided")
75
+ if not label:
76
+ raise ToolError("Model label must be provided")
70
77
 
71
- Returns
72
- -------
73
- JSON string with deployment ID and label, or error message.
74
- """
75
78
  client = get_sdk_client()
76
79
  try:
77
80
  prediction_servers = client.PredictionServer.list()
78
81
  if not prediction_servers:
79
- logger.error("No prediction servers available")
80
- return json.dumps({"error": "No prediction servers available"})
82
+ raise ToolError("No prediction servers available for deployment.")
81
83
  deployment = client.Deployment.create_from_learning_model(
82
84
  model_id=model_id,
83
85
  label=label,
84
86
  description=description,
85
87
  default_prediction_server_id=prediction_servers[0].id,
86
88
  )
87
- logger.info(f"Created deployment {deployment.id} with label {label}")
88
- return json.dumps({"deployment_id": deployment.id, "label": label})
89
+ return ToolResult(
90
+ content=f"Created deployment {deployment.id} with label {label}",
91
+ structured_content={
92
+ "deployment_id": deployment.id,
93
+ "label": label,
94
+ },
95
+ )
89
96
  except Exception as e:
90
- logger.error(f"Error deploying model {model_id}: {type(e).__name__}: {e}")
91
- return json.dumps({"error": f"Error deploying model {model_id}: {type(e).__name__}: {e}"})
97
+ raise ToolError(f"Error deploying model {model_id}: {type(e).__name__}: {e}")
@@ -19,9 +19,13 @@ import json
19
19
  import logging
20
20
  from datetime import datetime
21
21
  from datetime import timedelta
22
+ from typing import Annotated
22
23
  from typing import Any
23
24
 
24
25
  import pandas as pd
26
+ from fastmcp.exceptions import ToolError
27
+ from fastmcp.tools.tool import ToolResult
28
+ from mcp.types import TextContent
25
29
 
26
30
  from datarobot_genai.drmcp.core.clients import get_sdk_client
27
31
  from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
@@ -29,40 +33,18 @@ from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
29
33
  logger = logging.getLogger(__name__)
30
34
 
31
35
 
32
- @dr_mcp_tool(tags={"deployment", "info", "metadata"})
33
- async def get_deployment_info(deployment_id: str) -> str:
36
+ @dr_mcp_tool(tags={"predictive", "deployment", "read", "info", "metadata"})
37
+ async def get_deployment_info(
38
+ *,
39
+ deployment_id: Annotated[str, "The ID of the DataRobot deployment"] | None = None,
40
+ ) -> ToolError | ToolResult:
34
41
  """
35
42
  Retrieve information about the deployment, including the list of
36
43
  features needed to make predictions on this deployment.
37
-
38
- Args:
39
- deployment_id: The ID of the DataRobot deployment
40
-
41
- Returns
42
- -------
43
- JSON string containing model and feature information including:
44
- For datarobot native models will return model information for custom models
45
- this will likely just return features and total_features values.
46
-
47
- - model_type: Type of model
48
- - target: Name of the target feature
49
- - target_type: Type of the target feature
50
- - features: List of features with their importance and type
51
- - total_features: Total number of features
52
- - time_series_config: Time series configuration if applicable
53
-
54
- for features:
55
- - feature_name: Name of the feature
56
- - ``name`` : str, feature name
57
- - ``feature_type`` : str, feature type
58
- - ``importance`` : float, numeric measure of the relationship strength between
59
- the feature and target (independent of model or other features)
60
- - ``date_format`` : str or None, the date format string for how this feature was
61
- interpreted, null if not a date feature, compatible with
62
- https://docs.python.org/2/library/time.html#time.strftime.
63
- - ``known_in_advance`` : bool, whether the feature was selected as known in advance in
64
- a time series model, false for non-time series models.
65
44
  """
45
+ if not deployment_id:
46
+ raise ToolError("Deployment ID must be provided")
47
+
66
48
  client = get_sdk_client()
67
49
  deployment = client.Deployment.get(deployment_id)
68
50
 
@@ -112,40 +94,34 @@ async def get_deployment_info(deployment_id: str) -> str:
112
94
  "series_id_columns": partition.multiseries_id_columns or [],
113
95
  }
114
96
 
115
- return json.dumps(result, indent=2)
97
+ return ToolResult(
98
+ content=json.dumps(result, indent=2),
99
+ structured_content=result,
100
+ )
116
101
 
117
102
 
118
- @dr_mcp_tool(tags={"deployment", "template", "data"})
119
- async def generate_prediction_data_template(deployment_id: str, n_rows: int = 1) -> str:
120
- """
121
- Generate a template CSV with the correct structure for making predictions.
122
-
123
- This creates a template with:
124
- - All required feature columns in the correct order
125
- - Sample values based on feature types
126
- - Comments explaining each feature
127
- - When using this tool, always consider feature importance. For features with high importance,
128
- try to infer or ask for a reasonable value, using frequent values or domain knowledge if
129
- available. For less important features, you may leave them blank.
130
- - If frequent values are available for a feature, they will be used as sample values;
131
- otherwise, blank fields will be used.
132
- Please note that using frequent values in your predictions data can influence the prediction,
133
- think of it as sending in the average value for the feature. If you don't want this effect on
134
- your predictions leave the field blank you in predictions dataset.
135
-
136
- Args:
137
- deployment_id: The ID of the DataRobot deployment
138
- n_rows: Number of template rows to generate (default 1)
139
-
140
- Returns
141
- -------
142
- CSV template string with sample data ready for predictions
143
- """
103
+ @dr_mcp_tool(tags={"predictive", "deployment", "read", "template", "data"})
104
+ async def generate_prediction_data_template(
105
+ *,
106
+ deployment_id: Annotated[str, "The ID of the DataRobot deployment"] | None = None,
107
+ n_rows: Annotated[int, "Number of template rows to generate"] = 1,
108
+ ) -> ToolError | ToolResult:
109
+ """Generate a template CSV with the correct structure for making predictions."""
110
+ if not deployment_id:
111
+ raise ToolError("Deployment ID must be provided")
112
+ if n_rows is None or n_rows <= 0:
113
+ n_rows = 1
114
+
144
115
  # Get feature information
145
- features_json = await get_deployment_features(deployment_id)
116
+ features_result = await get_deployment_features(deployment_id=deployment_id)
146
117
  # Add error handling for empty or error responses
118
+ # Extract text content from ToolResult
119
+ if features_result.content and isinstance(features_result.content[0], TextContent):
120
+ features_json = features_result.content[0].text
121
+ else:
122
+ features_json = str(features_result.content)
147
123
  if not features_json or features_json.strip().startswith("Error"):
148
- return f"Error: {features_json}"
124
+ raise ToolError(f"Error with feature information: {features_json}")
149
125
  features_info = json.loads(features_json)
150
126
 
151
127
  # Create template data
@@ -218,49 +194,55 @@ async def generate_prediction_data_template(deployment_id: str, n_rows: int = 1)
218
194
  result += f"# Total Features: {features_info['total_features']}\n"
219
195
  result += df.to_csv(index=False)
220
196
 
221
- return str(result)
197
+ # Build structured content with template data and metadata
198
+ structured_content = {
199
+ "deployment_id": deployment_id,
200
+ "model_type": features_info["model_type"],
201
+ "target": features_info["target"],
202
+ "target_type": features_info["target_type"],
203
+ "total_features": features_info["total_features"],
204
+ "template_data": df.to_dict("records"), # Convert DataFrame to list of dicts
205
+ }
206
+
207
+ if "time_series_config" in features_info:
208
+ structured_content["time_series_config"] = features_info["time_series_config"]
209
+
210
+ return ToolResult(
211
+ content=str(result),
212
+ structured_content=structured_content,
213
+ )
222
214
 
223
215
 
224
- @dr_mcp_tool(tags={"deployment", "validation", "data"})
216
+ @dr_mcp_tool(tags={"predictive", "deployment", "read", "validation", "data"})
225
217
  async def validate_prediction_data(
226
- deployment_id: str,
227
- file_path: str | None = None,
228
- csv_string: str | None = None,
229
- ) -> str:
230
- """
231
- Validate if a CSV file is suitable for making predictions with a deployment.
232
-
233
- Checks:
234
- - All required features are present
235
- - Feature types match expectations
236
- - Missing values (null, empty string, or blank fields) are allowed and will not cause errors
237
- - No critical issues that would prevent predictions
238
-
239
- Args:
240
- deployment_id: The ID of the DataRobot deployment
241
- file_path: Path to the CSV file to validate (optional if csv_string is provided)
242
- csv_string: CSV data as a string (optional, used if file_path is not provided)
243
-
244
- Returns
245
- -------
246
- Validation report including any errors, warnings, and suggestions
247
- """
218
+ *,
219
+ deployment_id: Annotated[str, "The ID of the DataRobot deployment"] | None = None,
220
+ file_path: Annotated[
221
+ str, "Path to the CSV file to validate (optional if csv_string is provided)"
222
+ ]
223
+ | None = None,
224
+ csv_string: Annotated[str, "CSV data as a string (optional, used if file_path is not provided)"]
225
+ | None = None,
226
+ ) -> ToolError | ToolResult:
227
+ """Validate if a CSV file is suitable for making predictions with a deployment."""
248
228
  # Load the data
249
229
  if csv_string is not None:
250
230
  df = pd.read_csv(io.StringIO(csv_string))
251
231
  elif file_path is not None:
252
232
  df = pd.read_csv(file_path)
253
233
  else:
254
- return json.dumps(
255
- {
256
- "status": "error",
257
- "error": "Must provide either file_path or csv_string.",
258
- },
259
- indent=2,
260
- )
234
+ raise ToolError("Must provide either file_path or csv_string.")
235
+
236
+ if not deployment_id:
237
+ raise ToolError("Deployment ID must be provided")
261
238
 
262
239
  # Get deployment features
263
- features_json = await get_deployment_features(deployment_id)
240
+ features_result = await get_deployment_features(deployment_id=deployment_id)
241
+ # Extract text content from ToolResult
242
+ if features_result.content and isinstance(features_result.content[0], TextContent):
243
+ features_json = features_result.content[0].text
244
+ else:
245
+ features_json = str(features_result.content)
264
246
  features_info = json.loads(features_json)
265
247
 
266
248
  validation_report: dict[str, Any] = {
@@ -359,22 +341,29 @@ async def validate_prediction_data(
359
341
  "model_type": features_info["model_type"],
360
342
  }
361
343
 
362
- return json.dumps(validation_report, indent=2)
363
-
364
-
365
- @dr_mcp_tool(tags={"deployment", "features", "info"})
366
- async def get_deployment_features(deployment_id: str) -> str:
367
- """
368
- Retrieve only the features list for a deployment, as JSON string.
369
- Args:
370
- deployment_id: The ID of the DataRobot deployment
371
- Returns:
372
- JSON string containing only the features list and time series config if present.
373
- """
374
- info_json = await get_deployment_info(deployment_id)
344
+ return ToolResult(
345
+ content=json.dumps(validation_report, indent=2),
346
+ structured_content=validation_report,
347
+ )
348
+
349
+
350
+ @dr_mcp_tool(tags={"predictive", "deployment", "read", "features", "info"})
351
+ async def get_deployment_features(
352
+ *,
353
+ deployment_id: Annotated[str, "The ID of the DataRobot deployment"] | None = None,
354
+ ) -> ToolError | ToolResult:
355
+ """Retrieve only the features list for a deployment, as JSON string."""
356
+ if not deployment_id:
357
+ raise ToolError("Deployment ID must be provided")
358
+
359
+ info_result = await get_deployment_info(deployment_id=deployment_id)
360
+ # Extract text content from ToolResult
361
+ if info_result.content and isinstance(info_result.content[0], TextContent):
362
+ info_json = info_result.content[0].text
363
+ else:
364
+ info_json = str(info_result.content)
375
365
  if not info_json.strip().startswith("{"):
376
- # Return a default error JSON
377
- return json.dumps({"features": [], "total_features": 0, "error": info_json}, indent=2)
366
+ raise ToolError(f"Error with deployment info: {info_json}")
378
367
  info = json.loads(info_json)
379
368
  # Only keep features, time_series_config, and total_features
380
369
  result = {
@@ -389,4 +378,8 @@ async def get_deployment_features(deployment_id: str) -> str:
389
378
  result["target"] = info["target"]
390
379
  if "target_type" in info:
391
380
  result["target_type"] = info["target_type"]
392
- return json.dumps(result, indent=2)
381
+
382
+ return ToolResult(
383
+ content=json.dumps(result, indent=2),
384
+ structured_content=result,
385
+ )