datarobot-genai 0.2.20__py3-none-any.whl → 0.2.21__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.
@@ -30,6 +30,9 @@ from .atlassian import get_atlassian_cloud_id
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
33
+ # Search expand fields for CQL search - content.space gives us space.key directly
34
+ SEARCH_EXPAND_FIELDS = "content.space"
35
+
33
36
 
34
37
  class ConfluenceError(Exception):
35
38
  """Exception for Confluence API errors."""
@@ -75,6 +78,32 @@ class ConfluenceComment(BaseModel):
75
78
  }
76
79
 
77
80
 
81
+ class ContentSearchResult(BaseModel):
82
+ """Pydantic model for Confluence search result item."""
83
+
84
+ id: str
85
+ title: str
86
+ type: str
87
+ space_key: str = ""
88
+ space_name: str = ""
89
+ excerpt: str = ""
90
+ last_modified: str | None = None
91
+ url: str = ""
92
+
93
+ def as_flat_dict(self) -> dict[str, Any]:
94
+ """Return a flat dictionary representation of the search result."""
95
+ return {
96
+ "id": self.id,
97
+ "title": self.title,
98
+ "type": self.type,
99
+ "spaceKey": self.space_key,
100
+ "spaceName": self.space_name,
101
+ "excerpt": self.excerpt,
102
+ "lastModified": self.last_modified,
103
+ "url": self.url,
104
+ }
105
+
106
+
78
107
  class ConfluenceClient:
