datarobot-genai 0.2.15__py3-none-any.whl → 0.2.17__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/drmcp/tools/clients/confluence.py +88 -0
- datarobot_genai/drmcp/tools/clients/jira.py +44 -3
- datarobot_genai/drmcp/tools/confluence/tools.py +48 -0
- datarobot_genai/drmcp/tools/jira/tools.py +34 -0
- {datarobot_genai-0.2.15.dist-info → datarobot_genai-0.2.17.dist-info}/METADATA +1 -1
- {datarobot_genai-0.2.15.dist-info → datarobot_genai-0.2.17.dist-info}/RECORD +10 -10
- {datarobot_genai-0.2.15.dist-info → datarobot_genai-0.2.17.dist-info}/WHEEL +0 -0
- {datarobot_genai-0.2.15.dist-info → datarobot_genai-0.2.17.dist-info}/entry_points.txt +0 -0
- {datarobot_genai-0.2.15.dist-info → datarobot_genai-0.2.17.dist-info}/licenses/AUTHORS +0 -0
- {datarobot_genai-0.2.15.dist-info → datarobot_genai-0.2.17.dist-info}/licenses/LICENSE +0 -0
|
@@ -59,6 +59,22 @@ class ConfluencePage(BaseModel):
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
class ConfluenceComment(BaseModel):
|
|
63
|
+
"""Pydantic model for Confluence comment."""
|
|
64
|
+
|
|
65
|
+
comment_id: str = Field(..., description="The unique comment ID")
|
|
66
|
+
page_id: str = Field(..., description="The page ID where the comment was added")
|
|
67
|
+
body: str = Field(..., description="Comment content in storage format")
|
|
68
|
+
|
|
69
|
+
def as_flat_dict(self) -> dict[str, Any]:
|
|
70
|
+
"""Return a flat dictionary representation of the comment."""
|
|
71
|
+
return {
|
|
72
|
+
"comment_id": self.comment_id,
|
|
73
|
+
"page_id": self.page_id,
|
|
74
|
+
"body": self.body,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
62
78
|
class ConfluenceClient:
|
|
63
79
|
"""
|
|
64
80
|
Client for interacting with Confluence API using OAuth access token.
|
|
@@ -294,6 +310,78 @@ class ConfluenceClient:
|
|
|
294
310
|
|
|
295
311
|
return self._parse_response(response.json())
|
|
296
312
|
|
|
313
|
+
def _parse_comment_response(self, data: dict, page_id: str) -> ConfluenceComment:
|
|
314
|
+
"""Parse API response into ConfluenceComment."""
|
|
315
|
+
body_content = ""
|
|
316
|
+
body = data.get("body", {})
|
|
317
|
+
if isinstance(body, dict):
|
|
318
|
+
storage = body.get("storage", {})
|
|
319
|
+
if isinstance(storage, dict):
|
|
320
|
+
body_content = storage.get("value", "")
|
|
321
|
+
|
|
322
|
+
return ConfluenceComment(
|
|
323
|
+
comment_id=str(data.get("id", "")),
|
|
324
|
+
page_id=page_id,
|
|
325
|
+
body=body_content,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
async def add_comment(self, page_id: str, comment_body: str) -> ConfluenceComment:
|
|
329
|
+
"""
|
|
330
|
+
Add a comment to a Confluence page.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
page_id: The numeric page ID where the comment will be added
|
|
334
|
+
comment_body: The text content of the comment
|
|
335
|
+
|
|
336
|
+
Returns
|
|
337
|
+
-------
|
|
338
|
+
ConfluenceComment with the created comment data
|
|
339
|
+
|
|
340
|
+
Raises
|
|
341
|
+
------
|
|
342
|
+
ConfluenceError: If page not found, permission denied, or invalid content
|
|
343
|
+
httpx.HTTPStatusError: If the API request fails with unexpected status
|
|
344
|
+
"""
|
|
345
|
+
cloud_id = await self._get_cloud_id()
|
|
346
|
+
url = f"{ATLASSIAN_API_BASE}/ex/confluence/{cloud_id}/wiki/rest/api/content"
|
|
347
|
+
|
|
348
|
+
payload: dict[str, Any] = {
|
|
349
|
+
"type": "comment",
|
|
350
|
+
"container": {"id": page_id, "type": "page"},
|
|
351
|
+
"body": {
|
|
352
|
+
"storage": {
|
|
353
|
+
"value": comment_body,
|
|
354
|
+
"representation": "storage",
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
response = await self._client.post(url, json=payload)
|
|
360
|
+
|
|
361
|
+
if response.status_code == HTTPStatus.NOT_FOUND:
|
|
362
|
+
error_msg = self._extract_error_message(response)
|
|
363
|
+
raise ConfluenceError(
|
|
364
|
+
f"Page with ID '{page_id}' not found: {error_msg}",
|
|
365
|
+
status_code=404,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
if response.status_code == HTTPStatus.FORBIDDEN:
|
|
369
|
+
raise ConfluenceError(
|
|
370
|
+
f"Permission denied: you don't have access to add comments to page '{page_id}'",
|
|
371
|
+
status_code=403,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if response.status_code == HTTPStatus.BAD_REQUEST:
|
|
375
|
+
error_msg = self._extract_error_message(response)
|
|
376
|
+
raise ConfluenceError(f"Invalid request: {error_msg}", status_code=400)
|
|
377
|
+
|
|
378
|
+
if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
|
|
379
|
+
raise ConfluenceError("Rate limit exceeded. Please try again later.", status_code=429)
|
|
380
|
+
|
|
381
|
+
response.raise_for_status()
|
|
382
|
+
|
|
383
|
+
return self._parse_comment_response(response.json(), page_id)
|
|
384
|
+
|
|
297
385
|
async def __aenter__(self) -> "ConfluenceClient":
|
|
298
386
|
"""Async context manager entry."""
|
|
299
387
|
return self
|
|
@@ -25,6 +25,18 @@ from .atlassian import get_atlassian_cloud_id
|
|
|
25
25
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
|
+
RESPONSE_JIRA_ISSUE_FIELDS = {
|
|
29
|
+
"id",
|
|
30
|
+
"key",
|
|
31
|
+
"summary",
|
|
32
|
+
"status",
|
|
33
|
+
"reporter",
|
|
34
|
+
"assignee",
|
|
35
|
+
"created",
|
|
36
|
+
"updated",
|
|
37
|
+
}
|
|
38
|
+
RESPONSE_JIRA_ISSUE_FIELDS_STR = ",".join(RESPONSE_JIRA_ISSUE_FIELDS)
|
|
39
|
+
|
|
28
40
|
|
|
29
41
|
class _IssuePerson(BaseModel):
|
|
30
42
|
email_address: str = Field(alias="emailAddress")
|
|
@@ -112,6 +124,37 @@ class JiraClient:
|
|
|
112
124
|
cloud_id = await self._get_cloud_id()
|
|
113
125
|
return f"{ATLASSIAN_API_BASE}/ex/jira/{cloud_id}/rest/api/3/{path}"
|
|
114
126
|
|
|
127
|
+
async def search_jira_issues(self, jql_query: str, max_results: int) -> list[Issue]:
|
|
128
|
+
"""
|
|
129
|
+
Search Jira issues using JQL (Jira Query Language).
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
jql_query: JQL Query
|
|
133
|
+
max_results: Maximum number of issues to return
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
List of Jira issues
|
|
138
|
+
|
|
139
|
+
Raises
|
|
140
|
+
------
|
|
141
|
+
httpx.HTTPStatusError: If the API request fails
|
|
142
|
+
"""
|
|
143
|
+
url = await self._get_full_url("search/jql")
|
|
144
|
+
response = await self._client.post(
|
|
145
|
+
url,
|
|
146
|
+
json={
|
|
147
|
+
"jql": jql_query,
|
|
148
|
+
"fields": list(RESPONSE_JIRA_ISSUE_FIELDS),
|
|
149
|
+
"maxResults": max_results,
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
response.raise_for_status()
|
|
154
|
+
raw_issues = response.json().get("issues", [])
|
|
155
|
+
issues = [Issue(**issue) for issue in raw_issues]
|
|
156
|
+
return issues
|
|
157
|
+
|
|
115
158
|
async def get_jira_issue(self, issue_key: str) -> Issue:
|
|
116
159
|
"""
|
|
117
160
|
Get a Jira issue by its key.
|
|
@@ -128,9 +171,7 @@ class JiraClient:
|
|
|
128
171
|
httpx.HTTPStatusError: If the API request fails
|
|
129
172
|
"""
|
|
130
173
|
url = await self._get_full_url(f"issue/{issue_key}")
|
|
131
|
-
response = await self._client.get(
|
|
132
|
-
url, params={"fields": "id,key,summary,status,reporter,assignee,created,updated"}
|
|
133
|
-
)
|
|
174
|
+
response = await self._client.get(url, params={"fields": RESPONSE_JIRA_ISSUE_FIELDS_STR})
|
|
134
175
|
|
|
135
176
|
if response.status_code == HTTPStatus.NOT_FOUND:
|
|
136
177
|
raise ValueError(f"{issue_key} not found")
|
|
@@ -138,3 +138,51 @@ async def confluence_create_page(
|
|
|
138
138
|
content=f"New page '{title}' created successfully in space '{space_key}'.",
|
|
139
139
|
structured_content={"new_page_id": page_response.page_id, "title": page_response.title},
|
|
140
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
|
+
)
|
|
@@ -26,6 +26,40 @@ from datarobot_genai.drmcp.tools.clients.jira import JiraClient
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
@dr_mcp_tool(tags={"jira", "search", "issues"})
|
|
30
|
+
async def jira_search_issues(
|
|
31
|
+
*,
|
|
32
|
+
jql_query: Annotated[
|
|
33
|
+
str, "The JQL (Jira Query Language) string used to filter and search for issues."
|
|
34
|
+
],
|
|
35
|
+
max_results: Annotated[int, "Maximum number of issues to return. Default is 50."] = 50,
|
|
36
|
+
) -> ToolResult:
|
|
37
|
+
"""
|
|
38
|
+
Search for Jira issues using a powerful JQL query string.
|
|
39
|
+
|
|
40
|
+
Refer to JQL documentation for advanced query construction:
|
|
41
|
+
JQL functions: https://support.atlassian.com/jira-service-management-cloud/docs/jql-functions/
|
|
42
|
+
JQL fields: https://support.atlassian.com/jira-service-management-cloud/docs/jql-fields/
|
|
43
|
+
JQL keywords: https://support.atlassian.com/jira-service-management-cloud/docs/use-advanced-search-with-jira-query-language-jql/
|
|
44
|
+
JQL operators: https://support.atlassian.com/jira-service-management-cloud/docs/jql-operators/
|
|
45
|
+
"""
|
|
46
|
+
if not jql_query:
|
|
47
|
+
raise ToolError("Argument validation error: 'jql_query' cannot be empty.")
|
|
48
|
+
|
|
49
|
+
access_token = await get_atlassian_access_token()
|
|
50
|
+
if isinstance(access_token, ToolError):
|
|
51
|
+
raise access_token
|
|
52
|
+
|
|
53
|
+
async with JiraClient(access_token) as client:
|
|
54
|
+
issues = await client.search_jira_issues(jql_query=jql_query, max_results=max_results)
|
|
55
|
+
|
|
56
|
+
n = len(issues)
|
|
57
|
+
return ToolResult(
|
|
58
|
+
content=f"Successfully executed JQL query and retrieved {n} issue(s).",
|
|
59
|
+
structured_content={"data": [issue.as_flat_dict() for issue in issues], "count": n},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
29
63
|
@dr_mcp_tool(tags={"jira", "read", "get", "issue"})
|
|
30
64
|
async def jira_get_issue(
|
|
31
65
|
*, issue_key: Annotated[str, "The key (ID) of the Jira issue to retrieve, e.g., 'PROJ-123'."]
|
|
@@ -76,13 +76,13 @@ datarobot_genai/drmcp/test_utils/utils.py,sha256=esGKFv8aO31-Qg3owayeWp32BYe1CdY
|
|
|
76
76
|
datarobot_genai/drmcp/tools/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
77
77
|
datarobot_genai/drmcp/tools/clients/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
78
78
|
datarobot_genai/drmcp/tools/clients/atlassian.py,sha256=__M_uz7FrcbKCYRzeMn24DCEYD6OmFx_LuywHCxgXsA,6472
|
|
79
|
-
datarobot_genai/drmcp/tools/clients/confluence.py,sha256=
|
|
80
|
-
datarobot_genai/drmcp/tools/clients/jira.py,sha256=
|
|
79
|
+
datarobot_genai/drmcp/tools/clients/confluence.py,sha256=gDzy8t5t3b1mwEr-CuZ5BwXXQ52AXke8J_Ra7i_8T1g,13692
|
|
80
|
+
datarobot_genai/drmcp/tools/clients/jira.py,sha256=Rm91JAyrNIqxu66-9rU1YqoRXVnWbEy-Ahvy6f6HlVg,9823
|
|
81
81
|
datarobot_genai/drmcp/tools/clients/s3.py,sha256=GmwzvurFdNfvxOooA8g5S4osRysHYU0S9ypg_177Glg,953
|
|
82
82
|
datarobot_genai/drmcp/tools/confluence/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
83
|
-
datarobot_genai/drmcp/tools/confluence/tools.py,sha256=
|
|
83
|
+
datarobot_genai/drmcp/tools/confluence/tools.py,sha256=jSF7yXGFqqlMcavkRIY4HbMxb7tCeunA2ST41wa2vGI,7219
|
|
84
84
|
datarobot_genai/drmcp/tools/jira/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
|
|
85
|
-
datarobot_genai/drmcp/tools/jira/tools.py,sha256=
|
|
85
|
+
datarobot_genai/drmcp/tools/jira/tools.py,sha256=dfkqTU2HH-7n44hX80ODFacKq0p0LOchFcZtIIKFNMM,9687
|
|
86
86
|
datarobot_genai/drmcp/tools/predictive/__init__.py,sha256=WuOHlNNEpEmcF7gVnhckruJRKU2qtmJLE3E7zoCGLDo,1030
|
|
87
87
|
datarobot_genai/drmcp/tools/predictive/data.py,sha256=k4EJxJrl8DYVGVfJ0DM4YTfnZlC_K3OUHZ0eRUzfluI,3165
|
|
88
88
|
datarobot_genai/drmcp/tools/predictive/deployment.py,sha256=lm02Ayuo11L1hP41fgi3QpR1Eyty-Wc16rM0c8SgliM,3277
|
|
@@ -106,9 +106,9 @@ datarobot_genai/nat/datarobot_llm_clients.py,sha256=Yu208Ed_p_4P3HdpuM7fYnKcXtim
|
|
|
106
106
|
datarobot_genai/nat/datarobot_llm_providers.py,sha256=aDoQcTeGI-odqydPXEX9OGGNFbzAtpqzTvHHEkmJuEQ,4963
|
|
107
107
|
datarobot_genai/nat/datarobot_mcp_client.py,sha256=35FzilxNp4VqwBYI0NsOc91-xZm1C-AzWqrOdDy962A,9612
|
|
108
108
|
datarobot_genai/nat/helpers.py,sha256=Q7E3ADZdtFfS8E6OQPyw2wgA6laQ58N3bhLj5CBWwJs,3265
|
|
109
|
-
datarobot_genai-0.2.
|
|
110
|
-
datarobot_genai-0.2.
|
|
111
|
-
datarobot_genai-0.2.
|
|
112
|
-
datarobot_genai-0.2.
|
|
113
|
-
datarobot_genai-0.2.
|
|
114
|
-
datarobot_genai-0.2.
|
|
109
|
+
datarobot_genai-0.2.17.dist-info/METADATA,sha256=FAAExscPOUIc1_sTNKY3DPTygcVtfrql4fubM9b6nDg,6301
|
|
110
|
+
datarobot_genai-0.2.17.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
111
|
+
datarobot_genai-0.2.17.dist-info/entry_points.txt,sha256=jEW3WxDZ8XIK9-ISmTyt5DbmBb047rFlzQuhY09rGrM,284
|
|
112
|
+
datarobot_genai-0.2.17.dist-info/licenses/AUTHORS,sha256=isJGUXdjq1U7XZ_B_9AH8Qf0u4eX0XyQifJZ_Sxm4sA,80
|
|
113
|
+
datarobot_genai-0.2.17.dist-info/licenses/LICENSE,sha256=U2_VkLIktQoa60Nf6Tbt7E4RMlfhFSjWjcJJfVC-YCE,11341
|
|
114
|
+
datarobot_genai-0.2.17.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|