datarobot-genai 0.2.14__tar.gz → 0.2.16__tar.gz

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.
Files changed (115) hide show
  1. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/PKG-INFO +1 -1
  2. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/pyproject.toml +1 -1
  3. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/clients/confluence.py +113 -4
  4. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/clients/jira.py +119 -5
  5. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/confluence/tools.py +61 -2
  6. datarobot_genai-0.2.16/src/datarobot_genai/drmcp/tools/jira/tools.py +243 -0
  7. datarobot_genai-0.2.14/src/datarobot_genai/drmcp/tools/jira/tools.py +0 -101
  8. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/.gitignore +0 -0
  9. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/AUTHORS +0 -0
  10. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/LICENSE +0 -0
  11. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/README.md +0 -0
  12. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/__init__.py +0 -0
  13. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/__init__.py +0 -0
  14. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/agents/__init__.py +0 -0
  15. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/agents/base.py +0 -0
  16. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/chat/__init__.py +0 -0
  17. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/chat/auth.py +0 -0
  18. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/chat/client.py +0 -0
  19. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/chat/responses.py +0 -0
  20. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/cli/__init__.py +0 -0
  21. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/cli/agent_environment.py +0 -0
  22. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/cli/agent_kernel.py +0 -0
  23. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/custom_model.py +0 -0
  24. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/mcp/__init__.py +0 -0
  25. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/mcp/common.py +0 -0
  26. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/telemetry_agent.py +0 -0
  27. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/utils/__init__.py +0 -0
  28. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/utils/auth.py +0 -0
  29. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/core/utils/urls.py +0 -0
  30. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/crewai/__init__.py +0 -0
  31. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/crewai/agent.py +0 -0
  32. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/crewai/base.py +0 -0
  33. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/crewai/events.py +0 -0
  34. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/crewai/mcp.py +0 -0
  35. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/__init__.py +0 -0
  36. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/__init__.py +0 -0
  37. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/auth.py +0 -0
  38. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/clients.py +0 -0
  39. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/config.py +0 -0
  40. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/config_utils.py +0 -0
  41. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/constants.py +0 -0
  42. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/credentials.py +0 -0
  43. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dr_mcp_server.py +0 -0
  44. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dr_mcp_server_logo.py +0 -0
  45. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +0 -0
  46. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +0 -0
  47. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +0 -0
  48. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_prompts/register.py +0 -0
  49. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_prompts/utils.py +0 -0
  50. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/__init__.py +0 -0
  51. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
  52. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +0 -0
  53. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +0 -0
  54. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +0 -0
  55. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +0 -0
  56. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +0 -0
  57. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +0 -0
  58. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +0 -0
  59. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +0 -0
  60. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +0 -0
  61. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +0 -0
  62. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/register.py +0 -0
  63. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/dynamic_tools/schema.py +0 -0
  64. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/exceptions.py +0 -0
  65. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/logging.py +0 -0
  66. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/mcp_instance.py +0 -0
  67. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/mcp_server_tools.py +0 -0
  68. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/memory_management/__init__.py +0 -0
  69. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/memory_management/manager.py +0 -0
  70. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/memory_management/memory_tools.py +0 -0
  71. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/routes.py +0 -0
  72. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/routes_utils.py +0 -0
  73. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/server_life_cycle.py +0 -0
  74. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/telemetry.py +0 -0
  75. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/tool_config.py +0 -0
  76. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/tool_filter.py +0 -0
  77. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/core/utils.py +0 -0
  78. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/server.py +0 -0
  79. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/test_utils/__init__.py +0 -0
  80. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/test_utils/integration_mcp_server.py +0 -0
  81. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +0 -0
  82. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +0 -0
  83. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py +0 -0
  84. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/test_utils/tool_base_ete.py +0 -0
  85. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/test_utils/utils.py +0 -0
  86. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/__init__.py +0 -0
  87. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/clients/__init__.py +0 -0
  88. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/clients/atlassian.py +0 -0
  89. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/clients/s3.py +0 -0
  90. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/confluence/__init__.py +0 -0
  91. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/jira/__init__.py +0 -0
  92. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/predictive/__init__.py +0 -0
  93. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/predictive/data.py +0 -0
  94. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/predictive/deployment.py +0 -0
  95. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/predictive/deployment_info.py +0 -0
  96. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/predictive/model.py +0 -0
  97. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/predictive/predict.py +0 -0
  98. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/predictive/predict_realtime.py +0 -0
  99. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/predictive/project.py +0 -0
  100. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/drmcp/tools/predictive/training.py +0 -0
  101. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/langgraph/__init__.py +0 -0
  102. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/langgraph/agent.py +0 -0
  103. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/langgraph/mcp.py +0 -0
  104. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/llama_index/__init__.py +0 -0
  105. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/llama_index/agent.py +0 -0
  106. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/llama_index/base.py +0 -0
  107. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/llama_index/mcp.py +0 -0
  108. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/nat/__init__.py +0 -0
  109. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/nat/agent.py +0 -0
  110. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/nat/datarobot_auth_provider.py +0 -0
  111. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/nat/datarobot_llm_clients.py +0 -0
  112. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/nat/datarobot_llm_providers.py +0 -0
  113. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/nat/datarobot_mcp_client.py +0 -0
  114. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/nat/helpers.py +0 -0
  115. {datarobot_genai-0.2.14 → datarobot_genai-0.2.16}/src/datarobot_genai/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datarobot-genai
