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.
- diagram_to_iac/__init__.py +10 -0
- diagram_to_iac/actions/__init__.py +7 -0
- diagram_to_iac/actions/git_entry.py +174 -0
- diagram_to_iac/actions/supervisor_entry.py +116 -0
- diagram_to_iac/actions/terraform_agent_entry.py +207 -0
- diagram_to_iac/agents/__init__.py +26 -0
- diagram_to_iac/agents/demonstrator_langgraph/__init__.py +10 -0
- diagram_to_iac/agents/demonstrator_langgraph/agent.py +826 -0
- diagram_to_iac/agents/git_langgraph/__init__.py +10 -0
- diagram_to_iac/agents/git_langgraph/agent.py +1018 -0
- diagram_to_iac/agents/git_langgraph/pr.py +146 -0
- diagram_to_iac/agents/hello_langgraph/__init__.py +9 -0
- diagram_to_iac/agents/hello_langgraph/agent.py +621 -0
- diagram_to_iac/agents/policy_agent/__init__.py +15 -0
- diagram_to_iac/agents/policy_agent/agent.py +507 -0
- diagram_to_iac/agents/policy_agent/integration_example.py +191 -0
- diagram_to_iac/agents/policy_agent/tools/__init__.py +14 -0
- diagram_to_iac/agents/policy_agent/tools/tfsec_tool.py +259 -0
- diagram_to_iac/agents/shell_langgraph/__init__.py +21 -0
- diagram_to_iac/agents/shell_langgraph/agent.py +122 -0
- diagram_to_iac/agents/shell_langgraph/detector.py +50 -0
- diagram_to_iac/agents/supervisor_langgraph/__init__.py +17 -0
- diagram_to_iac/agents/supervisor_langgraph/agent.py +1947 -0
- diagram_to_iac/agents/supervisor_langgraph/demonstrator.py +22 -0
- diagram_to_iac/agents/supervisor_langgraph/guards.py +23 -0
- diagram_to_iac/agents/supervisor_langgraph/pat_loop.py +49 -0
- diagram_to_iac/agents/supervisor_langgraph/router.py +9 -0
- diagram_to_iac/agents/terraform_langgraph/__init__.py +15 -0
- diagram_to_iac/agents/terraform_langgraph/agent.py +1216 -0
- diagram_to_iac/agents/terraform_langgraph/parser.py +76 -0
- diagram_to_iac/core/__init__.py +7 -0
- diagram_to_iac/core/agent_base.py +19 -0
- diagram_to_iac/core/enhanced_memory.py +302 -0
- diagram_to_iac/core/errors.py +4 -0
- diagram_to_iac/core/issue_tracker.py +49 -0
- diagram_to_iac/core/memory.py +132 -0
- diagram_to_iac/r2d.py +345 -13
- diagram_to_iac/services/__init__.py +10 -0
- diagram_to_iac/services/observability.py +59 -0
- diagram_to_iac/services/step_summary.py +77 -0
- diagram_to_iac/tools/__init__.py +11 -0
- diagram_to_iac/tools/api_utils.py +108 -26
- diagram_to_iac/tools/git/__init__.py +45 -0
- diagram_to_iac/tools/git/git.py +956 -0
- diagram_to_iac/tools/hello/__init__.py +30 -0
- diagram_to_iac/tools/hello/cal_utils.py +31 -0
- diagram_to_iac/tools/hello/text_utils.py +97 -0
- diagram_to_iac/tools/llm_utils/__init__.py +20 -0
- diagram_to_iac/tools/llm_utils/anthropic_driver.py +87 -0
- diagram_to_iac/tools/llm_utils/base_driver.py +90 -0
- diagram_to_iac/tools/llm_utils/gemini_driver.py +89 -0
- diagram_to_iac/tools/llm_utils/openai_driver.py +93 -0
- diagram_to_iac/tools/llm_utils/router.py +303 -0
- diagram_to_iac/tools/sec_utils.py +4 -2
- diagram_to_iac/tools/shell/__init__.py +17 -0
- diagram_to_iac/tools/shell/shell.py +415 -0
- diagram_to_iac/tools/text_utils.py +277 -0
- diagram_to_iac/tools/tf/terraform.py +851 -0
- diagram_to_iac-0.9.0.dist-info/METADATA +256 -0
- diagram_to_iac-0.9.0.dist-info/RECORD +64 -0
- {diagram_to_iac-0.7.0.dist-info → diagram_to_iac-0.9.0.dist-info}/WHEEL +1 -1
- diagram_to_iac-0.9.0.dist-info/entry_points.txt +6 -0
- diagram_to_iac/agents/codegen_agent.py +0 -0
- diagram_to_iac/agents/consensus_agent.py +0 -0
- diagram_to_iac/agents/deployment_agent.py +0 -0
- diagram_to_iac/agents/github_agent.py +0 -0
- diagram_to_iac/agents/interpretation_agent.py +0 -0
- diagram_to_iac/agents/question_agent.py +0 -0
- diagram_to_iac/agents/supervisor.py +0 -0
- diagram_to_iac/agents/vision_agent.py +0 -0
- diagram_to_iac/core/config.py +0 -0
- diagram_to_iac/tools/cv_utils.py +0 -0
- diagram_to_iac/tools/gh_utils.py +0 -0
- diagram_to_iac/tools/tf_utils.py +0 -0
- diagram_to_iac-0.7.0.dist-info/METADATA +0 -16
- diagram_to_iac-0.7.0.dist-info/RECORD +0 -32
- diagram_to_iac-0.7.0.dist-info/entry_points.txt +0 -2
- {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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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,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
|
diagram_to_iac/tools/__init__.py
CHANGED
@@ -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
|