iflow-mcp-m507_ai-soc-agent 1.0.0__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.
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/METADATA +410 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/RECORD +85 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/top_level.txt +1 -0
- src/__init__.py +8 -0
- src/ai_controller/README.md +139 -0
- src/ai_controller/__init__.py +12 -0
- src/ai_controller/agent_executor.py +596 -0
- src/ai_controller/cli/__init__.py +2 -0
- src/ai_controller/cli/main.py +243 -0
- src/ai_controller/session_manager.py +409 -0
- src/ai_controller/web/__init__.py +2 -0
- src/ai_controller/web/server.py +1181 -0
- src/ai_controller/web/static/css/README.md +102 -0
- src/api/__init__.py +13 -0
- src/api/case_management.py +271 -0
- src/api/edr.py +187 -0
- src/api/kb.py +136 -0
- src/api/siem.py +308 -0
- src/core/__init__.py +10 -0
- src/core/config.py +242 -0
- src/core/config_storage.py +684 -0
- src/core/dto.py +50 -0
- src/core/errors.py +36 -0
- src/core/logging.py +128 -0
- src/integrations/__init__.py +8 -0
- src/integrations/case_management/__init__.py +5 -0
- src/integrations/case_management/iris/__init__.py +11 -0
- src/integrations/case_management/iris/iris_client.py +885 -0
- src/integrations/case_management/iris/iris_http.py +274 -0
- src/integrations/case_management/iris/iris_mapper.py +263 -0
- src/integrations/case_management/iris/iris_models.py +128 -0
- src/integrations/case_management/thehive/__init__.py +8 -0
- src/integrations/case_management/thehive/thehive_client.py +193 -0
- src/integrations/case_management/thehive/thehive_http.py +147 -0
- src/integrations/case_management/thehive/thehive_mapper.py +190 -0
- src/integrations/case_management/thehive/thehive_models.py +125 -0
- src/integrations/cti/__init__.py +6 -0
- src/integrations/cti/local_tip/__init__.py +10 -0
- src/integrations/cti/local_tip/local_tip_client.py +90 -0
- src/integrations/cti/local_tip/local_tip_http.py +110 -0
- src/integrations/cti/opencti/__init__.py +10 -0
- src/integrations/cti/opencti/opencti_client.py +101 -0
- src/integrations/cti/opencti/opencti_http.py +418 -0
- src/integrations/edr/__init__.py +6 -0
- src/integrations/edr/elastic_defend/__init__.py +6 -0
- src/integrations/edr/elastic_defend/elastic_defend_client.py +351 -0
- src/integrations/edr/elastic_defend/elastic_defend_http.py +162 -0
- src/integrations/eng/__init__.py +10 -0
- src/integrations/eng/clickup/__init__.py +8 -0
- src/integrations/eng/clickup/clickup_client.py +513 -0
- src/integrations/eng/clickup/clickup_http.py +156 -0
- src/integrations/eng/github/__init__.py +8 -0
- src/integrations/eng/github/github_client.py +169 -0
- src/integrations/eng/github/github_http.py +158 -0
- src/integrations/eng/trello/__init__.py +8 -0
- src/integrations/eng/trello/trello_client.py +207 -0
- src/integrations/eng/trello/trello_http.py +162 -0
- src/integrations/kb/__init__.py +12 -0
- src/integrations/kb/fs_kb_client.py +313 -0
- src/integrations/siem/__init__.py +6 -0
- src/integrations/siem/elastic/__init__.py +6 -0
- src/integrations/siem/elastic/elastic_client.py +3319 -0
- src/integrations/siem/elastic/elastic_http.py +165 -0
- src/mcp/README.md +183 -0
- src/mcp/TOOLS.md +2827 -0
- src/mcp/__init__.py +13 -0
- src/mcp/__main__.py +18 -0
- src/mcp/agent_profiles.py +408 -0
- src/mcp/flow_agent_profiles.py +424 -0
- src/mcp/mcp_server.py +4086 -0
- src/mcp/rules_engine.py +487 -0
- src/mcp/runbook_manager.py +264 -0
- src/orchestrator/__init__.py +11 -0
- src/orchestrator/incident_workflow.py +244 -0
- src/orchestrator/tools_case.py +1085 -0
- src/orchestrator/tools_cti.py +359 -0
- src/orchestrator/tools_edr.py +315 -0
- src/orchestrator/tools_eng.py +378 -0
- src/orchestrator/tools_kb.py +156 -0
- src/orchestrator/tools_siem.py +1709 -0
- src/web/__init__.py +8 -0
- src/web/config_server.py +511 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ClickUp client for creating tasks and recommendations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any, Optional, List
|
|
8
|
+
|
|
9
|
+
from ....core.config import SamiConfig
|
|
10
|
+
from ....core.errors import IntegrationError
|
|
11
|
+
from ....core.logging import get_logger
|
|
12
|
+
from .clickup_http import ClickUpHttpClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = get_logger("sami.integrations.clickup.client")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ClickUpClient:
|
|
19
|
+
"""
|
|
20
|
+
Client for interacting with ClickUp API to create tasks and recommendations.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
http_client: ClickUpHttpClient,
|
|
26
|
+
fine_tuning_list_id: str,
|
|
27
|
+
engineering_list_id: str,
|
|
28
|
+
space_id: Optional[str] = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Initialize ClickUp client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
http_client: HTTP client for ClickUp API
|
|
35
|
+
fine_tuning_list_id: ClickUp list ID for fine-tuning recommendations
|
|
36
|
+
(can be a placeholder; if so, the client will auto-discover a list
|
|
37
|
+
in the configured space)
|
|
38
|
+
engineering_list_id: ClickUp list ID for engineering/visibility recommendations
|
|
39
|
+
(can be a placeholder; if so, the client will auto-discover a list
|
|
40
|
+
in the configured space)
|
|
41
|
+
space_id: Optional ClickUp space ID that contains the engineering boards.
|
|
42
|
+
"""
|
|
43
|
+
self._http = http_client
|
|
44
|
+
self.fine_tuning_list_id = fine_tuning_list_id
|
|
45
|
+
self.engineering_list_id = engineering_list_id
|
|
46
|
+
self.space_id = space_id
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_config(cls, config: SamiConfig) -> "ClickUpClient":
|
|
50
|
+
"""
|
|
51
|
+
Factory to construct a client from ``SamiConfig``.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
config: SamiConfig instance with ClickUp configuration
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
ClickUpClient instance
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
IntegrationError: If ClickUp configuration is not set
|
|
61
|
+
"""
|
|
62
|
+
if not config.eng or not config.eng.clickup:
|
|
63
|
+
raise IntegrationError("ClickUp configuration is not set in SamiConfig")
|
|
64
|
+
|
|
65
|
+
clickup_config = config.eng.clickup
|
|
66
|
+
http_client = ClickUpHttpClient(
|
|
67
|
+
api_token=clickup_config.api_token,
|
|
68
|
+
timeout_seconds=clickup_config.timeout_seconds,
|
|
69
|
+
verify_ssl=clickup_config.verify_ssl,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return cls(
|
|
73
|
+
http_client=http_client,
|
|
74
|
+
fine_tuning_list_id=clickup_config.fine_tuning_list_id,
|
|
75
|
+
engineering_list_id=clickup_config.engineering_list_id,
|
|
76
|
+
space_id=clickup_config.space_id,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
# Internal helpers for resolving list IDs
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _is_placeholder_list_id(list_id: Optional[str]) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Return True if the configured list ID looks like a placeholder.
|
|
87
|
+
"""
|
|
88
|
+
if not list_id:
|
|
89
|
+
return True
|
|
90
|
+
placeholders = {
|
|
91
|
+
"123456789",
|
|
92
|
+
"987654321",
|
|
93
|
+
"REPLACE_WITH_FINE_TUNING_LIST_ID",
|
|
94
|
+
"REPLACE_WITH_VISIBILITY_LIST_ID",
|
|
95
|
+
}
|
|
96
|
+
return list_id in placeholders
|
|
97
|
+
|
|
98
|
+
def _get_space_lists(self) -> List[Dict[str, Any]]:
|
|
99
|
+
"""
|
|
100
|
+
Get all lists in the configured space.
|
|
101
|
+
|
|
102
|
+
This is a lightweight, production-safe version of the helper logic
|
|
103
|
+
we used in tests to enumerate ClickUp lists.
|
|
104
|
+
"""
|
|
105
|
+
if not self.space_id:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
response = self._http.get(f"/v2/space/{self.space_id}/list")
|
|
110
|
+
except IntegrationError as e:
|
|
111
|
+
logger.warning(f"Failed to get lists for ClickUp space {self.space_id}: {e}")
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
lists = response.get("lists")
|
|
115
|
+
if isinstance(lists, list):
|
|
116
|
+
return lists
|
|
117
|
+
if isinstance(response, list):
|
|
118
|
+
return response
|
|
119
|
+
if isinstance(response, dict):
|
|
120
|
+
return [response]
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
def _auto_discover_list_id(self, purpose: str) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Auto-discover a list ID for the given purpose (\"fine_tuning\" or \"engineering\").
|
|
126
|
+
|
|
127
|
+
Strategy:
|
|
128
|
+
- Prefer lists in the configured space (space_id).
|
|
129
|
+
- Try to match list names by keywords (fine-tuning vs visibility/engineering).
|
|
130
|
+
- Fall back to the first list in the space if no better match is found.
|
|
131
|
+
"""
|
|
132
|
+
lists = self._get_space_lists()
|
|
133
|
+
if not lists:
|
|
134
|
+
raise IntegrationError(
|
|
135
|
+
"Unable to auto-discover ClickUp list ID: no lists found in the "
|
|
136
|
+
"configured space. Please configure valid fine_tuning_list_id and "
|
|
137
|
+
"engineering_list_id in config.eng.clickup."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
purpose = purpose.lower()
|
|
141
|
+
keywords: List[str] = []
|
|
142
|
+
if purpose == "fine_tuning":
|
|
143
|
+
# Match things like 'Fine-tuning Tasks'
|
|
144
|
+
keywords = ["fine", "tun"]
|
|
145
|
+
elif purpose == "engineering":
|
|
146
|
+
# Match things like 'Visibility Tasks', 'Engineering Tasks', etc.
|
|
147
|
+
keywords = ["visib", "engineer", "eng"]
|
|
148
|
+
|
|
149
|
+
def matches_purpose(name: str) -> bool:
|
|
150
|
+
name_l = name.lower()
|
|
151
|
+
if not keywords:
|
|
152
|
+
return False
|
|
153
|
+
# For fine-tuning, require both 'fine' and 'tun' to reduce false matches.
|
|
154
|
+
if purpose == "fine_tuning":
|
|
155
|
+
return "fine" in name_l and "tun" in name_l
|
|
156
|
+
# For engineering/visibility, any of the keywords is good enough.
|
|
157
|
+
return any(k in name_l for k in keywords)
|
|
158
|
+
|
|
159
|
+
# First, try to find best matches by name.
|
|
160
|
+
matched_lists: List[Dict[str, Any]] = []
|
|
161
|
+
for lst in lists:
|
|
162
|
+
name = lst.get("name")
|
|
163
|
+
if isinstance(name, str) and matches_purpose(name):
|
|
164
|
+
matched_lists.append(lst)
|
|
165
|
+
|
|
166
|
+
chosen: Optional[Dict[str, Any]] = None
|
|
167
|
+
if matched_lists:
|
|
168
|
+
chosen = matched_lists[0]
|
|
169
|
+
else:
|
|
170
|
+
# Fall back to the first list in the space.
|
|
171
|
+
chosen = lists[0]
|
|
172
|
+
|
|
173
|
+
list_id = chosen.get("id")
|
|
174
|
+
if not list_id:
|
|
175
|
+
raise IntegrationError(
|
|
176
|
+
"Unable to auto-discover ClickUp list ID: discovered list has no 'id'."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
logger.info(
|
|
180
|
+
"Auto-discovered ClickUp list for %s: %s (id=%s)",
|
|
181
|
+
purpose,
|
|
182
|
+
chosen.get("name", "<unnamed>"),
|
|
183
|
+
list_id,
|
|
184
|
+
)
|
|
185
|
+
return str(list_id)
|
|
186
|
+
|
|
187
|
+
def _resolve_list_id(self, configured_id: str, purpose: str) -> str:
|
|
188
|
+
"""
|
|
189
|
+
Resolve the effective list ID for the given purpose.
|
|
190
|
+
|
|
191
|
+
If a non-placeholder list ID is configured, use it directly.
|
|
192
|
+
Otherwise, auto-discover a list ID in the configured space.
|
|
193
|
+
"""
|
|
194
|
+
if not self._is_placeholder_list_id(configured_id):
|
|
195
|
+
return configured_id
|
|
196
|
+
|
|
197
|
+
if not self.space_id:
|
|
198
|
+
raise IntegrationError(
|
|
199
|
+
f"ClickUp space_id is not configured, and the {purpose} "
|
|
200
|
+
"list ID looks like a placeholder. Please configure "
|
|
201
|
+
"eng.clickup.space_id and valid list IDs in config.json."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return self._auto_discover_list_id(purpose)
|
|
205
|
+
|
|
206
|
+
def create_fine_tuning_recommendation(
|
|
207
|
+
self,
|
|
208
|
+
title: str,
|
|
209
|
+
description: str,
|
|
210
|
+
status: Optional[str] = None,
|
|
211
|
+
tags: Optional[list[str]] = None,
|
|
212
|
+
) -> Dict[str, Any]:
|
|
213
|
+
"""
|
|
214
|
+
Create a fine-tuning recommendation task in ClickUp.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
title: Task name
|
|
218
|
+
description: Task description
|
|
219
|
+
status: Optional status name (defaults to first status in list)
|
|
220
|
+
tags: Optional list of tag names to add to the task
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Dictionary with task information
|
|
224
|
+
"""
|
|
225
|
+
logger.info(f"Creating fine-tuning recommendation: {title}")
|
|
226
|
+
|
|
227
|
+
# Resolve which ClickUp list to use (supports placeholders + auto-discovery).
|
|
228
|
+
list_id = self._resolve_list_id(self.fine_tuning_list_id, purpose="fine_tuning")
|
|
229
|
+
|
|
230
|
+
# Get list info to find status
|
|
231
|
+
list_info = self._http.get(f"/v2/list/{list_id}")
|
|
232
|
+
statuses = list_info.get("statuses", [])
|
|
233
|
+
|
|
234
|
+
# Find the target status (ClickUp expects status as a string, not an object)
|
|
235
|
+
target_status_value: Optional[str] = None
|
|
236
|
+
if status:
|
|
237
|
+
for status_item in statuses:
|
|
238
|
+
if status_item.get("status") == status:
|
|
239
|
+
target_status_value = status_item.get("status")
|
|
240
|
+
break
|
|
241
|
+
if not target_status_value:
|
|
242
|
+
raise IntegrationError(f"Status '{status}' not found in fine-tuning list")
|
|
243
|
+
else:
|
|
244
|
+
# Use the first status if no status specified
|
|
245
|
+
if not statuses:
|
|
246
|
+
raise IntegrationError("No statuses found in fine-tuning list")
|
|
247
|
+
target_status_value = statuses[0].get("status")
|
|
248
|
+
|
|
249
|
+
# Create the task
|
|
250
|
+
task_data = {
|
|
251
|
+
"name": title,
|
|
252
|
+
"description": description,
|
|
253
|
+
"status": target_status_value,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Add tags if provided
|
|
257
|
+
if tags:
|
|
258
|
+
task_data["tags"] = tags
|
|
259
|
+
|
|
260
|
+
response = self._http.post(f"/v2/list/{list_id}/task", json_data=task_data)
|
|
261
|
+
task = response.get("task", response)
|
|
262
|
+
|
|
263
|
+
logger.info(f"Created fine-tuning recommendation task: {task.get('id')}")
|
|
264
|
+
return task
|
|
265
|
+
|
|
266
|
+
def create_visibility_recommendation(
|
|
267
|
+
self,
|
|
268
|
+
title: str,
|
|
269
|
+
description: str,
|
|
270
|
+
status: Optional[str] = None,
|
|
271
|
+
tags: Optional[list[str]] = None,
|
|
272
|
+
) -> Dict[str, Any]:
|
|
273
|
+
"""
|
|
274
|
+
Create a visibility/engineering recommendation task in ClickUp.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
title: Task name
|
|
278
|
+
description: Task description
|
|
279
|
+
status: Optional status name (defaults to first status in list)
|
|
280
|
+
tags: Optional list of tag names to add to the task
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Dictionary with task information
|
|
284
|
+
"""
|
|
285
|
+
logger.info(f"Creating visibility recommendation: {title}")
|
|
286
|
+
|
|
287
|
+
# Resolve which ClickUp list to use (supports placeholders + auto-discovery).
|
|
288
|
+
list_id = self._resolve_list_id(self.engineering_list_id, purpose="engineering")
|
|
289
|
+
|
|
290
|
+
# Get list info to find status
|
|
291
|
+
list_info = self._http.get(f"/v2/list/{list_id}")
|
|
292
|
+
statuses = list_info.get("statuses", [])
|
|
293
|
+
|
|
294
|
+
# Find the target status (ClickUp expects status as a string, not an object)
|
|
295
|
+
target_status_value: Optional[str] = None
|
|
296
|
+
if status:
|
|
297
|
+
for status_item in statuses:
|
|
298
|
+
if status_item.get("status") == status:
|
|
299
|
+
target_status_value = status_item.get("status")
|
|
300
|
+
break
|
|
301
|
+
if not target_status_value:
|
|
302
|
+
raise IntegrationError(f"Status '{status}' not found in engineering list")
|
|
303
|
+
else:
|
|
304
|
+
# Use the first status if no status specified
|
|
305
|
+
if not statuses:
|
|
306
|
+
raise IntegrationError("No statuses found in engineering list")
|
|
307
|
+
target_status_value = statuses[0].get("status")
|
|
308
|
+
|
|
309
|
+
# Create the task
|
|
310
|
+
task_data = {
|
|
311
|
+
"name": title,
|
|
312
|
+
"description": description,
|
|
313
|
+
"status": target_status_value,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
# Add tags if provided
|
|
317
|
+
if tags:
|
|
318
|
+
task_data["tags"] = tags
|
|
319
|
+
|
|
320
|
+
response = self._http.post(f"/v2/list/{list_id}/task", json_data=task_data)
|
|
321
|
+
task = response.get("task", response)
|
|
322
|
+
|
|
323
|
+
logger.info(f"Created visibility recommendation task: {task.get('id')}")
|
|
324
|
+
return task
|
|
325
|
+
|
|
326
|
+
def list_fine_tuning_recommendations(
|
|
327
|
+
self,
|
|
328
|
+
archived: bool = False,
|
|
329
|
+
include_closed: bool = True,
|
|
330
|
+
order_by: Optional[str] = None,
|
|
331
|
+
reverse: bool = False,
|
|
332
|
+
subtasks: bool = False,
|
|
333
|
+
statuses: Optional[list[str]] = None,
|
|
334
|
+
include_markdown_description: bool = False,
|
|
335
|
+
) -> List[Dict[str, Any]]:
|
|
336
|
+
"""
|
|
337
|
+
List all fine-tuning recommendation tasks from the fine-tuning list.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
archived: Include archived tasks (default: False)
|
|
341
|
+
include_closed: Include closed tasks (default: True)
|
|
342
|
+
order_by: Order tasks by field (e.g., "created", "updated", "priority")
|
|
343
|
+
reverse: Reverse the order (default: False)
|
|
344
|
+
subtasks: Include subtasks (default: False)
|
|
345
|
+
statuses: Filter by status names (optional)
|
|
346
|
+
include_markdown_description: Include markdown in descriptions (default: False)
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
List of task dictionaries
|
|
350
|
+
"""
|
|
351
|
+
logger.info("Listing fine-tuning recommendations")
|
|
352
|
+
|
|
353
|
+
# Resolve which ClickUp list to use
|
|
354
|
+
list_id = self._resolve_list_id(self.fine_tuning_list_id, purpose="fine_tuning")
|
|
355
|
+
|
|
356
|
+
# Build query parameters
|
|
357
|
+
params: Dict[str, Any] = {
|
|
358
|
+
"archived": str(archived).lower(),
|
|
359
|
+
"include_closed": str(include_closed).lower(),
|
|
360
|
+
"subtasks": str(subtasks).lower(),
|
|
361
|
+
"include_markdown_description": str(include_markdown_description).lower(),
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if order_by:
|
|
365
|
+
params["order_by"] = order_by
|
|
366
|
+
if reverse:
|
|
367
|
+
params["reverse"] = "true"
|
|
368
|
+
if statuses:
|
|
369
|
+
params["statuses[]"] = statuses
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
response = self._http.get(f"/v2/list/{list_id}/task", params=params)
|
|
373
|
+
tasks = response.get("tasks", [])
|
|
374
|
+
|
|
375
|
+
logger.info(f"Found {len(tasks)} fine-tuning recommendation tasks")
|
|
376
|
+
return tasks
|
|
377
|
+
except IntegrationError as e:
|
|
378
|
+
logger.error(f"Failed to list fine-tuning recommendations: {e}")
|
|
379
|
+
raise
|
|
380
|
+
|
|
381
|
+
def list_visibility_recommendations(
|
|
382
|
+
self,
|
|
383
|
+
archived: bool = False,
|
|
384
|
+
include_closed: bool = True,
|
|
385
|
+
order_by: Optional[str] = None,
|
|
386
|
+
reverse: bool = False,
|
|
387
|
+
subtasks: bool = False,
|
|
388
|
+
statuses: Optional[list[str]] = None,
|
|
389
|
+
include_markdown_description: bool = False,
|
|
390
|
+
) -> List[Dict[str, Any]]:
|
|
391
|
+
"""
|
|
392
|
+
List all visibility/engineering recommendation tasks from the engineering list.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
archived: Include archived tasks (default: False)
|
|
396
|
+
include_closed: Include closed tasks (default: True)
|
|
397
|
+
order_by: Order tasks by field (e.g., "created", "updated", "priority")
|
|
398
|
+
reverse: Reverse the order (default: False)
|
|
399
|
+
subtasks: Include subtasks (default: False)
|
|
400
|
+
statuses: Filter by status names (optional)
|
|
401
|
+
include_markdown_description: Include markdown in descriptions (default: False)
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
List of task dictionaries
|
|
405
|
+
"""
|
|
406
|
+
logger.info("Listing visibility recommendations")
|
|
407
|
+
|
|
408
|
+
# Resolve which ClickUp list to use
|
|
409
|
+
list_id = self._resolve_list_id(self.engineering_list_id, purpose="engineering")
|
|
410
|
+
|
|
411
|
+
# Build query parameters
|
|
412
|
+
params: Dict[str, Any] = {
|
|
413
|
+
"archived": str(archived).lower(),
|
|
414
|
+
"include_closed": str(include_closed).lower(),
|
|
415
|
+
"subtasks": str(subtasks).lower(),
|
|
416
|
+
"include_markdown_description": str(include_markdown_description).lower(),
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if order_by:
|
|
420
|
+
params["order_by"] = order_by
|
|
421
|
+
if reverse:
|
|
422
|
+
params["reverse"] = "true"
|
|
423
|
+
if statuses:
|
|
424
|
+
params["statuses[]"] = statuses
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
response = self._http.get(f"/v2/list/{list_id}/task", params=params)
|
|
428
|
+
tasks = response.get("tasks", [])
|
|
429
|
+
|
|
430
|
+
logger.info(f"Found {len(tasks)} visibility recommendation tasks")
|
|
431
|
+
return tasks
|
|
432
|
+
except IntegrationError as e:
|
|
433
|
+
logger.error(f"Failed to list visibility recommendations: {e}")
|
|
434
|
+
raise
|
|
435
|
+
|
|
436
|
+
def add_comment_to_fine_tuning_recommendation(
|
|
437
|
+
self,
|
|
438
|
+
task_id: str,
|
|
439
|
+
comment_text: str,
|
|
440
|
+
) -> Dict[str, Any]:
|
|
441
|
+
"""
|
|
442
|
+
Add a comment to a fine-tuning recommendation task.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
task_id: ClickUp task ID
|
|
446
|
+
comment_text: Comment text/content
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Dictionary with comment information
|
|
450
|
+
"""
|
|
451
|
+
logger.info(f"Adding comment to fine-tuning recommendation task: {task_id}")
|
|
452
|
+
|
|
453
|
+
comment_data = {
|
|
454
|
+
"comment_text": comment_text,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
response = self._http.post(f"/v2/task/{task_id}/comment", json_data=comment_data)
|
|
459
|
+
comment = response.get("comment", response)
|
|
460
|
+
|
|
461
|
+
logger.info(f"Added comment to fine-tuning recommendation task: {task_id}")
|
|
462
|
+
return comment
|
|
463
|
+
except IntegrationError as e:
|
|
464
|
+
logger.error(f"Failed to add comment to fine-tuning recommendation task {task_id}: {e}")
|
|
465
|
+
raise
|
|
466
|
+
|
|
467
|
+
def add_comment_to_visibility_recommendation(
|
|
468
|
+
self,
|
|
469
|
+
task_id: str,
|
|
470
|
+
comment_text: str,
|
|
471
|
+
) -> Dict[str, Any]:
|
|
472
|
+
"""
|
|
473
|
+
Add a comment to a visibility/engineering recommendation task.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
task_id: ClickUp task ID
|
|
477
|
+
comment_text: Comment text/content
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Dictionary with comment information
|
|
481
|
+
"""
|
|
482
|
+
logger.info(f"Adding comment to visibility recommendation task: {task_id}")
|
|
483
|
+
|
|
484
|
+
comment_data = {
|
|
485
|
+
"comment_text": comment_text,
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
response = self._http.post(f"/v2/task/{task_id}/comment", json_data=comment_data)
|
|
490
|
+
comment = response.get("comment", response)
|
|
491
|
+
|
|
492
|
+
logger.info(f"Added comment to visibility recommendation task: {task_id}")
|
|
493
|
+
return comment
|
|
494
|
+
except IntegrationError as e:
|
|
495
|
+
logger.error(f"Failed to add comment to visibility recommendation task {task_id}: {e}")
|
|
496
|
+
raise
|
|
497
|
+
|
|
498
|
+
def ping(self) -> bool:
|
|
499
|
+
"""
|
|
500
|
+
Check if ClickUp API is reachable by testing access to the configured lists.
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
True if API is reachable, False otherwise
|
|
504
|
+
"""
|
|
505
|
+
try:
|
|
506
|
+
# Try to get list info for fine-tuning list (this verifies both connectivity and list access)
|
|
507
|
+
list_id = self._resolve_list_id(self.fine_tuning_list_id, purpose="fine_tuning")
|
|
508
|
+
self._http.get(f"/v2/list/{list_id}")
|
|
509
|
+
return True
|
|
510
|
+
except IntegrationError:
|
|
511
|
+
logger.exception("ClickUp ping failed")
|
|
512
|
+
return False
|
|
513
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level HTTP client for ClickUp API.
|
|
3
|
+
|
|
4
|
+
This module is responsible for:
|
|
5
|
+
- authentication (API token)
|
|
6
|
+
- building URLs
|
|
7
|
+
- making HTTP requests
|
|
8
|
+
- basic error handling
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
from ....core.errors import IntegrationError
|
|
20
|
+
from ....core.logging import get_logger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = get_logger("sami.integrations.clickup.http")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ClickUpHttpClient:
|
|
28
|
+
"""
|
|
29
|
+
Simple HTTP client for ClickUp's REST API.
|
|
30
|
+
|
|
31
|
+
ClickUp API documentation: https://clickup.com/api
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
api_token: str
|
|
35
|
+
timeout_seconds: int = 30
|
|
36
|
+
verify_ssl: bool = True
|
|
37
|
+
|
|
38
|
+
def _headers(self) -> Dict[str, str]:
|
|
39
|
+
"""Get authentication headers."""
|
|
40
|
+
return {
|
|
41
|
+
"Authorization": self.api_token,
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"Accept": "application/json",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def _build_url(self, endpoint: str) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Build a full URL from an endpoint.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
endpoint: API endpoint path (e.g., "/v2/list/{list_id}/task")
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Full URL string
|
|
55
|
+
"""
|
|
56
|
+
# Official ClickUp API base URL is https://api.clickup.com/api/v2/...
|
|
57
|
+
# We keep the version in the endpoint and include the /api prefix here.
|
|
58
|
+
base_url = "https://api.clickup.com/api"
|
|
59
|
+
endpoint = endpoint.lstrip("/")
|
|
60
|
+
return f"{base_url}/{endpoint}"
|
|
61
|
+
|
|
62
|
+
def _handle_clickup_error(self, response: requests.Response) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Raise IntegrationError if the response indicates an error.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
response: HTTP response object
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
IntegrationError: If the response indicates an error
|
|
71
|
+
"""
|
|
72
|
+
if response.status_code < 400:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
error_data = response.json()
|
|
77
|
+
message = error_data.get("err", error_data.get("message", f"HTTP {response.status_code}"))
|
|
78
|
+
except Exception:
|
|
79
|
+
message = f"HTTP {response.status_code}: {response.text[:200]}"
|
|
80
|
+
|
|
81
|
+
raise IntegrationError(f"ClickUp API error: {message}")
|
|
82
|
+
|
|
83
|
+
def request(
|
|
84
|
+
self,
|
|
85
|
+
method: str,
|
|
86
|
+
endpoint: str,
|
|
87
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
88
|
+
params: Optional[Dict[str, Any]] = None,
|
|
89
|
+
) -> Dict[str, Any]:
|
|
90
|
+
"""
|
|
91
|
+
Make an HTTP request to ClickUp API.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
method: HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
95
|
+
endpoint: API endpoint path
|
|
96
|
+
json_data: JSON payload (for POST, PUT, PATCH)
|
|
97
|
+
params: Query parameters
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Response JSON as dictionary
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
IntegrationError: If the request fails
|
|
104
|
+
"""
|
|
105
|
+
url = self._build_url(endpoint)
|
|
106
|
+
headers = self._headers()
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
logger.debug(f"ClickUp {method} {url}")
|
|
110
|
+
if params:
|
|
111
|
+
logger.debug(f" Query params: {params}")
|
|
112
|
+
if json_data:
|
|
113
|
+
logger.debug(f" JSON payload: {json.dumps(json_data)[:200]}...")
|
|
114
|
+
|
|
115
|
+
response = requests.request(
|
|
116
|
+
method=method,
|
|
117
|
+
url=url,
|
|
118
|
+
headers=headers,
|
|
119
|
+
json=json_data,
|
|
120
|
+
params=params,
|
|
121
|
+
timeout=self.timeout_seconds,
|
|
122
|
+
verify=self.verify_ssl,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
logger.debug(f"ClickUp response status: {response.status_code}")
|
|
126
|
+
if response.status_code >= 400:
|
|
127
|
+
logger.error(f"ClickUp API error - Status: {response.status_code}, URL: {url}, Response: {response.text[:500]}")
|
|
128
|
+
|
|
129
|
+
self._handle_clickup_error(response)
|
|
130
|
+
|
|
131
|
+
if response.status_code == 204: # No Content
|
|
132
|
+
return {}
|
|
133
|
+
|
|
134
|
+
return response.json()
|
|
135
|
+
|
|
136
|
+
except requests.exceptions.Timeout as e:
|
|
137
|
+
raise IntegrationError(f"ClickUp API request timeout: {e}") from e
|
|
138
|
+
except requests.exceptions.RequestException as e:
|
|
139
|
+
raise IntegrationError(f"ClickUp API request failed: {e}") from e
|
|
140
|
+
|
|
141
|
+
def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
142
|
+
"""GET request."""
|
|
143
|
+
return self.request("GET", endpoint, params=params)
|
|
144
|
+
|
|
145
|
+
def post(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
146
|
+
"""POST request."""
|
|
147
|
+
return self.request("POST", endpoint, json_data=json_data, params=params)
|
|
148
|
+
|
|
149
|
+
def put(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
150
|
+
"""PUT request."""
|
|
151
|
+
return self.request("PUT", endpoint, json_data=json_data, params=params)
|
|
152
|
+
|
|
153
|
+
def delete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
154
|
+
"""DELETE request."""
|
|
155
|
+
return self.request("DELETE", endpoint, params=params)
|
|
156
|
+
|