3
- Version: 0.2.14
3
+ Version: 0.2.16
4
4
  Summary: Generic helpers for GenAI
5
5
  Project-URL: Homepage, https://github.com/datarobot-oss/datarobot-genai
6
6
  Author: DataRobot, Inc.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datarobot-genai"
7
- version = "0.2.14"
7
+ version = "0.2.16"
8
8
  description = "Generic helpers for GenAI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10, <3.13"
@@ -31,6 +31,14 @@ from .atlassian import get_atlassian_cloud_id
31
31
  logger = logging.getLogger(__name__)
32
32
 
33
33
 
34
+ class ConfluenceError(Exception):
35
+ """Exception for Confluence API errors."""
36
+
37
+ def __init__(self, message: str, status_code: int | None = None) -> None:
38
+ super().__init__(message)
39
+ self.status_code = status_code
40
+
41
+
34
42
  class ConfluencePage(BaseModel):
35
43
  """Pydantic model for Confluence page."""
36
44
 
@@ -133,7 +141,7 @@ class ConfluenceClient:
133
141
 
134
142
  Raises
135
143
  ------
136
- ValueError: If page is not found
144
+ ConfluenceError: If page is not found
137
145
  httpx.HTTPStatusError: If the API request fails
138
146
  """
139
147
  cloud_id = await self._get_cloud_id()
@@ -142,7 +150,7 @@ class ConfluenceClient:
142
150
  response = await self._client.get(url, params={"expand": self.EXPAND_FIELDS})
143
151
 
144
152
  if response.status_code == HTTPStatus.NOT_FOUND:
145
- raise ValueError(f"Page with ID '{page_id}' not found")
153
+ raise ConfluenceError(f"Page with ID '{page_id}' not found", status_code=404)
146
154
 
147
155
  response.raise_for_status()
148
156
  return self._parse_response(response.json())
@@ -161,7 +169,7 @@ class ConfluenceClient:
161
169
 
162
170
  Raises
163
171
  ------
164
- ValueError: If the page is not found
172
+ ConfluenceError: If the page is not found
165
173
  httpx.HTTPStatusError: If the API request fails
166
174
  """
167
175
  cloud_id = await self._get_cloud_id()
@@ -181,10 +189,111 @@ class ConfluenceClient:
181
189
  results = data.get("results", [])
182
190
 
183
191
  if not results:
