diagram-to-iac 0.7.0__py3-none-any.whl → 0.9.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.
Files changed (78) hide show
  1. diagram_to_iac/__init__.py +10 -0
  2. diagram_to_iac/actions/__init__.py +7 -0
  3. diagram_to_iac/actions/git_entry.py +174 -0
  4. diagram_to_iac/actions/supervisor_entry.py +116 -0
  5. diagram_to_iac/actions/terraform_agent_entry.py +207 -0
  6. diagram_to_iac/agents/__init__.py +26 -0
  7. diagram_to_iac/agents/demonstrator_langgraph/__init__.py +10 -0
  8. diagram_to_iac/agents/demonstrator_langgraph/agent.py +826 -0
  9. diagram_to_iac/agents/git_langgraph/__init__.py +10 -0
  10. diagram_to_iac/agents/git_langgraph/agent.py +1018 -0
  11. diagram_to_iac/agents/git_langgraph/pr.py +146 -0
  12. diagram_to_iac/agents/hello_langgraph/__init__.py +9 -0
  13. diagram_to_iac/agents/hello_langgraph/agent.py +621 -0
  14. diagram_to_iac/agents/policy_agent/__init__.py +15 -0
  15. diagram_to_iac/agents/policy_agent/agent.py +507 -0
  16. diagram_to_iac/agents/policy_agent/integration_example.py +191 -0
  17. diagram_to_iac/agents/policy_agent/tools/__init__.py +14 -0
  18. diagram_to_iac/agents/policy_agent/tools/tfsec_tool.py +259 -0
  19. diagram_to_iac/agents/shell_langgraph/__init__.py +21 -0
  20. diagram_to_iac/agents/shell_langgraph/agent.py +122 -0
  21. diagram_to_iac/agents/shell_langgraph/detector.py +50 -0
  22. diagram_to_iac/agents/supervisor_langgraph/__init__.py +17 -0
  23. diagram_to_iac/agents/supervisor_langgraph/agent.py +1947 -0
  24. diagram_to_iac/agents/supervisor_langgraph/demonstrator.py +22 -0
  25. diagram_to_iac/agents/supervisor_langgraph/guards.py +23 -0
  26. diagram_to_iac/agents/supervisor_langgraph/pat_loop.py +49 -0
  27. diagram_to_iac/agents/supervisor_langgraph/router.py +9 -0
  28. diagram_to_iac/agents/terraform_langgraph/__init__.py +15 -0
  29. diagram_to_iac/agents/terraform_langgraph/agent.py +1216 -0
  30. diagram_to_iac/agents/terraform_langgraph/parser.py +76 -0
  31. diagram_to_iac/core/__init__.py +7 -0
  32. diagram_to_iac/core/agent_base.py +19 -0
  33. diagram_to_iac/core/enhanced_memory.py +302 -0
  34. diagram_to_iac/core/errors.py +4 -0
  35. diagram_to_iac/core/issue_tracker.py +49 -0
  36. diagram_to_iac/core/memory.py +132 -0
  37. diagram_to_iac/r2d.py +345 -13
  38. diagram_to_iac/services/__init__.py +10 -0
  39. diagram_to_iac/services/observability.py +59 -0
  40. diagram_to_iac/services/step_summary.py +77 -0
  41. diagram_to_iac/tools/__init__.py +11 -0
  42. diagram_to_iac/tools/api_utils.py +108 -26
  43. diagram_to_iac/tools/git/__init__.py +45 -0
  44. diagram_to_iac/tools/git/git.py +956 -0
  45. diagram_to_iac/tools/hello/__init__.py +30 -0
  46. diagram_to_iac/tools/hello/cal_utils.py +31 -0
  47. diagram_to_iac/tools/hello/text_utils.py +97 -0
  48. diagram_to_iac/tools/llm_utils/__init__.py +20 -0
  49. diagram_to_iac/tools/llm_utils/anthropic_driver.py +87 -0
  50. diagram_to_iac/tools/llm_utils/base_driver.py +90 -0
  51. diagram_to_iac/tools/llm_utils/gemini_driver.py +89 -0
  52. diagram_to_iac/tools/llm_utils/openai_driver.py +93 -0
  53. diagram_to_iac/tools/llm_utils/router.py +303 -0
  54. diagram_to_iac/tools/sec_utils.py +4 -2
  55. diagram_to_iac/tools/shell/__init__.py +17 -0
  56. diagram_to_iac/tools/shell/shell.py +415 -0
  57. diagram_to_iac/tools/text_utils.py +277 -0
  58. diagram_to_iac/tools/tf/terraform.py +851 -0
  59. diagram_to_iac-0.9.0.dist-info/METADATA +256 -0
  60. diagram_to_iac-0.9.0.dist-info/RECORD +64 -0
  61. {diagram_to_iac-0.7.0.dist-info → diagram_to_iac-0.9.0.dist-info}/WHEEL +1 -1
  62. diagram_to_iac-0.9.0.dist-info/entry_points.txt +6 -0
  63. diagram_to_iac/agents/codegen_agent.py +0 -0
  64. diagram_to_iac/agents/consensus_agent.py +0 -0
  65. diagram_to_iac/agents/deployment_agent.py +0 -0
  66. diagram_to_iac/agents/github_agent.py +0 -0
  67. diagram_to_iac/agents/interpretation_agent.py +0 -0
  68. diagram_to_iac/agents/question_agent.py +0 -0
  69. diagram_to_iac/agents/supervisor.py +0 -0
  70. diagram_to_iac/agents/vision_agent.py +0 -0
  71. diagram_to_iac/core/config.py +0 -0
  72. diagram_to_iac/tools/cv_utils.py +0 -0
  73. diagram_to_iac/tools/gh_utils.py +0 -0
  74. diagram_to_iac/tools/tf_utils.py +0 -0
  75. diagram_to_iac-0.7.0.dist-info/METADATA +0 -16
  76. diagram_to_iac-0.7.0.dist-info/RECORD +0 -32
  77. diagram_to_iac-0.7.0.dist-info/entry_points.txt +0 -2
  78. {diagram_to_iac-0.7.0.dist-info → diagram_to_iac-0.9.0.dist-info}/top_level.txt +0 -0
