diagram-to-iac 1.0.2__py3-none-any.whl → 1.0.4__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.
- diagram_to_iac/actions/supervisor_entry.py +165 -3
- diagram_to_iac/agents/git_langgraph/agent.py +60 -28
- diagram_to_iac/agents/supervisor_langgraph/agent.py +362 -33
- diagram_to_iac/agents/supervisor_langgraph/github_listener.py +433 -0
- diagram_to_iac/core/registry.py +674 -0
- diagram_to_iac/services/commenter.py +589 -0
- diagram_to_iac/tools/llm_utils/__init__.py +3 -1
- diagram_to_iac/tools/llm_utils/grok_driver.py +71 -0
- diagram_to_iac/tools/llm_utils/router.py +220 -30
- {diagram_to_iac-1.0.2.dist-info → diagram_to_iac-1.0.4.dist-info}/METADATA +5 -4
- {diagram_to_iac-1.0.2.dist-info → diagram_to_iac-1.0.4.dist-info}/RECORD +14 -10
- {diagram_to_iac-1.0.2.dist-info → diagram_to_iac-1.0.4.dist-info}/WHEEL +0 -0
- {diagram_to_iac-1.0.2.dist-info → diagram_to_iac-1.0.4.dist-info}/entry_points.txt +0 -0
- {diagram_to_iac-1.0.2.dist-info → diagram_to_iac-1.0.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,589 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Commenter Service for DevOps-in-a-Box R2D Action
|
4
|
+
================================================
|
5
|
+
|
6
|
+
This service provides centralized comment and label management for GitHub issues
|
7
|
+
created by the R2D workflow. It handles template rendering, comment posting,
|
8
|
+
and automatic label management based on deployment status.
|
9
|
+
|
10
|
+
Key Features:
|
11
|
+
- Template-based comment generation with variable substitution
|
12
|
+
- Automatic GitHub label management
|
13
|
+
- Status tracking with label updates
|
14
|
+
- Integration with RunRegistry for state management
|
15
|
+
- Consistent formatting and messaging
|
16
|
+
|
17
|
+
Usage:
|
18
|
+
from diagram_to_iac.services.commenter import Commenter
|
19
|
+
|
20
|
+
commenter = Commenter()
|
21
|
+
commenter.post_resume_comment(issue_id=123, sha="abc1234", run_key="run-123")
|
22
|
+
commenter.update_labels(issue_id=123, status="in_progress")
|
23
|
+
"""
|
24
|
+
|
25
|
+
import os
|
26
|
+
import logging
|
27
|
+
from datetime import datetime, timezone
|
28
|
+
from pathlib import Path
|
29
|
+
from typing import Dict, Any, Optional, List
|
30
|
+
from dataclasses import dataclass, field
|
31
|
+
|
32
|
+
from diagram_to_iac.agents.git_langgraph.agent import GitAgent, GitAgentInput, GitAgentOutput
|
33
|
+
from diagram_to_iac.core.registry import RunRegistry, RunStatus
|
34
|
+
|
35
|
+
|
36
|
+
@dataclass
|
37
|
+
class CommentTemplate:
|
38
|
+
"""Represents a comment template with metadata."""
|
39
|
+
name: str
|
40
|
+
template_path: Path
|
41
|
+
required_vars: List[str] = field(default_factory=list)
|
42
|
+
optional_vars: List[str] = field(default_factory=list)
|
43
|
+
|
44
|
+
|
45
|
+
@dataclass
|
46
|
+
class LabelRule:
|
47
|
+
"""Represents a label management rule."""
|
48
|
+
status: str
|
49
|
+
labels_to_add: List[str] = field(default_factory=list)
|
50
|
+
labels_to_remove: List[str] = field(default_factory=list)
|
51
|
+
close_issue: bool = False
|
52
|
+
|
53
|
+
|
54
|
+
class Commenter:
|
55
|
+
"""
|
56
|
+
Service for managing GitHub comments and labels for R2D deployments.
|
57
|
+
|
58
|
+
Provides template-based comment generation with automatic variable substitution
|
59
|
+
and consistent label management based on deployment status.
|
60
|
+
"""
|
61
|
+
|
62
|
+
# Standard R2D labels
|
63
|
+
LABEL_R2D_DEPLOYMENT = "r2d-deployment"
|
64
|
+
LABEL_R2D_IN_PROGRESS = "r2d-in-progress"
|
65
|
+
LABEL_R2D_SUCCEEDED = "r2d-succeeded"
|
66
|
+
LABEL_NEEDS_SECRET = "needs-secret"
|
67
|
+
LABEL_AUTOMATED = "automated"
|
68
|
+
LABEL_INFRASTRUCTURE = "infrastructure"
|
69
|
+
|
70
|
+
def __init__(self, git_agent: Optional[GitAgent] = None, registry: Optional[RunRegistry] = None):
|
71
|
+
"""
|
72
|
+
Initialize the Commenter service.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
git_agent: GitAgent instance for GitHub operations (optional)
|
76
|
+
registry: RunRegistry for state management (optional)
|
77
|
+
"""
|
78
|
+
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
79
|
+
|
80
|
+
# Initialize Git agent for GitHub operations
|
81
|
+
self.git_agent = git_agent or GitAgent()
|
82
|
+
|
83
|
+
# Initialize registry for state tracking
|
84
|
+
from diagram_to_iac.core.registry import get_default_registry
|
85
|
+
self.registry = registry or get_default_registry()
|
86
|
+
|
87
|
+
# Find templates directory
|
88
|
+
self.templates_dir = Path(__file__).parent.parent / "templates" / "issue_comments"
|
89
|
+
if not self.templates_dir.exists():
|
90
|
+
raise FileNotFoundError(f"Templates directory not found: {self.templates_dir}")
|
91
|
+
|
92
|
+
# Load available templates
|
93
|
+
self.templates = self._load_templates()
|
94
|
+
|
95
|
+
# Define label management rules
|
96
|
+
self.label_rules = self._setup_label_rules()
|
97
|
+
|
98
|
+
self.logger.info(f"Commenter initialized with {len(self.templates)} templates")
|
99
|
+
|
100
|
+
def _load_templates(self) -> Dict[str, CommentTemplate]:
|
101
|
+
"""Load all comment templates from the templates directory."""
|
102
|
+
templates = {}
|
103
|
+
|
104
|
+
template_configs = {
|
105
|
+
"resume": {
|
106
|
+
"required_vars": ["sha", "run_key", "previous_status", "timestamp"],
|
107
|
+
"optional_vars": []
|
108
|
+
},
|
109
|
+
"new_commit": {
|
110
|
+
"required_vars": ["sha", "previous_issue_id", "timestamp"],
|
111
|
+
"optional_vars": ["pr_number"]
|
112
|
+
},
|
113
|
+
"need_pat": {
|
114
|
+
"required_vars": ["sha", "timestamp"],
|
115
|
+
"optional_vars": ["workspace_name"]
|
116
|
+
},
|
117
|
+
"fix_proposed": {
|
118
|
+
"required_vars": ["error_type", "fix_description", "timestamp", "sha"],
|
119
|
+
"optional_vars": ["fix_details"]
|
120
|
+
},
|
121
|
+
"success": {
|
122
|
+
"required_vars": ["sha", "timestamp", "terraform_summary"],
|
123
|
+
"optional_vars": ["duration", "resource_count"]
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
for template_name, config in template_configs.items():
|
128
|
+
template_path = self.templates_dir / f"{template_name}.txt"
|
129
|
+
if template_path.exists():
|
130
|
+
templates[template_name] = CommentTemplate(
|
131
|
+
name=template_name,
|
132
|
+
template_path=template_path,
|
133
|
+
required_vars=config["required_vars"],
|
134
|
+
optional_vars=config["optional_vars"]
|
135
|
+
)
|
136
|
+
self.logger.debug(f"Loaded template: {template_name}")
|
137
|
+
else:
|
138
|
+
self.logger.warning(f"Template file not found: {template_path}")
|
139
|
+
|
140
|
+
return templates
|
141
|
+
|
142
|
+
def _setup_label_rules(self) -> Dict[str, LabelRule]:
|
143
|
+
"""Setup label management rules for different deployment statuses."""
|
144
|
+
return {
|
145
|
+
"created": LabelRule(
|
146
|
+
status="created",
|
147
|
+
labels_to_add=[self.LABEL_R2D_DEPLOYMENT, self.LABEL_AUTOMATED, self.LABEL_INFRASTRUCTURE],
|
148
|
+
labels_to_remove=[]
|
149
|
+
),
|
150
|
+
"in_progress": LabelRule(
|
151
|
+
status="in_progress",
|
152
|
+
labels_to_add=[self.LABEL_R2D_IN_PROGRESS],
|
153
|
+
labels_to_remove=[self.LABEL_NEEDS_SECRET]
|
154
|
+
),
|
155
|
+
"needs_secret": LabelRule(
|
156
|
+
status="needs_secret",
|
157
|
+
labels_to_add=[self.LABEL_NEEDS_SECRET],
|
158
|
+
labels_to_remove=[self.LABEL_R2D_IN_PROGRESS]
|
159
|
+
),
|
160
|
+
"succeeded": LabelRule(
|
161
|
+
status="succeeded",
|
162
|
+
labels_to_add=[self.LABEL_R2D_SUCCEEDED],
|
163
|
+
labels_to_remove=[self.LABEL_R2D_IN_PROGRESS, self.LABEL_NEEDS_SECRET],
|
164
|
+
close_issue=True
|
165
|
+
),
|
166
|
+
"error": LabelRule(
|
167
|
+
status="error",
|
168
|
+
labels_to_add=[],
|
169
|
+
labels_to_remove=[self.LABEL_R2D_IN_PROGRESS]
|
170
|
+
),
|
171
|
+
"waiting_for_pat": LabelRule(
|
172
|
+
status="waiting_for_pat",
|
173
|
+
labels_to_add=[self.LABEL_NEEDS_SECRET],
|
174
|
+
labels_to_remove=[self.LABEL_R2D_IN_PROGRESS]
|
175
|
+
),
|
176
|
+
"waiting_for_pr": LabelRule(
|
177
|
+
status="waiting_for_pr",
|
178
|
+
labels_to_add=[],
|
179
|
+
labels_to_remove=[self.LABEL_R2D_IN_PROGRESS, self.LABEL_NEEDS_SECRET]
|
180
|
+
),
|
181
|
+
"completed": LabelRule(
|
182
|
+
status="completed",
|
183
|
+
labels_to_add=[self.LABEL_R2D_SUCCEEDED],
|
184
|
+
labels_to_remove=[self.LABEL_R2D_IN_PROGRESS, self.LABEL_NEEDS_SECRET],
|
185
|
+
close_issue=True
|
186
|
+
),
|
187
|
+
"failed": LabelRule(
|
188
|
+
status="failed",
|
189
|
+
labels_to_add=[],
|
190
|
+
labels_to_remove=[self.LABEL_R2D_IN_PROGRESS]
|
191
|
+
),
|
192
|
+
"cancelled": LabelRule(
|
193
|
+
status="cancelled",
|
194
|
+
labels_to_add=[],
|
195
|
+
labels_to_remove=[self.LABEL_R2D_IN_PROGRESS, self.LABEL_NEEDS_SECRET],
|
196
|
+
close_issue=True
|
197
|
+
)
|
198
|
+
}
|
199
|
+
|
200
|
+
def _render_template(self, template_name: str, variables: Dict[str, Any]) -> str:
|
201
|
+
"""
|
202
|
+
Render a comment template with the provided variables.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
template_name: Name of the template to render
|
206
|
+
variables: Dictionary of variables to substitute
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
Rendered template content
|
210
|
+
|
211
|
+
Raises:
|
212
|
+
ValueError: If template not found or required variables missing
|
213
|
+
"""
|
214
|
+
if template_name not in self.templates:
|
215
|
+
raise ValueError(f"Template '{template_name}' not found")
|
216
|
+
|
217
|
+
template = self.templates[template_name]
|
218
|
+
|
219
|
+
# Add default values for common variables
|
220
|
+
default_vars = {
|
221
|
+
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"),
|
222
|
+
"workspace_name": "default",
|
223
|
+
"pr_number": "N/A",
|
224
|
+
"duration": "N/A",
|
225
|
+
"resource_count": "N/A",
|
226
|
+
"fix_details": "See above for details"
|
227
|
+
}
|
228
|
+
|
229
|
+
# Merge variables with defaults (user variables take precedence)
|
230
|
+
final_vars = {**default_vars, **variables}
|
231
|
+
|
232
|
+
# Check for required variables (after applying defaults)
|
233
|
+
missing_vars = [var for var in template.required_vars if var not in final_vars]
|
234
|
+
if missing_vars:
|
235
|
+
raise ValueError(f"Missing required variables for template '{template_name}': {missing_vars}")
|
236
|
+
|
237
|
+
# Load template content
|
238
|
+
try:
|
239
|
+
with open(template.template_path, 'r') as f:
|
240
|
+
content = f.read()
|
241
|
+
except Exception as e:
|
242
|
+
raise RuntimeError(f"Failed to load template '{template_name}': {e}")
|
243
|
+
|
244
|
+
# Render template
|
245
|
+
try:
|
246
|
+
rendered = content.format(**final_vars)
|
247
|
+
self.logger.debug(f"Successfully rendered template '{template_name}'")
|
248
|
+
return rendered
|
249
|
+
except KeyError as e:
|
250
|
+
raise ValueError(f"Template variable not found: {e}")
|
251
|
+
except Exception as e:
|
252
|
+
raise RuntimeError(f"Failed to render template '{template_name}': {e}")
|
253
|
+
|
254
|
+
def _post_comment(self, issue_id: int, comment_body: str, repo_url: Optional[str] = None) -> bool:
|
255
|
+
"""
|
256
|
+
Post a comment to a GitHub issue.
|
257
|
+
|
258
|
+
Args:
|
259
|
+
issue_id: GitHub issue ID
|
260
|
+
comment_body: Comment content to post
|
261
|
+
repo_url: Repository URL (optional, for context)
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
True if comment posted successfully, False otherwise
|
265
|
+
"""
|
266
|
+
try:
|
267
|
+
# Use GitAgent to post the comment
|
268
|
+
git_input = GitAgentInput(
|
269
|
+
query=f"comment on issue {issue_id}: {comment_body}",
|
270
|
+
issue_id=issue_id
|
271
|
+
)
|
272
|
+
|
273
|
+
result = self.git_agent.run(git_input)
|
274
|
+
|
275
|
+
if not result.success:
|
276
|
+
error_msg = result.artifacts.get("error_message", "Unknown error")
|
277
|
+
self.logger.error(f"Failed to post comment to issue #{issue_id}: {error_msg}")
|
278
|
+
return False
|
279
|
+
|
280
|
+
self.logger.info(f"Successfully posted comment to issue #{issue_id}")
|
281
|
+
return True
|
282
|
+
|
283
|
+
except Exception as e:
|
284
|
+
self.logger.error(f"Error posting comment to issue #{issue_id}: {e}")
|
285
|
+
return False
|
286
|
+
|
287
|
+
def update_labels(self, issue_id: int, status: str, repo_url: Optional[str] = None) -> bool:
|
288
|
+
"""
|
289
|
+
Update GitHub issue labels based on deployment status.
|
290
|
+
|
291
|
+
Args:
|
292
|
+
issue_id: GitHub issue ID
|
293
|
+
status: Deployment status (maps to RunStatus values)
|
294
|
+
repo_url: Repository URL (optional, for context)
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
True if labels updated successfully, False otherwise
|
298
|
+
"""
|
299
|
+
if status not in self.label_rules:
|
300
|
+
self.logger.warning(f"No label rule defined for status: {status}")
|
301
|
+
return False
|
302
|
+
|
303
|
+
rule = self.label_rules[status]
|
304
|
+
|
305
|
+
try:
|
306
|
+
# Add labels
|
307
|
+
for label in rule.labels_to_add:
|
308
|
+
git_input = GitAgentInput(
|
309
|
+
query=f"add label {label} to issue {issue_id}",
|
310
|
+
issue_id=issue_id
|
311
|
+
)
|
312
|
+
result = self.git_agent.run(git_input)
|
313
|
+
|
314
|
+
if not result.success:
|
315
|
+
error_msg = result.artifacts.get("error_message", "Unknown error")
|
316
|
+
self.logger.warning(f"Failed to add label '{label}' to issue #{issue_id}: {error_msg}")
|
317
|
+
else:
|
318
|
+
self.logger.debug(f"Added label '{label}' to issue #{issue_id}")
|
319
|
+
|
320
|
+
# Remove labels
|
321
|
+
for label in rule.labels_to_remove:
|
322
|
+
git_input = GitAgentInput(
|
323
|
+
query=f"remove label {label} from issue {issue_id}",
|
324
|
+
issue_id=issue_id
|
325
|
+
)
|
326
|
+
result = self.git_agent.run(git_input)
|
327
|
+
|
328
|
+
if not result.success:
|
329
|
+
error_msg = result.artifacts.get("error_message", "Unknown error")
|
330
|
+
self.logger.warning(f"Failed to remove label '{label}' from issue #{issue_id}: {error_msg}")
|
331
|
+
else:
|
332
|
+
self.logger.debug(f"Removed label '{label}' from issue #{issue_id}")
|
333
|
+
|
334
|
+
# Close issue if required
|
335
|
+
if rule.close_issue:
|
336
|
+
git_input = GitAgentInput(
|
337
|
+
query=f"close issue {issue_id}",
|
338
|
+
issue_id=issue_id
|
339
|
+
)
|
340
|
+
result = self.git_agent.run(git_input)
|
341
|
+
|
342
|
+
if not result.success:
|
343
|
+
error_msg = result.artifacts.get("error_message", "Unknown error")
|
344
|
+
self.logger.warning(f"Failed to close issue #{issue_id}: {error_msg}")
|
345
|
+
else:
|
346
|
+
self.logger.info(f"Closed issue #{issue_id}")
|
347
|
+
|
348
|
+
self.logger.info(f"Successfully updated labels for issue #{issue_id} with status '{status}'")
|
349
|
+
return True
|
350
|
+
|
351
|
+
except Exception as e:
|
352
|
+
self.logger.error(f"Error updating labels for issue #{issue_id}: {e}")
|
353
|
+
return False
|
354
|
+
|
355
|
+
# Template-specific comment methods
|
356
|
+
|
357
|
+
def post_resume_comment(self, issue_id: int, sha: str, run_key: str,
|
358
|
+
previous_status: str = "unknown", repo_url: Optional[str] = None) -> bool:
|
359
|
+
"""
|
360
|
+
Post a resume deployment comment.
|
361
|
+
|
362
|
+
Args:
|
363
|
+
issue_id: GitHub issue ID
|
364
|
+
sha: Commit SHA being resumed
|
365
|
+
run_key: Registry run key
|
366
|
+
previous_status: Previous deployment status
|
367
|
+
repo_url: Repository URL (optional)
|
368
|
+
|
369
|
+
Returns:
|
370
|
+
True if comment posted successfully, False otherwise
|
371
|
+
"""
|
372
|
+
variables = {
|
373
|
+
"sha": sha,
|
374
|
+
"run_key": run_key,
|
375
|
+
"previous_status": previous_status
|
376
|
+
}
|
377
|
+
|
378
|
+
try:
|
379
|
+
comment_body = self._render_template("resume", variables)
|
380
|
+
success = self._post_comment(issue_id, comment_body, repo_url)
|
381
|
+
|
382
|
+
if success:
|
383
|
+
# Update labels to in-progress
|
384
|
+
self.update_labels(issue_id, "in_progress", repo_url)
|
385
|
+
|
386
|
+
return success
|
387
|
+
|
388
|
+
except Exception as e:
|
389
|
+
self.logger.error(f"Error posting resume comment: {e}")
|
390
|
+
return False
|
391
|
+
|
392
|
+
def post_new_commit_comment(self, issue_id: int, sha: str, previous_issue_id: int,
|
393
|
+
pr_number: Optional[int] = None, repo_url: Optional[str] = None) -> bool:
|
394
|
+
"""
|
395
|
+
Post a new commit deployment comment.
|
396
|
+
|
397
|
+
Args:
|
398
|
+
issue_id: GitHub issue ID
|
399
|
+
sha: New commit SHA
|
400
|
+
previous_issue_id: Previous umbrella issue ID
|
401
|
+
pr_number: Pull request number (optional)
|
402
|
+
repo_url: Repository URL (optional)
|
403
|
+
|
404
|
+
Returns:
|
405
|
+
True if comment posted successfully, False otherwise
|
406
|
+
"""
|
407
|
+
variables = {
|
408
|
+
"sha": sha,
|
409
|
+
"previous_issue_id": previous_issue_id
|
410
|
+
}
|
411
|
+
|
412
|
+
if pr_number:
|
413
|
+
variables["pr_number"] = pr_number
|
414
|
+
|
415
|
+
try:
|
416
|
+
comment_body = self._render_template("new_commit", variables)
|
417
|
+
return self._post_comment(issue_id, comment_body, repo_url)
|
418
|
+
|
419
|
+
except Exception as e:
|
420
|
+
self.logger.error(f"Error posting new commit comment: {e}")
|
421
|
+
return False
|
422
|
+
|
423
|
+
def post_need_pat_comment(self, issue_id: int, sha: str, workspace_name: str = "default",
|
424
|
+
repo_url: Optional[str] = None) -> bool:
|
425
|
+
"""
|
426
|
+
Post a PAT required comment.
|
427
|
+
|
428
|
+
Args:
|
429
|
+
issue_id: GitHub issue ID
|
430
|
+
sha: Commit SHA waiting for PAT
|
431
|
+
workspace_name: Terraform workspace name
|
432
|
+
repo_url: Repository URL (optional)
|
433
|
+
|
434
|
+
Returns:
|
435
|
+
True if comment posted successfully, False otherwise
|
436
|
+
"""
|
437
|
+
variables = {
|
438
|
+
"sha": sha,
|
439
|
+
"workspace_name": workspace_name
|
440
|
+
}
|
441
|
+
|
442
|
+
try:
|
443
|
+
comment_body = self._render_template("need_pat", variables)
|
444
|
+
success = self._post_comment(issue_id, comment_body, repo_url)
|
445
|
+
|
446
|
+
if success:
|
447
|
+
# Update labels to waiting for secret
|
448
|
+
self.update_labels(issue_id, "waiting_for_pat", repo_url)
|
449
|
+
|
450
|
+
return success
|
451
|
+
|
452
|
+
except Exception as e:
|
453
|
+
self.logger.error(f"Error posting need PAT comment: {e}")
|
454
|
+
return False
|
455
|
+
|
456
|
+
def post_fix_proposed_comment(self, issue_id: int, sha: str, error_type: str,
|
457
|
+
fix_description: str, fix_details: str = "",
|
458
|
+
repo_url: Optional[str] = None) -> bool:
|
459
|
+
"""
|
460
|
+
Post a fix proposed comment.
|
461
|
+
|
462
|
+
Args:
|
463
|
+
issue_id: GitHub issue ID
|
464
|
+
sha: Commit SHA being fixed
|
465
|
+
error_type: Type of error being fixed
|
466
|
+
fix_description: Description of the fix
|
467
|
+
fix_details: Detailed fix information
|
468
|
+
repo_url: Repository URL (optional)
|
469
|
+
|
470
|
+
Returns:
|
471
|
+
True if comment posted successfully, False otherwise
|
472
|
+
"""
|
473
|
+
variables = {
|
474
|
+
"sha": sha,
|
475
|
+
"error_type": error_type,
|
476
|
+
"fix_description": fix_description,
|
477
|
+
"fix_details": fix_details or "Automatic fix applied"
|
478
|
+
}
|
479
|
+
|
480
|
+
try:
|
481
|
+
comment_body = self._render_template("fix_proposed", variables)
|
482
|
+
return self._post_comment(issue_id, comment_body, repo_url)
|
483
|
+
|
484
|
+
except Exception as e:
|
485
|
+
self.logger.error(f"Error posting fix proposed comment: {e}")
|
486
|
+
return False
|
487
|
+
|
488
|
+
def post_success_comment(self, issue_id: int, sha: str, terraform_summary: str,
|
489
|
+
duration: str = "N/A", resource_count: str = "N/A",
|
490
|
+
repo_url: Optional[str] = None) -> bool:
|
491
|
+
"""
|
492
|
+
Post a deployment success comment.
|
493
|
+
|
494
|
+
Args:
|
495
|
+
issue_id: GitHub issue ID
|
496
|
+
sha: Completed commit SHA
|
497
|
+
terraform_summary: Summary of Terraform operations
|
498
|
+
duration: Deployment duration
|
499
|
+
resource_count: Number of resources deployed
|
500
|
+
repo_url: Repository URL (optional)
|
501
|
+
|
502
|
+
Returns:
|
503
|
+
True if comment posted successfully, False otherwise
|
504
|
+
"""
|
505
|
+
variables = {
|
506
|
+
"sha": sha,
|
507
|
+
"terraform_summary": terraform_summary,
|
508
|
+
"duration": duration,
|
509
|
+
"resource_count": resource_count
|
510
|
+
}
|
511
|
+
|
512
|
+
try:
|
513
|
+
comment_body = self._render_template("success", variables)
|
514
|
+
success = self._post_comment(issue_id, comment_body, repo_url)
|
515
|
+
|
516
|
+
if success:
|
517
|
+
# Update labels to succeeded and close issue
|
518
|
+
self.update_labels(issue_id, "completed", repo_url)
|
519
|
+
|
520
|
+
return success
|
521
|
+
|
522
|
+
except Exception as e:
|
523
|
+
self.logger.error(f"Error posting success comment: {e}")
|
524
|
+
return False
|
525
|
+
|
526
|
+
# Utility methods
|
527
|
+
|
528
|
+
def get_template_variables(self, template_name: str) -> Dict[str, List[str]]:
|
529
|
+
"""
|
530
|
+
Get required and optional variables for a template.
|
531
|
+
|
532
|
+
Args:
|
533
|
+
template_name: Name of the template
|
534
|
+
|
535
|
+
Returns:
|
536
|
+
Dictionary with 'required' and 'optional' variable lists
|
537
|
+
"""
|
538
|
+
if template_name not in self.templates:
|
539
|
+
raise ValueError(f"Template '{template_name}' not found")
|
540
|
+
|
541
|
+
template = self.templates[template_name]
|
542
|
+
return {
|
543
|
+
"required": template.required_vars,
|
544
|
+
"optional": template.optional_vars
|
545
|
+
}
|
546
|
+
|
547
|
+
def list_available_templates(self) -> List[str]:
|
548
|
+
"""
|
549
|
+
Get list of available comment templates.
|
550
|
+
|
551
|
+
Returns:
|
552
|
+
List of template names
|
553
|
+
"""
|
554
|
+
return list(self.templates.keys())
|
555
|
+
|
556
|
+
def get_label_rules(self) -> Dict[str, Dict[str, Any]]:
|
557
|
+
"""
|
558
|
+
Get all label management rules.
|
559
|
+
|
560
|
+
Returns:
|
561
|
+
Dictionary of status -> rule mappings
|
562
|
+
"""
|
563
|
+
return {
|
564
|
+
status: {
|
565
|
+
"labels_to_add": rule.labels_to_add,
|
566
|
+
"labels_to_remove": rule.labels_to_remove,
|
567
|
+
"close_issue": rule.close_issue
|
568
|
+
}
|
569
|
+
for status, rule in self.label_rules.items()
|
570
|
+
}
|
571
|
+
|
572
|
+
|
573
|
+
# Convenience functions for backwards compatibility
|
574
|
+
def post_resume_comment(issue_id: int, sha: str, run_key: str, **kwargs) -> bool:
|
575
|
+
"""Convenience function to post a resume comment."""
|
576
|
+
commenter = Commenter()
|
577
|
+
return commenter.post_resume_comment(issue_id, sha, run_key, **kwargs)
|
578
|
+
|
579
|
+
|
580
|
+
def post_success_comment(issue_id: int, sha: str, terraform_summary: str, **kwargs) -> bool:
|
581
|
+
"""Convenience function to post a success comment."""
|
582
|
+
commenter = Commenter()
|
583
|
+
return commenter.post_success_comment(issue_id, sha, terraform_summary, **kwargs)
|
584
|
+
|
585
|
+
|
586
|
+
def update_issue_labels(issue_id: int, status: str, **kwargs) -> bool:
|
587
|
+
"""Convenience function to update issue labels."""
|
588
|
+
commenter = Commenter()
|
589
|
+
return commenter.update_labels(issue_id, status, **kwargs)
|
@@ -9,6 +9,7 @@ from .base_driver import BaseLLMDriver
|
|
9
9
|
from .openai_driver import OpenAIDriver
|
10
10
|
from .anthropic_driver import AnthropicDriver
|
11
11
|
from .gemini_driver import GoogleDriver
|
12
|
+
from .grok_driver import GrokDriver
|
12
13
|
|
13
14
|
__all__ = [
|
14
15
|
"LLMRouter",
|
@@ -16,5 +17,6 @@ __all__ = [
|
|
16
17
|
"BaseLLMDriver",
|
17
18
|
"OpenAIDriver",
|
18
19
|
"AnthropicDriver",
|
19
|
-
"GoogleDriver"
|
20
|
+
"GoogleDriver",
|
21
|
+
"GrokDriver"
|
20
22
|
]
|
@@ -0,0 +1,71 @@
|
|
1
|
+
"""
|
2
|
+
Grok LLM Driver
|
3
|
+
|
4
|
+
Placeholder implementation for Grok (X.AI) models.
|
5
|
+
Currently returns "not implemented" exceptions as requested.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
from typing import Dict, Any, List
|
10
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
11
|
+
from .base_driver import BaseLLMDriver
|
12
|
+
|
13
|
+
|
14
|
+
class GrokDriver(BaseLLMDriver):
|
15
|
+
"""Grok (X.AI) LLM driver - placeholder implementation."""
|
16
|
+
|
17
|
+
SUPPORTED_MODELS = [
|
18
|
+
"grok-1",
|
19
|
+
"grok-1.5",
|
20
|
+
"grok-1.5-vision"
|
21
|
+
]
|
22
|
+
|
23
|
+
MODEL_CAPABILITIES = {
|
24
|
+
"grok-1": {"context_length": 131072, "function_calling": False, "vision": False},
|
25
|
+
"grok-1.5": {"context_length": 131072, "function_calling": False, "vision": False},
|
26
|
+
"grok-1.5-vision": {"context_length": 131072, "function_calling": False, "vision": True},
|
27
|
+
}
|
28
|
+
|
29
|
+
def validate_config(self, config: Dict[str, Any]) -> bool:
|
30
|
+
"""Validate Grok-specific configuration."""
|
31
|
+
if not os.getenv("GROK_API_KEY"):
|
32
|
+
raise ValueError("GROK_API_KEY environment variable not set")
|
33
|
+
|
34
|
+
model = config.get("model")
|
35
|
+
if model and model not in self.SUPPORTED_MODELS:
|
36
|
+
raise ValueError(f"Unsupported Grok model: {model}. Supported models: {self.SUPPORTED_MODELS}")
|
37
|
+
|
38
|
+
# Validate temperature range (assuming similar to OpenAI)
|
39
|
+
temperature = config.get("temperature")
|
40
|
+
if temperature is not None and (temperature < 0 or temperature > 2):
|
41
|
+
raise ValueError("Grok temperature must be between 0 and 2")
|
42
|
+
|
43
|
+
return True
|
44
|
+
|
45
|
+
def create_llm(self, config: Dict[str, Any]) -> BaseChatModel:
|
46
|
+
"""Create Grok LLM instance - not implemented yet."""
|
47
|
+
raise NotImplementedError(
|
48
|
+
"Grok driver is not yet implemented. "
|
49
|
+
"This is a placeholder for future X.AI/Grok integration. "
|
50
|
+
"Please use OpenAI, Anthropic, or Google providers instead."
|
51
|
+
)
|
52
|
+
|
53
|
+
def get_supported_models(self) -> List[str]:
|
54
|
+
"""Return list of supported Grok models."""
|
55
|
+
return self.SUPPORTED_MODELS.copy()
|
56
|
+
|
57
|
+
def get_model_capabilities(self, model: str) -> Dict[str, Any]:
|
58
|
+
"""Return capabilities for specific Grok model."""
|
59
|
+
return self.MODEL_CAPABILITIES.get(model, {})
|
60
|
+
|
61
|
+
def estimate_cost(self, model: str, input_tokens: int, output_tokens: int) -> float:
|
62
|
+
"""Estimate cost for Grok model usage - placeholder."""
|
63
|
+
# Placeholder pricing - actual Grok pricing would need to be researched
|
64
|
+
base_costs = {
|
65
|
+
"grok-1": {"input": 0.000005, "output": 0.000015}, # Estimated
|
66
|
+
"grok-1.5": {"input": 0.000008, "output": 0.000024}, # Estimated
|
67
|
+
"grok-1.5-vision": {"input": 0.000012, "output": 0.000036} # Estimated
|
68
|
+
}
|
69
|
+
|
70
|
+
costs = base_costs.get(model, {"input": 0.000001, "output": 0.000003})
|
71
|
+
return (input_tokens * costs["input"]) + (output_tokens * costs["output"])
|