184
- raise ValueError(f"Page with title '{title}' not found in space '{space_key}'")
192
+ raise ConfluenceError(
193
+ f"Page with title '{title}' not found in space '{space_key}'", status_code=404
194
+ )
185
195
 
186
196
  return self._parse_response(results[0])
187
197
 
198
+ def _extract_error_message(self, response: httpx.Response) -> str:
199
+ """Extract error message from Confluence API error response."""
200
+ try:
201
+ error_data = response.json()
202
+ # Confluence API returns errors in different formats
203
+ if "message" in error_data:
204
+ return error_data["message"]
205
+ if "errorMessages" in error_data and error_data["errorMessages"]:
206
+ return "; ".join(error_data["errorMessages"])
207
+ if "errors" in error_data:
208
+ errors = error_data["errors"]
209
+ if isinstance(errors, list):
210
+ return "; ".join(str(e) for e in errors)
211
+ if isinstance(errors, dict):
212
+ return "; ".join(f"{k}: {v}" for k, v in errors.items())
213
+ except Exception:
214
+ pass
215
+ return response.text or "Unknown error"
216
+
217
+ async def create_page(
218
+ self,
219
+ space_key: str,
220
+ title: str,
221
+ body_content: str,
222
+ parent_id: int | None = None,
223
+ ) -> ConfluencePage:
224
+ """
225
+ Create a new Confluence page in a specified space.
226
+
227
+ Args:
228
+ space_key: The key of the Confluence space where the page should live
229
+ title: The title of the new page
230
+ body_content: The content in Confluence Storage Format (XML) or raw text
231
+ parent_id: Optional ID of the parent page for creating a child page
232
+
233
+ Returns
234
+ -------
235
+ ConfluencePage with the created page data
236
+
237
+ Raises
238
+ ------
239
+ ConfluenceError: If space not found, parent page not found, duplicate title,
240
+ permission denied, or invalid content
241
+ httpx.HTTPStatusError: If the API request fails with unexpected status
242
+ """
243
+ cloud_id = await self._get_cloud_id()
244
+ url = f"{ATLASSIAN_API_BASE}/ex/confluence/{cloud_id}/wiki/rest/api/content"
245
+
246
+ payload: dict[str, Any] = {
247
+ "type": "page",
248
+ "title": title,
249
+ "space": {"key": space_key},
250
+ "body": {
251
+ "storage": {
252
+ "value": body_content,
253
+ "representation": "storage",
254
+ }
255
+ },
256
+ }
257
+
258
+ if parent_id is not None:
259
+ payload["ancestors"] = [{"id": parent_id}]
260
+
261
+ response = await self._client.post(url, json=payload)
262
+
263
+ if response.status_code == HTTPStatus.NOT_FOUND:
264
+ error_msg = self._extract_error_message(response)
265
+ if parent_id is not None and "ancestor" in error_msg.lower():
266
+ raise ConfluenceError(
267
+ f"Parent page with ID '{parent_id}' not found", status_code=404
268
+ )
269
+ raise ConfluenceError(
270
+ f"Space '{space_key}' not found or resource unavailable: {error_msg}",
271
+ status_code=404,
272
+ )
273
+
274
+ if response.status_code == HTTPStatus.CONFLICT:
275
+ raise ConfluenceError(
276
+ f"A page with title '{title}' already exists in space '{space_key}'",
277
+ status_code=409,
278
+ )
279
+
280
+ if response.status_code == HTTPStatus.FORBIDDEN:
281
+ raise ConfluenceError(
282
+ f"Permission denied: you don't have access to create pages in space '{space_key}'",
283
+ status_code=403,
284
+ )
285
+
286
+ if response.status_code == HTTPStatus.BAD_REQUEST:
287
+ error_msg = self._extract_error_message(response)
288
+ raise ConfluenceError(f"Invalid request: {error_msg}", status_code=400)
289
+
290
+ if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
291
+ raise ConfluenceError("Rate limit exceeded. Please try again later.", status_code=429)
292
+
293
+ response.raise_for_status()
294
+
295
+ return self._parse_response(response.json())
296
+
188
297
  async def __aenter__(self) -> "ConfluenceClient":