79
108
  """
80
109
  Client for interacting with Confluence API using OAuth access token.
@@ -382,6 +411,76 @@ class ConfluenceClient:
382
411
 
383
412
  return self._parse_comment_response(response.json(), page_id)
384
413
 
414
+ async def search_confluence_content(
415
+ self, cql_query: str, max_results: int
416
+ ) -> list[ContentSearchResult]:
417
+ """
418
+ Search Confluence content using CQL (Confluence Query Language).
419
+
420
+ Args:
421
+ cql_query: CQL Query
422
+ max_results: Maximum number of results to return
423
+
424
+ Returns
425
+ -------
426
+ List of Confluence content search results
427
+
428
+ Raises
429
+ ------
430
+ ConfluenceError: If the API request fails (400, 403, 429)
431
+ """
432
+ cloud_id = await self._get_cloud_id()
433
+ url = f"{ATLASSIAN_API_BASE}/ex/confluence/{cloud_id}/wiki/rest/api/search"
434
+
435
+ response = await self._client.get(
436
+ url,
437
+ params={
438
+ "cql": cql_query,
439
+ "limit": max_results,
440
+ "expand": SEARCH_EXPAND_FIELDS,
441
+ },
442
+ )
443
+
444
+ if response.status_code == HTTPStatus.BAD_REQUEST:
445
+ error_msg = self._extract_error_message(response)
446
+ raise ConfluenceError(f"Invalid CQL query: {error_msg}", status_code=400)
447
+
448
+ if response.status_code == HTTPStatus.FORBIDDEN:
449
+ raise ConfluenceError(
450
+ "Permission denied: you don't have access to search this content",
451
+ status_code=403,
452
+ )
453
+
454
+ if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
455
+ raise ConfluenceError("Rate limit exceeded. Please try again later.", status_code=429)
456
+
457
+ response.raise_for_status()
458
+ raw_results = response.json().get("results", [])
459
+ results = [ContentSearchResult(**self._parse_search_item(item)) for item in raw_results]
460
+ return results
461
+
462
+ def _parse_search_item(self, item: dict) -> dict:
463
+ """Parse raw search API response item into model-compatible dict."""
464
+ content = item.get("content", item)
465
+ links = content.get("_links", {})
466
+ base_url = links.get("base", "")
467
+ webui = links.get("webui", "")
468
+ url = f"{base_url}{webui}" if base_url and webui else webui
469
+
470
+ # Get space from content.space (requires expand=content.space)
471
+ content_space = content.get("space", {})
472
+
473
+ return {
474
+ "id": str(content.get("id", "")),
475
+ "title": content.get("title", ""),
476
+ "type": content.get("type", "page"),
477
+ "space_key": content_space.get("key", ""),
478
+ "space_name": content_space.get("name", ""),
479
+ "excerpt": item.get("excerpt", ""),
480
+ "last_modified": item.get("lastModified"),
481
+ "url": url,
482
+ }
483
+
385
484
  async def __aenter__(self) -> "ConfluenceClient":
386
485
  """Async context manager entry."""
387
486
  return self
@@ -186,3 +186,69 @@ async def confluence_add_comment(
186
186
  "page_id": page_id,
187
187
  },
188
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datarobot-genai
3
- Version: 0.2.20
3
+ Version: 0.2.21
4
4
  Summary: Generic helpers for GenAI
5
5
  Project-URL: Homepage, https://github.com/datarobot-oss/datarobot-genai
6
6
  Author: DataRobot, Inc.
@@ -78,12 +78,12 @@ datarobot_genai/drmcp/test_utils/utils.py,sha256=esGKFv8aO31-Qg3owayeWp32BYe1CdY
78
78
  datarobot_genai/drmcp/tools/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
79
79
  datarobot_genai/drmcp/tools/clients/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
80
80
  datarobot_genai/drmcp/tools/clients/atlassian.py,sha256=__M_uz7FrcbKCYRzeMn24DCEYD6OmFx_LuywHCxgXsA,6472
81
- datarobot_genai/drmcp/tools/clients/confluence.py,sha256=gDzy8t5t3b1mwEr-CuZ5BwXXQ52AXke8J_Ra7i_8T1g,13692
81
+ datarobot_genai/drmcp/tools/clients/confluence.py,sha256=YS5XsKd-jK5Yg0rgwOcC76v9e8fDJgUZIW5B9kcq5B0,17101
82
82
  datarobot_genai/drmcp/tools/clients/gdrive.py,sha256=QmNTmJdSqYO5Y5Vnp3roNZiNNJeocBVjF9UcSzcjgRY,8635
83
83
  datarobot_genai/drmcp/tools/clients/jira.py,sha256=Rm91JAyrNIqxu66-9rU1YqoRXVnWbEy-Ahvy6f6HlVg,9823
84
84
  datarobot_genai/drmcp/tools/clients/s3.py,sha256=GmwzvurFdNfvxOooA8g5S4osRysHYU0S9ypg_177Glg,953
85
85
  datarobot_genai/drmcp/tools/confluence/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
86
- datarobot_genai/drmcp/tools/confluence/tools.py,sha256=jSF7yXGFqqlMcavkRIY4HbMxb7tCeunA2ST41wa2vGI,7219
86
+ datarobot_genai/drmcp/tools/confluence/tools.py,sha256=ySwABe8osAzky3BO3lRaF6UHnXQgaurkmvM0iHFfL30,9849
87
87
  datarobot_genai/drmcp/tools/gdrive/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
88
  datarobot_genai/drmcp/tools/gdrive/tools.py,sha256=wmCUSaCWqepdlOIApA8tZ-grPYSV7wZKoer6uRy26Qg,3459
89
89
  datarobot_genai/drmcp/tools/jira/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
@@ -111,9 +111,9 @@ datarobot_genai/nat/datarobot_llm_clients.py,sha256=Yu208Ed_p_4P3HdpuM7fYnKcXtim
111
111
  datarobot_genai/nat/datarobot_llm_providers.py,sha256=aDoQcTeGI-odqydPXEX9OGGNFbzAtpqzTvHHEkmJuEQ,4963
112
112
  datarobot_genai/nat/datarobot_mcp_client.py,sha256=35FzilxNp4VqwBYI0NsOc91-xZm1C-AzWqrOdDy962A,9612
113
113
  datarobot_genai/nat/helpers.py,sha256=Q7E3ADZdtFfS8E6OQPyw2wgA6laQ58N3bhLj5CBWwJs,3265
114
- datarobot_genai-0.2.20.dist-info/METADATA,sha256=iWXFNkplo7YmQoa5bCbVPhhv-KzDIHsotkbKf-bEbEk,6301
115
- datarobot_genai-0.2.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
116
- datarobot_genai-0.2.20.dist-info/entry_points.txt,sha256=jEW3WxDZ8XIK9-ISmTyt5DbmBb047rFlzQuhY09rGrM,284
117
- datarobot_genai-0.2.20.dist-info/licenses/AUTHORS,sha256=isJGUXdjq1U7XZ_B_9AH8Qf0u4eX0XyQifJZ_Sxm4sA,80
118
- datarobot_genai-0.2.20.dist-info/licenses/LICENSE,sha256=U2_VkLIktQoa60Nf6Tbt7E4RMlfhFSjWjcJJfVC-YCE,11341
119
- datarobot_genai-0.2.20.dist-info/RECORD,,
114
+ datarobot_genai-0.2.21.dist-info/METADATA,sha256=wdbafq2Z77RRyxy7nG-kYCUU5pfCbsDbBTH7QdENnRU,6301
115
+ datarobot_genai-0.2.21.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
116
+ datarobot_genai-0.2.21.dist-info/entry_points.txt,sha256=jEW3WxDZ8XIK9-ISmTyt5DbmBb047rFlzQuhY09rGrM,284
117
+ datarobot_genai-0.2.21.dist-info/licenses/AUTHORS,sha256=isJGUXdjq1U7XZ_B_9AH8Qf0u4eX0XyQifJZ_Sxm4sA,80
118
+ datarobot_genai-0.2.21.dist-info/licenses/LICENSE,sha256=U2_VkLIktQoa60Nf6Tbt7E4RMlfhFSjWjcJJfVC-YCE,11341
119
+ datarobot_genai-0.2.21.dist-info/RECORD,,