diagram_to_iac/r2d.py CHANGED
@@ -1,13 +1,345 @@
1
- # src/diagram_to_iac/r2d.py
2
-
3
- import sys, json
4
- import requests # external package, so pipreqs must pick it up
5
-
6
- def main():
7
- # a sanity check that our imports actually work:
8
- print("✅ diagram-to-iac CLI is up and running!")
9
- print("• requests version:", requests.__version__)
10
- # verify stdlib too
11
- data = {"ok": True}
12
- print("• json dump:", json.dumps(data))
13
- sys.exit(0)
1
+ #!/usr/bin/env python3
2
+ """
3
+ R2D (Repo-to-Deployment) CLI - DevOps-in-a-Box Entry Point
4
+
5
+ This is the main CLI for the R2D workflow, which orchestrates the complete
6
+ Repo-to-Deployment process using the SupervisorAgent.
7
+
8
+ Usage:
9
+ diagram-to-iac <repository_url>
10
+ r2d-agent <repository_url>
11
+
12
+ The CLI will:
13
+ 1. Validate the repository URL
14
+ 2. Initialize the SupervisorAgent
15
+ 3. Execute the complete R2D workflow
16
+ 4. Handle errors gracefully with GitHub Issues
17
+ 5. Generate observability artifacts
18
+
19
+ Mission: "One container, many minds—zero manual toil."
20
+ """
21
+
22
+ import sys
23
+ import argparse
24
+ import logging
25
+ from pathlib import Path
26
+ from datetime import datetime
27
+ import os
28
+
29
+ def setup_logging(verbose: bool = False) -> None:
30
+ """Configure logging for the R2D CLI."""
31
+ level = logging.DEBUG if verbose else logging.INFO
32
+ logging.basicConfig(
33
+ level=level,
34
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
35
+ datefmt='%Y-%m-%d %H:%M:%S'
36
+ )
37
+
38
+ def print_banner() -> None:
39
+ """Print the DevOps-in-a-Box banner."""
40
+ banner = """
41
+ ╔══════════════════════════════════════════════════════════════════════════════╗
42
+ ║ 🤖 DevOps-in-a-Box: R2D CLI ║
43
+ ║ "One container, many minds—zero manual toil." ║
44
+ ║ ║
45
+ ║ Mission: Self-healing, Terraform-first DevOps automation ║
46
+ ║ Workflow: Clone → Classify → Validate → Deploy → Auto-fix → Summarize ║
47
+ ╚══════════════════════════════════════════════════════════════════════════════╝
48
+ """
49
+ print(banner)
50
+
51
+ def validate_repository_url(repo_url: str) -> bool:
52
+ """Validate that the repository URL is properly formatted."""
53
+ if not repo_url:
54
+ return False
55
+
56
+ # Basic validation for common Git URL formats
57
+ valid_patterns = [
58
+ 'https://github.com/',
59
+ 'https://gitlab.com/',
60
+ 'git@github.com:',
61
+ 'git@gitlab.com:',
62
+ 'https://bitbucket.org/',
63
+ ]
64
+
65
+ return any(repo_url.startswith(pattern) for pattern in valid_patterns)
66
+
67
+ def validate_environment_variables() -> tuple[bool, list[str]]:
68
+ """
69
+ Validate required environment variables for R2D workflow.
70
+
71
+ Returns:
72
+ tuple: (is_valid, list of missing/invalid variables)
73
+ """
74
+ errors = []
75
+
76
+ # Check for required GitHub token
77
+ github_token = os.getenv('GITHUB_TOKEN')
78
+ if not github_token:
79
+ errors.append("GITHUB_TOKEN is required for repository operations and issue creation")
80
+
81
+ # Check for at least one AI API key
82
+ ai_keys = {
83
+ 'OPENAI_API_KEY': os.getenv('OPENAI_API_KEY'),
84
+ 'ANTHROPIC_API_KEY': os.getenv('ANTHROPIC_API_KEY'),
85
+ 'GOOGLE_API_KEY': os.getenv('GOOGLE_API_KEY'),
86
+ }
87
+
88
+ available_keys = [key for key, value in ai_keys.items() if value]
89
+
90
+ if not available_keys:
91
+ errors.append(
92
+ "At least one AI API key is required (OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY)\n"
93
+ " The SupervisorAgent and other agents require AI capabilities for decision-making"
94
+ )
95
+
96
+ return len(errors) == 0, errors
97
+
98
+ def print_environment_status() -> None:
99
+ """Print the status of environment variables."""
100
+ print("🔍 Environment Validation")
101
+ print("═══════════════════════")
102
+
103
+ # GitHub token
104
+ github_token = os.getenv('GITHUB_TOKEN')
105
+ if github_token:
106
+ print("✅ GITHUB_TOKEN: configured")
107
+ else:
108
+ print("❌ GITHUB_TOKEN: missing")
109
+
110
+ # AI API keys
111
+ ai_keys = {
112
+ 'OPENAI_API_KEY': os.getenv('OPENAI_API_KEY'),
113
+ 'ANTHROPIC_API_KEY': os.getenv('ANTHROPIC_API_KEY'),
114
+ 'GOOGLE_API_KEY': os.getenv('GOOGLE_API_KEY'),
115
+ }
116
+
117
+ available_ai_keys = []
118
+ for key, value in ai_keys.items():
119
+ if value:
120
+ print(f"✅ {key}: configured")
121
+ available_ai_keys.append(key)
122
+ else:
123
+ print(f"⚪ {key}: not configured")
124
+
125
+ if available_ai_keys:
126
+ print(f"✅ AI capabilities: enabled via {', '.join(available_ai_keys)}")
127
+ else:
128
+ print("❌ AI capabilities: no API keys configured")
129
+
130
+ # Optional tokens
131
+ tf_token = os.getenv('TFE_TOKEN')
132
+ if tf_token:
133
+ print("✅ TFE_TOKEN: configured (Terraform Cloud operations enabled)")
134
+ else:
135
+ print("⚪ TFE_TOKEN: not configured (Terraform Cloud operations disabled)")
136
+
137
+ print("")
138
+
139
+ def create_argument_parser() -> argparse.ArgumentParser:
140
+ """Create and configure the argument parser for R2D CLI."""
141
+ parser = argparse.ArgumentParser(
142
+ prog='diagram-to-iac',
143
+ description='DevOps-in-a-Box R2D CLI - Automated Repo-to-Deployment',
144
+ formatter_class=argparse.RawDescriptionHelpFormatter,
145
+ epilog="""
146
+ Examples:
147
+ %(prog)s https://github.com/user/repo
148
+ %(prog)s https://github.com/user/infra-repo --dry-run
149
+ %(prog)s git@github.com:user/terraform-config.git --branch-name production
150
+
151
+ Environment Variables:
152
+ GITHUB_TOKEN - GitHub personal access token (required)
153
+ TFE_TOKEN - Terraform Cloud API token (recommended)
154
+
155
+ AI API Keys (at least one required):
156
+ OPENAI_API_KEY - OpenAI API key (for GPT models)
157
+ ANTHROPIC_API_KEY - Anthropic API key (for Claude models)
158
+ GOOGLE_API_KEY - Google API key (for Gemini models)
159
+
160
+ Mission: "One container, many minds—zero manual toil."
161
+ """
162
+ )
163
+
164
+ # Positional argument for repository URL
165
+ parser.add_argument(
166
+ 'repository_url',
167
+ help='Repository URL to process (GitHub, GitLab, Bitbucket supported)'
168
+ )
169
+
170
+ # Optional arguments
171
+ parser.add_argument(
172
+ '--dry-run',
173
+ action='store_true',
174
+ help='Run in dry-run mode - simulate actions without making changes'
175
+ )
176
+ parser.add_argument(
177
+ '--branch-name',
178
+ type=str,
179
+ help='Custom branch name for operations (auto-generated if not provided)'
180
+ )
181
+ parser.add_argument(
182
+ '--thread-id',
183
+ type=str,
184
+ help='Thread ID for conversation tracking and resumption'
185
+ )
186
+ parser.add_argument(
187
+ '--verbose',
188
+ action='store_true',
189
+ help='Enable verbose logging'
190
+ )
191
+ parser.add_argument(
192
+ '--no-interactive',
193
+ action='store_true',
194
+ help='Skip interactive prompts'
195
+ )
196
+ parser.add_argument(
197
+ '--config-path',
198
+ type=str,
199
+ help='Path to custom configuration file'
200
+ )
201
+
202
+ return parser
203
+
204
+ def main() -> int:
205
+ """Main entry point for the R2D CLI."""
206
+ try:
207
+ # Parse arguments
208
+ parser = create_argument_parser()
209
+ args = parser.parse_args()
210
+
211
+ # Set up logging
212
+ setup_logging(args.verbose)
213
+ logger = logging.getLogger(__name__)
214
+
215
+ # Print banner
216
+ print_banner()
217
+
218
+ # Validate environment variables
219
+ print_environment_status()
220
+ env_valid, env_errors = validate_environment_variables()
221
+
222
+ if not env_valid:
223
+ print("❌ Environment validation failed:")
224
+ for error in env_errors:
225
+ print(f" • {error}")
226
+ print("")
227
+ print("ℹ️ Please set the required environment variables and try again")
228
+ print("💡 Tip: You can test API connectivity with: python -c 'from diagram_to_iac.tools.api_utils import test_all_apis; test_all_apis()'")
229
+ return 1
230
+
231
+ # Validate repository URL
232
+ if not validate_repository_url(args.repository_url):
233
+ print("❌ Error: Invalid repository URL format")
234
+ print("ℹ️ Supported formats: https://github.com/user/repo, git@github.com:user/repo.git")
235
+ return 1
236
+
237
+ # Validate environment variables
238
+ is_valid, validation_errors = validate_environment_variables()
239
+ if not is_valid:
240
+ print("❌ Error: Invalid environment configuration")
241
+ for error in validation_errors:
242
+ print(f"⚠️ {error}")
243
+ print("ℹ️ Please set the required environment variables and try again")
244
+ return 1
245
+
246
+ print(f"🎯 Repository: {args.repository_url}")
247
+ print(f"🧪 Dry run: {args.dry_run}")
248
+ print(f"🌿 Branch: {args.branch_name or 'auto-generated'}")
249
+ print("")
250
+
251
+ # Import SupervisorAgent (delayed import for faster CLI startup)
252
+ try:
253
+ from diagram_to_iac.agents.supervisor_langgraph import (
254
+ SupervisorAgent,
255
+ SupervisorAgentInput,
256
+ )
257
+ from diagram_to_iac.services import (
258
+ get_log_path,
259
+ generate_step_summary,
260
+ reset_log_bus
261
+ )
262
+ except ImportError as e:
263
+ logger.error(f"Failed to import required modules: {e}")
264
+ print("❌ Error: Required modules not available")
265
+ print("ℹ️ Please ensure diagram-to-iac is properly installed")
266
+ return 1
267
+
268
+ # Initialize SupervisorAgent
269
+ print("🚀 Initializing SupervisorAgent...")
270
+ try:
271
+ agent_kwargs = {}
272
+ if args.config_path:
273
+ agent_kwargs['config_path'] = args.config_path
274
+
275
+ supervisor = SupervisorAgent(**agent_kwargs)
276
+ except Exception as e:
277
+ logger.error(f"Failed to initialize SupervisorAgent: {e}")
278
+ print(f"❌ Error: Failed to initialize SupervisorAgent: {e}")
279
+ return 1
280
+
281
+ # Reset log bus for clean run
282
+ reset_log_bus()
283
+
284
+ # Prepare SupervisorAgent input
285
+ supervisor_input = SupervisorAgentInput(
286
+ repo_url=args.repository_url,
287
+ branch_name=args.branch_name,
288
+ thread_id=args.thread_id,
289
+ dry_run=args.dry_run,
290
+ )
291
+
292
+ # Execute R2D workflow
293
+ print("🔄 Executing R2D workflow...")
294
+ print("┌─────────────────────────────────────────────────────────────────────────────┐")
295
+ print("│ Workflow: Clone → Detect Stack → Branch Create → Terraform → Issue/PR │")
296
+ print("│ Agents: Git Agent | Shell Agent | Terraform Agent | Policy Agent │")
297
+ print("└─────────────────────────────────────────────────────────────────────────────┘")
298
+ print("")
299
+
300
+ result = supervisor.run(supervisor_input)
301
+
302
+ # Generate step summary
303
+ try:
304
+ step_summary_path = Path("step-summary.md")
305
+ generate_step_summary(get_log_path(), step_summary_path)
306
+ print(f"📊 Step summary generated: {step_summary_path}")
307
+ except Exception as e:
308
+ logger.warning(f"Failed to generate step summary: {e}")
309
+
310
+ # Display results
311
+ print("")
312
+ print("🏁 R2D Workflow Results")
313
+ print("═══════════════════════")
314
+ print(f"🎯 Repository: {result.repo_url}")
315
+ print(f"✅ Success: {result.success}")
316
+ print(f"🌿 Branch created: {result.branch_created}")
317
+ print(f"📝 Issues opened: {result.issues_opened}")
318
+ print(f"🏗️ Stack detected: {result.stack_detected}")
319
+
320
+ if result.terraform_summary:
321
+ print(f"⚡ Terraform summary: {result.terraform_summary}")
322
+
323
+ if result.message:
324
+ print(f"💬 Message: {result.message}")
325
+
326
+ if not result.success:
327
+ print("")
328
+ print("⚠️ R2D workflow encountered issues")
329
+ print("🔄 SupervisorAgent handles errors via GitHub Issues automatically")
330
+ print("📋 Check created issues for error details and auto-fix suggestions")
331
+
332
+ # Return appropriate exit code
333
+ return 0 if result.success else 1
334
+
335
+ except KeyboardInterrupt:
336
+ print("\n⚠️ Operation cancelled by user")
337
+ return 130
338
+ except Exception as e:
339
+ logger.error(f"Unexpected error in R2D CLI: {e}", exc_info=True)
340
+ print(f"❌ Unexpected error: {e}")
341
+ print("🐛 Please report this issue to: https://github.com/amartyamandal/diagram-to-iac/issues")
342
+ return 1
343
+
344
+ if __name__ == "__main__":
345
+ sys.exit(main())
@@ -0,0 +1,10 @@
1
+ from .observability import LogBus, log_event, get_log_path, reset_log_bus
2
+ from .step_summary import generate_step_summary
3
+
4
+ __all__ = [
5
+ "LogBus",
6
+ "log_event",
7
+ "get_log_path",
8
+ "reset_log_bus",
9
+ "generate_step_summary",
10
+ ]
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ import threading
7
+ from typing import Any, Dict
8
+
9
+
10
+ class LogBus:
11
+ """Simple JSONL logging service."""
12
+
13
+ def __init__(self, log_dir: str | Path | None = None) -> None:
14
+ self.log_dir = Path(log_dir) if log_dir else Path(__file__).resolve().parents[3] / "logs"
15
+ self.log_dir.mkdir(parents=True, exist_ok=True)
16
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
17
+ self.log_path = self.log_dir / f"run-{timestamp}.jsonl"
18
+ self._lock = threading.Lock()
19
+
20
+ def log(self, event: Dict[str, Any]) -> None:
21
+ """Append an event as a JSON line with timestamp."""
22
+ payload = event.copy()
23
+ payload.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
24
+ line = json.dumps(payload)
25
+ with self._lock:
26
+ try:
27
+ with open(self.log_path, "a", encoding="utf-8") as f:
28
+ f.write(line + "\n")
29
+ except Exception as exc: # noqa: BLE001
30
+ # Logging should never raise; print error and continue
31
+ print(f"LogBus write failed: {exc}")
32
+
33
+
34
+ _global_bus: LogBus | None = None
35
+
36
+
37
+ def _get_bus() -> LogBus:
38
+ global _global_bus
39
+ if _global_bus is None:
40
+ _global_bus = LogBus()
41
+ return _global_bus
42
+
43
+
44
+ def log_event(event_type: str, **kwargs: Any) -> None:
45
+ """Write a structured log event using the global bus."""
46
+ event = {"type": event_type, **kwargs}
47
+ _get_bus().log(event)
48
+
49
+
50
+ def get_log_path() -> Path:
51
+ """Return the path of the current log file."""
52
+ return _get_bus().log_path
53
+
54
+
55
+ def reset_log_bus() -> None:
56
+ """Create a fresh global log bus with a new log file."""
57
+ global _global_bus
58
+ # Nothing to close since LogBus opens files on demand
59
+ _global_bus = LogBus()
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+
9
+ def _parse_timestamp(ts: str) -> datetime:
10
+ try:
11
+ return datetime.fromisoformat(ts)
12
+ except Exception:
13
+ # Fallback for timestamps that may not be ISO formatted
14
+ return datetime.strptime(ts.split(".")[0], "%Y-%m-%dT%H:%M:%S")
15
+
16
+
17
+ def generate_step_summary(log_path: str | Path, output_path: str | Path, *, stdout: bool = False) -> str:
18
+ """Generate a Markdown step summary from JSONL logs."""
19
+ log_path = Path(log_path)
20
+ output_path = Path(output_path)
21
+
22
+ modules: set[str] = set()
23
+ adds = changes = destroys = 0
24
+ critical = high = medium = low = 0
25
+ start: datetime | None = None
26
+ end: datetime | None = None
27
+
28
+ if not log_path.exists():
29
+ raise FileNotFoundError(log_path)
30
+
31
+ with log_path.open("r", encoding="utf-8") as f:
32
+ for line in f:
33
+ line = line.strip()
34
+ if not line:
35
+ continue
36
+ data = json.loads(line)
37
+ ts = data.get("timestamp")
38
+ if ts:
39
+ ts_dt = _parse_timestamp(ts)
40
+ if start is None or ts_dt < start:
41
+ start = ts_dt
42
+ if end is None or ts_dt > end:
43
+ end = ts_dt
44
+ result = data.get("result", "")
45
+ if result:
46
+ modules.update(re.findall(r"module\.([\w-]+)", result))
47
+ m = re.search(r"Plan:\s*(\d+)\s*to\s*add,\s*(\d+)\s*to\s*change,\s*(\d+)\s*to\s*destroy", result)
48
+ if m:
49
+ adds = int(m.group(1))
50
+ changes = int(m.group(2))
51
+ destroys = int(m.group(3))
52
+ m = re.search(r"(\d+)\s*critical", result, re.IGNORECASE)
53
+ if m:
54
+ critical = int(m.group(1))
55
+ m = re.search(r"(\d+)\s*high", result, re.IGNORECASE)
56
+ if m:
57
+ high = int(m.group(1))
58
+ m = re.search(r"(\d+)\s*medium", result, re.IGNORECASE)
59
+ if m:
60
+ medium = int(m.group(1))
61
+ m = re.search(r"(\d+)\s*low", result, re.IGNORECASE)
62
+ if m:
63
+ low = int(m.group(1))
64
+
65
+ runtime = (end - start).total_seconds() if start and end else 0
66
+ modules_str = ", ".join(sorted(modules)) if modules else "root"
67
+
68
+ md = (
69
+ "| Module | Adds | Changes | Destroys | Critical | High | Medium | Low | Run Time (s) |\n"
70
+ "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n"
71
+ f"| {modules_str} | {adds} | {changes} | {destroys} | {critical} | {high} | {medium} | {low} | {int(runtime)} |\n"
72
+ )
73
+
74
+ output_path.write_text(md, encoding="utf-8")
75
+ if stdout:
76
+ print(md)
77
+ return md
@@ -0,0 +1,11 @@
1
+ # This file makes the 'tools' module a package.
2
+
3
+ # Note: Agent-specific tools organization:
4
+ # - cal_utils, text_utils -> tools/hello/
5
+ # - git_tools -> tools/git/ (moved from agents/git_langgraph/tools/)
6
+ # - shell_tools -> agents/shell_langgraph/tools/
7
+
8
+ # You can expose shared tools here as needed
9
+ # For example:
10
+ # from .llm_utils import router (if router.py is in llm_utils subdirectory)
11
+ # from .cv_utils import some_vision_function