189
298
  """Async context manager entry."""
190
299
  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")
@@ -107,10 +119,41 @@ class JiraClient:
107
119
  self._cloud_id = await get_atlassian_cloud_id(self._client, service_type="jira")
108
120
  return self._cloud_id
109
121
 
110
- async def _get_full_url(self, url: str) -> str:
122
+ async def _get_full_url(self, path: str) -> str:
111
123
  """Return URL for Jira API."""
112
124
  cloud_id = await self._get_cloud_id()
113
- return f"{ATLASSIAN_API_BASE}/ex/jira/{cloud_id}/rest/api/3/{url}"
125
+ return f"{ATLASSIAN_API_BASE}/ex/jira/{cloud_id}/rest/api/3/{path}"
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
114
157
 
115
158
  async def get_jira_issue(self, issue_key: str) -> Issue:
116
159
  """
@@ -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")
@@ -209,6 +250,79 @@ class JiraClient:
209
250
  jsoned = response.json()
210
251
  return jsoned["key"]
211
252
 
253
+ async def update_jira_issue(self, issue_key: str, fields: dict[str, Any]) -> list[str]:
254
+ """
255
+ Update Jira issue.
256
+
257
+ Args:
258
+ issue_key: The key of the Jira issue, e.g., 'PROJ-123'
259
+ fields: A dictionary of field names and their new values
260
+ e.g., {'description': 'New content'}
261
+
262
+ Returns
263
+ -------
264
+ List of updated fields
265
+
266
+ Raises
267
+ ------
268
+ httpx.HTTPStatusError: If the API request fails
269
+ """
270
+ url = await self._get_full_url(f"issue/{issue_key}")
271
+ payload = {"fields": fields}
272
+
273
+ response = await self._client.put(url, json=payload)
274
+
275
+ response.raise_for_status()
276
+ return list(fields.keys())
277
+
278
+ async def get_available_jira_transitions(self, issue_key: str) -> dict[str, str]:
279
+ """
280
+ Get Available Jira Transitions.
281
+
282
+ Args:
283
+ issue_key: The key of the Jira issue, e.g., 'PROJ-123'
284
+
285
+ Returns
286
+ -------
287
+ Dictionary where key is the transition name and value is the transition ID
288
+
289
+ Raises
290
+ ------
291
+ httpx.HTTPStatusError: If the API request fails
292
+ """
293
+ url = await self._get_full_url(f"issue/{issue_key}/transitions")
294
+ response = await self._client.get(url)
295
+ response.raise_for_status()
296
+ jsoned = response.json()
297
+ transitions = {
298
+ transition["name"]: transition["id"] for transition in jsoned.get("transitions", [])
299
+ }
300
+ return transitions
301
+
302
+ async def transition_jira_issue(self, issue_key: str, transition_id: str) -> None:
303
+ """
304
+ Transition Jira issue.
305
+
306
+ Args:
307
+ issue_key: The key of the Jira issue, e.g., 'PROJ-123'
308
+ transition_id: Id of target transitionm e.g. '123'.
309
+ Can be obtained from `get_available_jira_transitions`.
310
+
311
+ Returns
312
+ -------
313
+ Nothing
314
+
315
+ Raises
316
+ ------
317
+ httpx.HTTPStatusError: If the API request fails
318
+ """
319
+ url = await self._get_full_url(f"issue/{issue_key}")
320
+ payload = {"transition": {"id": transition_id}}
321
+
322
+ response = await self._client.post(url, json=payload)
323
+
324
+ response.raise_for_status()
325
+
212
326
  async def __aenter__(self) -> "JiraClient":
213
327
  """Async context manager entry."""
214
328
  return self
@@ -23,6 +23,7 @@ from fastmcp.tools.tool import ToolResult
23
23
  from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
24
24
  from datarobot_genai.drmcp.tools.clients.atlassian import get_atlassian_access_token
25
25
  from datarobot_genai.drmcp.tools.clients.confluence import ConfluenceClient
26
+ from datarobot_genai.drmcp.tools.clients.confluence import ConfluenceError
26
27
 
27
28
  logger = logging.getLogger(__name__)
28
29
 
@@ -65,8 +66,8 @@ async def confluence_get_page(
65
66
  "'space_key' is required when identifying a page by title."
66
67
  )
67
68
  page_response = await client.get_page_by_title(page_id_or_title, space_key)
68
- except ValueError as e:
69
- logger.error(f"Value error getting Confluence page: {e}")
69
+ except ConfluenceError as e:
70
+ logger.error(f"Confluence error getting page: {e}")
70
71
  raise ToolError(str(e))
71
72
  except Exception as e:
72
73
  logger.error(f"Unexpected error getting Confluence page: {e}")
@@ -79,3 +80,61 @@ async def confluence_get_page(
79
80
  content=f"Successfully retrieved page '{page_response.title}'.",
80
81
  structured_content=page_response.as_flat_dict(),
81
82
  )
83
+
84
+
85
+ @dr_mcp_tool(tags={"confluence", "write", "create", "page"})
86
+ async def confluence_create_page(
87
+ *,
88
+ space_key: Annotated[str, "The key of the Confluence space where the new page should live."],
89
+ title: Annotated[str, "The title of the new page."],
90
+ body_content: Annotated[
91
+ str,
92
+ "The content of the page, typically in Confluence Storage Format (XML) or raw text.",
93
+ ],
94
+ parent_id: Annotated[
95
+ int | None,
96
+ "The ID of the parent page, used to create a child page.",
97
+ ] = None,
98
+ ) -> ToolResult:
99
+ """Create a new documentation page in a specified Confluence space.
100
+
101
+ Use this tool to create new Confluence pages with content in storage format.
102
+ The page will be created at the root level of the space unless a parent_id
103
+ is provided, in which case it will be created as a child page.
104
+
105
+ Usage:
106
+ - Root page: space_key="PROJ", title="New Page", body_content="<p>Content</p>"
107
+ - Child page: space_key="PROJ", title="Sub Page", body_content="<p>Content</p>",
108
+ parent_id=123456
109
+ """
110
+ if not all([space_key, title, body_content]):
111
+ raise ToolError(
112
+ "Argument validation error: space_key, title, and body_content are required fields."
113
+ )
114
+
115
+ access_token = await get_atlassian_access_token()
116
+ if isinstance(access_token, ToolError):
117
+ raise access_token
118
+
119
+ try:
120
+ async with ConfluenceClient(access_token) as client:
121
+ page_response = await client.create_page(
122
+ space_key=space_key,
123
+ title=title,
124
+ body_content=body_content,
125
+ parent_id=parent_id,
126
+ )
127
+ except ConfluenceError as e:
128
+ logger.error(f"Confluence error creating page: {e}")
129
+ raise ToolError(str(e))
130
+ except Exception as e:
131
+ logger.error(f"Unexpected error creating Confluence page: {e}")
132
+ raise ToolError(
133
+ f"An unexpected error occurred while creating Confluence page "
134
+ f"'{title}' in space '{space_key}': {str(e)}"
135
+ )
136
+
137
+ return ToolResult(
138
+ content=f"New page '{title}' created successfully in space '{space_key}'.",
139
+ structured_content={"new_page_id": page_response.page_id, "title": page_response.title},
140
+ )
@@ -0,0 +1,243 @@
1
+ # Copyright 2025 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
+ import logging
16
+ from typing import Annotated
17
+ from typing import Any
18
+
19
+ from fastmcp.exceptions import ToolError
20
+ from fastmcp.tools.tool import ToolResult
21
+
22
+ from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
23
+ from datarobot_genai.drmcp.tools.clients.atlassian import get_atlassian_access_token
24
+ from datarobot_genai.drmcp.tools.clients.jira import JiraClient
25
+
26
+ logger = logging.getLogger(__name__)
27
+
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
+
63
+ @dr_mcp_tool(tags={"jira", "read", "get", "issue"})
64
+ async def jira_get_issue(
65
+ *, issue_key: Annotated[str, "The key (ID) of the Jira issue to retrieve, e.g., 'PROJ-123'."]
66
+ ) -> ToolResult:
67
+ """Retrieve all fields and details for a single Jira issue by its key."""
68
+ if not issue_key:
69
+ raise ToolError("Argument validation error: 'issue_key' cannot be empty.")
70
+
71
+ access_token = await get_atlassian_access_token()
72
+ if isinstance(access_token, ToolError):
73
+ raise access_token
74
+
75
+ try:
76
+ async with JiraClient(access_token) as client:
77
+ issue = await client.get_jira_issue(issue_key)
78
+ except Exception as e:
79
+ logger.error(f"Unexpected error while getting Jira issue: {e}")
80
+ raise ToolError(
81
+ f"An unexpected error occurred while getting Jira issue '{issue_key}': {str(e)}"
82
+ )
83
+
84
+ return ToolResult(
85
+ content=f"Successfully retrieved details for issue '{issue_key}'.",
86
+ structured_content=issue.as_flat_dict(),
87
+ )
88
+
89
+
90
+ @dr_mcp_tool(tags={"jira", "create", "add", "issue"})
91
+ async def jira_create_issue(
92
+ *,
93
+ project_key: Annotated[str, "The key of the project where the issue should be created."],
94
+ summary: Annotated[str, "A brief summary or title for the new issue."],
95
+ issue_type: Annotated[str, "The type of issue to create (e.g., 'Task', 'Bug', 'Story')."],
96
+ description: Annotated[str | None, "Detailed description of the issue."] = None,
97
+ ) -> ToolResult:
98
+ """Create a new Jira issue with mandatory project, summary, and type information."""
99
+ if not all([project_key, summary, issue_type]):
100
+ raise ToolError(
101
+ "Argument validation error: project_key, summary, and issue_type are required fields."
102
+ )
103
+
104
+ access_token = await get_atlassian_access_token()
105
+ if isinstance(access_token, ToolError):
106
+ raise access_token
107
+
108
+ async with JiraClient(access_token) as client:
109
+ # Maybe we should cache it somehow?
110
+ # It'll be probably constant through whole mcp server lifecycle...
111
+ issue_types = await client.get_jira_issue_types(project_key=project_key)
112
+
113
+ try:
114
+ issue_type_id = issue_types[issue_type]
115
+ except KeyError:
116
+ possible_issue_types = ",".join(issue_types)
117
+ raise ToolError(
118
+ f"Unexpected issue type `{issue_type}`. Possible values are {possible_issue_types}."
119
+ )
120
+
121
+ try:
122
+ async with JiraClient(access_token) as client:
123
+ issue_key = await client.create_jira_issue(
124
+ project_key=project_key,
125
+ summary=summary,
126
+ issue_type_id=issue_type_id,
127
+ description=description,
128
+ )
129
+ except Exception as e:
130
+ logger.error(f"Unexpected error while creating Jira issue: {e}")
131
+ raise ToolError(f"An unexpected error occurred while creating Jira issue: {str(e)}")
132
+
133
+ return ToolResult(
134
+ content=f"Successfully created issue '{issue_key}'.",
135
+ structured_content={"newIssueKey": issue_key, "projectKey": project_key},
136
+ )
137
+
138
+
139
+ @dr_mcp_tool(tags={"jira", "update", "edit", "issue"})
140
+ async def jira_update_issue(
141
+ *,
142
+ issue_key: Annotated[str, "The key (ID) of the Jira issue to retrieve, e.g., 'PROJ-123'."],
143
+ fields_to_update: Annotated[
144
+ dict[str, Any],
145
+ "A dictionary of field names and their new values (e.g., {'summary': 'New content'}).",
146
+ ],
147
+ ) -> ToolResult:
148
+ """
149
+ Modify descriptive fields or custom fields on an existing Jira issue using its key.
150
+ If you want to update issue status you should use `jira_transition_issue` tool instead.
151
+
152
+ Some fields needs very specific schema to allow update.
153
+ You should follow jira rest api guidance.
154
+ Good example is description field:
155
+ "description": {
156
+ "type": "text",
157
+ "version": 1,
158
+ "text": [
159
+ {
160
+ "type": "paragraph",
161
+ "content": [
162
+ {
163
+ "type": "text",
164
+ "text": "[HERE YOU PUT REAL DESCRIPTION]"
165
+ }
166
+ ]
167
+ }
168
+ ]
169
+ }
170
+ """
171
+ if not issue_key:
172
+ raise ToolError("Argument validation error: 'issue_key' cannot be empty.")
173
+ if not fields_to_update or not isinstance(fields_to_update, dict):
174
+ raise ToolError(
175
+ "Argument validation error: 'fields_to_update' must be a non-empty dictionary."
176
+ )
177
+
178
+ access_token = await get_atlassian_access_token()
179
+ if isinstance(access_token, ToolError):
180
+ raise access_token
181
+
182
+ try:
183
+ async with JiraClient(access_token) as client:
184
+ updated_fields = await client.update_jira_issue(
185
+ issue_key=issue_key, fields=fields_to_update
186
+ )
187
+ except Exception as e:
188
+ logger.error(f"Unexpected error while updating Jira issue: {e}")
189
+ raise ToolError(f"An unexpected error occurred while updating Jira issue: {str(e)}")
190
+
191
+ updated_fields_str = ",".join(updated_fields)
192
+ return ToolResult(
193
+ content=f"Successfully updated issue '{issue_key}'. Fields modified: {updated_fields_str}.",
194
+ structured_content={"updatedIssueKey": issue_key, "fields": updated_fields},
195
+ )
196
+
197
+
198
+ @dr_mcp_tool(tags={"jira", "update", "transition", "issue"})
199
+ async def jira_transition_issue(
200
+ *,
201
+ issue_key: Annotated[str, "The key (ID) of the Jira issue to transition, e.g. 'PROJ-123'."],
202
+ transition_name: Annotated[
203
+ str, "The exact name of the target status/transition (e.g., 'In Progress')."
204
+ ],
205
+ ) -> ToolResult:
206
+ """
207
+ Move a Jira issue through its defined workflow to a new status.
208
+ This leverages Jira's workflow engine directly.
209
+ """
210
+ if not all([issue_key, transition_name]):
211
+ raise ToolError("Argument validation error: issue_key and transition name/ID are required.")
212
+
213
+ access_token = await get_atlassian_access_token()
214
+ if isinstance(access_token, ToolError):
215
+ raise access_token
216
+
217
+ async with JiraClient(access_token) as client:
218
+ available_transitions = await client.get_available_jira_transitions(issue_key=issue_key)
219
+
220
+ try:
221
+ transition_id = available_transitions[transition_name]
222
+ except KeyError:
223
+ available_transitions_str = ",".join(available_transitions)
224
+ raise ToolError(
225
+ f"Unexpected transition name `{transition_name}`. "
226
+ f"Possible values are {available_transitions_str}."
227
+ )
228
+
229
+ try:
230
+ async with JiraClient(access_token) as client:
231
+ await client.transition_jira_issue(issue_key=issue_key, transition_id=transition_id)
232
+ except Exception as e:
233
+ logger.error(f"Unexpected error while transitioning Jira issue: {e}")
234
+ raise ToolError(f"An unexpected error occurred while transitioning Jira issue: {str(e)}")
235
+
236
+ return ToolResult(
237
+ content=f"Successfully transitioned issue '{issue_key}' to status '{transition_name}'.",
238
+ structured_content={
239
+ "transitionedIssueKey": issue_key,
240
+ "newStatusName": transition_name,
241
+ "newStatusId": transition_id,
242
+ },
243
+ )
@@ -1,101 +0,0 @@
1
- # Copyright 2025 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
- import logging
16
- from typing import Annotated
17
-
18
- from fastmcp.exceptions import ToolError
19
- from fastmcp.tools.tool import ToolResult
20
-
21
- from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
22
- from datarobot_genai.drmcp.tools.clients.atlassian import get_atlassian_access_token
23
- from datarobot_genai.drmcp.tools.clients.jira import JiraClient
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- @dr_mcp_tool(tags={"jira", "read", "get", "issue"})
29
- async def jira_get_issue(
30
- *, issue_key: Annotated[str, "The key (ID) of the Jira issue to retrieve, e.g., 'PROJ-123'."]
31
- ) -> ToolResult:
32
- """Retrieve all fields and details for a single Jira issue by its key."""
33
- if not issue_key:
34
- raise ToolError("Argument validation error: 'issue_key' cannot be empty.")
35
-
36
- access_token = await get_atlassian_access_token()
37
- if isinstance(access_token, ToolError):
38
- raise access_token
39
-
40
- try:
41
- async with JiraClient(access_token) as client:
42
- issue = await client.get_jira_issue(issue_key)
43
- except Exception as e:
44
- logger.error(f"Unexpected error while getting Jira issue: {e}")
45
- raise ToolError(
46
- f"An unexpected error occurred while getting Jira issue '{issue_key}': {str(e)}"
47
- )
48
-
49
- return ToolResult(
50
- content=f"Successfully retrieved details for issue '{issue_key}'.",
51
- structured_content=issue.as_flat_dict(),
52
- )
53
-
54
-
55
- @dr_mcp_tool(tags={"jira", "create", "add", "issue"})
56
- async def jira_create_issue(
57
- *,
58
- project_key: Annotated[str, "The key of the project where the issue should be created."],
59
- summary: Annotated[str, "A brief summary or title for the new issue."],
60
- issue_type: Annotated[str, "The type of issue to create (e.g., 'Task', 'Bug', 'Story')."],
61
- description: Annotated[str | None, "Detailed description of the issue."] = None,
62
- ) -> ToolResult:
63
- """Create a new Jira issue with mandatory project, summary, and type information."""
64
- if not all([project_key, summary, issue_type]):
65
- raise ToolError(
66
- "Argument validation error: project_key, summary, and issue_type are required fields."
67
- )
68
-
69
- access_token = await get_atlassian_access_token()
70
- if isinstance(access_token, ToolError):
71
- raise access_token
72
-
73
- async with JiraClient(access_token) as client:
74
- # Maybe we should cache it somehow?
75
- # It'll be probably constant through whole mcp server lifecycle...
76
- issue_types = await client.get_jira_issue_types(project_key=project_key)
77
-
78
- try:
79
- issue_type_id = issue_types[issue_type]
80
- except KeyError:
81
- possible_issue_types = ",".join(issue_types)
82
- raise ToolError(
83
- f"Unexpected issue type `{issue_type}`. Possible values are {possible_issue_types}."
84
- )
85
-
86
- try:
87
- async with JiraClient(access_token) as client:
88
- issue_key = await client.create_jira_issue(
89
- project_key=project_key,
90
- summary=summary,
91
- issue_type_id=issue_type_id,
92
- description=description,
93
- )
94
- except Exception as e:
95
- logger.error(f"Unexpected error while creating Jira issue: {e}")
96
- raise ToolError(f"An unexpected error occurred while creating Jira issue: {str(e)}")
97
-
98
- return ToolResult(
99
- content=f"Successfully created issue '{issue_key}'.",
100
- structured_content={"newIssueKey": issue_key, "projectKey": project_key},
101
- )