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
@@ -0,0 +1,826 @@
1
+ """
2
+ DemonstratorAgent: Intelligent Interactive Dry-Run Agent
3
+
4
+ This agent specializes in demonstrating errors, analyzing fixes, and guiding users
5
+ through interactive problem-solving in an organic, agentic way.
6
+
7
+ Follows the same organic LangGraph pattern as other agents in the system.
8
+ """
9
+
10
+ import os
11
+ import re
12
+ import yaml
13
+ import uuid
14
+ import logging
15
+ import getpass
16
+ from datetime import datetime
17
+ from typing import Dict, List, Optional, Union, TypedDict, Any
18
+
19
+ from langchain_core.messages import HumanMessage
20
+ from langgraph.graph import StateGraph, END
21
+ from langgraph.checkpoint.memory import MemorySaver
22
+
23
+ from diagram_to_iac.core.agent_base import AgentBase
24
+ from diagram_to_iac.core.memory import create_memory
25
+ from diagram_to_iac.tools.llm_utils.router import LLMRouter, get_llm
26
+ from diagram_to_iac.tools.text_utils import (
27
+ generate_organic_issue_title,
28
+ enhance_error_message_for_issue,
29
+ create_issue_metadata_section,
30
+ )
31
+
32
+ # Import the agents we need to delegate to
33
+ from diagram_to_iac.agents.git_langgraph import GitAgent, GitAgentInput
34
+ from diagram_to_iac.agents.terraform_langgraph import TerraformAgent, TerraformAgentInput
35
+
36
+
37
+ class DemonstratorAgentInput(TypedDict):
38
+ """Input structure for DemonstratorAgent."""
39
+
40
+ query: str # The demonstration request
41
+ error_type: str # Type of error to demonstrate
42
+ error_message: str # Original error message
43
+ repo_url: str # Repository URL
44
+ branch_name: Optional[str] # Branch name
45
+ stack_detected: Optional[Dict[str, int]] # Detected stack info
46
+ issue_title: str # Proposed issue title
47
+ issue_body: str # Proposed issue body
48
+ existing_issue_id: Optional[int] # Existing issue ID if any
49
+ thread_id: Optional[str] # Thread ID for conversation continuity
50
+
51
+
52
+ class DemonstratorAgentOutput(TypedDict):
53
+ """Output structure for DemonstratorAgent."""
54
+
55
+ result: str # Demonstration result
56
+ action_taken: str # What action was taken
57
+ user_choice: str # User's choice during demonstration
58
+ retry_attempted: bool # Whether a retry was attempted
59
+ retry_successful: Optional[bool] # Whether retry succeeded
60
+ issue_created: bool # Whether an issue was created
61
+ error_message: Optional[str] # Any error during demonstration
62
+ thread_id: str # Thread ID
63
+
64
+
65
+ class DemonstratorAgentState(TypedDict):
66
+ """State structure for DemonstratorAgent LangGraph."""
67
+
68
+ input_message: HumanMessage
69
+ query: str
70
+ error_type: str
71
+ error_message: str
72
+ repo_url: str
73
+ branch_name: Optional[str]
74
+ stack_detected: Optional[Dict[str, int]]
75
+ issue_title: str
76
+ issue_body: str
77
+ existing_issue_id: Optional[int]
78
+ thread_id: Optional[str]
79
+
80
+ # State tracking
81
+ error_analysis: Optional[Dict[str, Any]]
82
+ user_choice: Optional[str]
83
+ user_inputs: Optional[Dict[str, str]]
84
+ retry_attempted: bool
85
+ retry_successful: Optional[bool]
86
+ issue_created: bool
87
+ final_result: str
88
+ operation_type: str
89
+ error_occurred: Optional[str]
90
+
91
+
92
+ class DemonstratorAgent(AgentBase):
93
+ """
94
+ DemonstratorAgent specializes in interactive dry-run demonstrations.
95
+
96
+ Uses organic LangGraph architecture to:
97
+ 1. Analyze errors intelligently
98
+ 2. Guide users through fixing problems
99
+ 3. Demonstrate potential solutions
100
+ 4. Handle retries with user-provided fixes
101
+ 5. Create GitHub issues when needed
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ config_path: Optional[str] = None,
107
+ memory_type: str = "persistent",
108
+ git_agent: Optional[GitAgent] = None,
109
+ terraform_agent: Optional[TerraformAgent] = None,
110
+ ) -> None:
111
+ self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
112
+
113
+ # Load configuration
114
+ if config_path is None:
115
+ base_dir = os.path.dirname(os.path.abspath(__file__))
116
+ config_path = os.path.join(base_dir, "config.yaml")
117
+
118
+ try:
119
+ with open(config_path, "r") as f:
120
+ self.config = yaml.safe_load(f)
121
+ if self.config is None:
122
+ self._set_default_config()
123
+ else:
124
+ self.logger.info(f"Configuration loaded successfully from {config_path}")
125
+ except FileNotFoundError:
126
+ self.logger.warning(f"Configuration file not found at {config_path}. Using defaults.")
127
+ self._set_default_config()
128
+ except yaml.YAMLError as e:
129
+ self.logger.error(f"Error parsing YAML configuration: {e}. Using defaults.")
130
+ self._set_default_config()
131
+
132
+ # Initialize enhanced LLM router
133
+ self.llm_router = LLMRouter()
134
+ self.logger.info("Enhanced LLM router initialized")
135
+
136
+ # Initialize enhanced memory system
137
+ self.memory = create_memory(memory_type)
138
+ self.logger.info(f"Enhanced memory system initialized: {type(self.memory).__name__}")
139
+
140
+ # Initialize checkpointer
141
+ self.checkpointer = MemorySaver()
142
+ self.logger.info("MemorySaver checkpointer initialized")
143
+
144
+ # Initialize delegate agents
145
+ self.git_agent = git_agent or GitAgent()
146
+ self.terraform_agent = terraform_agent or TerraformAgent()
147
+ self.logger.info("Delegate agents initialized")
148
+
149
+ # Build and compile the LangGraph
150
+ self.runnable = self._build_graph()
151
+ self.logger.info("DemonstratorAgent initialized successfully with organic LangGraph architecture")
152
+
153
+ def _set_default_config(self):
154
+ """Set default configuration values."""
155
+ self.config = {
156
+ "llm": {"model_name": "gpt-4o-mini", "temperature": 0.1},
157
+ "routing_keys": {
158
+ "analyze": "ROUTE_TO_ANALYZE",
159
+ "demonstrate": "ROUTE_TO_DEMONSTRATE",
160
+ "collect_inputs": "ROUTE_TO_COLLECT_INPUTS",
161
+ "retry": "ROUTE_TO_RETRY",
162
+ "create_issue": "ROUTE_TO_CREATE_ISSUE",
163
+ "end": "ROUTE_TO_END",
164
+ },
165
+ "prompts": {
166
+ "planner_prompt": """User request: "{user_input}"
167
+
168
+ This is a dry-run demonstration request for error: {error_type}
169
+ Error message: {error_message}
170
+
171
+ Analyze this request and determine the appropriate action:
172
+ 1. If need to analyze the error for user guidance, respond with "{route_analyze}"
173
+ 2. If need to demonstrate the issue to user, respond with "{route_demonstrate}"
174
+ 3. If need to collect user inputs for fixing, respond with "{route_collect_inputs}"
175
+ 4. If need to retry with new information, respond with "{route_retry}"
176
+ 5. If need to create GitHub issue, respond with "{route_create_issue}"
177
+ 6. If demonstration is complete, respond with "{route_end}"
178
+
179
+ Important: Focus on being helpful and educational in demonstrating the error and potential fixes."""
180
+ },
181
+ }
182
+ self.logger.info("Default configuration set")
183
+
184
+ # --- AgentBase interface ---
185
+
186
+ def plan(self, query: str, **kwargs):
187
+ """Generate a plan for the demonstration (required by AgentBase)."""
188
+ return {
189
+ "input_query": query,
190
+ "predicted_action": "interactive_demonstration",
191
+ "description": "Demonstrate error and guide user through fixing it",
192
+ }
193
+
194
+ def report(self, *args, **kwargs):
195
+ """Get current memory state (required by AgentBase)."""
196
+ return self.get_memory_state()
197
+
198
+ def run(self, demo_input: DemonstratorAgentInput) -> DemonstratorAgentOutput:
199
+ """
200
+ Run the DemonstratorAgent with organic LangGraph execution.
201
+ """
202
+ self.logger.info(f"DemonstratorAgent run invoked for error type: '{demo_input['error_type']}'")
203
+
204
+ if not self.runnable:
205
+ error_msg = "DemonstratorAgent runnable not initialized"
206
+ self.logger.error(error_msg)
207
+ return DemonstratorAgentOutput(
208
+ result=f"Error: {error_msg}",
209
+ action_taken="error",
210
+ user_choice="none",
211
+ retry_attempted=False,
212
+ retry_successful=None,
213
+ issue_created=False,
214
+ error_message=error_msg,
215
+ thread_id=demo_input.get("thread_id", str(uuid.uuid4())),
216
+ )
217
+
218
+ try:
219
+ # Create initial state
220
+ initial_state: DemonstratorAgentState = {
221
+ "input_message": HumanMessage(content=demo_input["query"]),
222
+ "query": demo_input["query"],
223
+ "error_type": demo_input["error_type"],
224
+ "error_message": demo_input["error_message"],
225
+ "repo_url": demo_input["repo_url"],
226
+ "branch_name": demo_input.get("branch_name"),
227
+ "stack_detected": demo_input.get("stack_detected"),
228
+ "issue_title": demo_input["issue_title"],
229
+ "issue_body": demo_input["issue_body"],
230
+ "existing_issue_id": demo_input.get("existing_issue_id"),
231
+ "thread_id": demo_input.get("thread_id", str(uuid.uuid4())),
232
+ "error_analysis": None,
233
+ "user_choice": None,
234
+ "user_inputs": None,
235
+ "retry_attempted": False,
236
+ "retry_successful": None,
237
+ "issue_created": False,
238
+ "final_result": "",
239
+ "operation_type": "",
240
+ "error_occurred": None,
241
+ }
242
+
243
+ # Execute the organic LangGraph workflow
244
+ config = {"configurable": {"thread_id": initial_state["thread_id"]}}
245
+ final_state = self.runnable.invoke(initial_state, config=config)
246
+
247
+ # Convert final state to output
248
+ return DemonstratorAgentOutput(
249
+ result=final_state.get("final_result", "Demonstration completed"),
250
+ action_taken=final_state.get("operation_type", "unknown"),
251
+ user_choice=final_state.get("user_choice", "none"),
252
+ retry_attempted=final_state.get("retry_attempted", False),
253
+ retry_successful=final_state.get("retry_successful"),
254
+ issue_created=final_state.get("issue_created", False),
255
+ error_message=final_state.get("error_occurred"),
256
+ thread_id=final_state["thread_id"],
257
+ )
258
+
259
+ except Exception as e:
260
+ self.logger.error(f"Error in DemonstratorAgent run: {e}", exc_info=True)
261
+ return DemonstratorAgentOutput(
262
+ result=f"Demonstration failed: {str(e)}",
263
+ action_taken="error",
264
+ user_choice="none",
265
+ retry_attempted=False,
266
+ retry_successful=None,
267
+ issue_created=False,
268
+ error_message=str(e),
269
+ thread_id=demo_input.get("thread_id", str(uuid.uuid4())),
270
+ )
271
+
272
+ # --- Organic LangGraph Architecture ---
273
+
274
+ def _build_graph(self) -> StateGraph:
275
+ """Build the organic LangGraph for DemonstratorAgent."""
276
+ graph = StateGraph(DemonstratorAgentState)
277
+
278
+ # Add nodes
279
+ graph.add_node("planner_node", self._planner_node)
280
+ graph.add_node("analyze_error_node", self._analyze_error_node)
281
+ graph.add_node("demonstrate_issue_node", self._demonstrate_issue_node)
282
+ graph.add_node("collect_inputs_node", self._collect_inputs_node)
283
+ graph.add_node("retry_operation_node", self._retry_operation_node)
284
+ graph.add_node("create_issue_node", self._create_issue_node)
285
+
286
+ # Set entry point
287
+ graph.set_entry_point("planner_node")
288
+
289
+ # Add routing
290
+ graph.add_conditional_edges("planner_node", self._route_after_planner)
291
+ graph.add_conditional_edges("analyze_error_node", self._route_after_analysis)
292
+ graph.add_conditional_edges("demonstrate_issue_node", self._route_after_demonstration)
293
+ graph.add_conditional_edges("collect_inputs_node", self._route_after_collection)
294
+ graph.add_conditional_edges("retry_operation_node", self._route_after_retry)
295
+ graph.add_edge("create_issue_node", END)
296
+
297
+ return graph.compile(checkpointer=self.checkpointer)
298
+
299
+ def _planner_node(self, state: DemonstratorAgentState) -> DemonstratorAgentState:
300
+ """LLM planner node for demonstration workflow."""
301
+ self.logger.info("DemonstratorAgent planner analyzing demonstration request")
302
+
303
+ # Get LLM
304
+ llm_config = self.config.get("llm", {})
305
+ try:
306
+ llm = self.llm_router.get_llm(
307
+ model_name=llm_config.get("model_name", "gpt-4o-mini"),
308
+ temperature=llm_config.get("temperature", 0.1),
309
+ agent_name="demonstrator_agent",
310
+ )
311
+ except Exception as e:
312
+ self.logger.error(f"Failed to get LLM: {e}")
313
+ llm = get_llm()
314
+
315
+ # Store conversation in memory
316
+ self.memory.add_to_conversation(
317
+ "user", state["query"], {"agent": "demonstrator_agent", "node": "planner"}
318
+ )
319
+
320
+ try:
321
+ # Build analysis prompt
322
+ routing_keys = self.config.get("routing_keys", {})
323
+ prompt_template = self.config.get("prompts", {}).get("planner_prompt", "")
324
+
325
+ analysis_prompt = prompt_template.format(
326
+ user_input=state["query"],
327
+ error_type=state["error_type"],
328
+ error_message=state["error_message"],
329
+ route_analyze=routing_keys.get("analyze", "ROUTE_TO_ANALYZE"),
330
+ route_demonstrate=routing_keys.get("demonstrate", "ROUTE_TO_DEMONSTRATE"),
331
+ route_collect_inputs=routing_keys.get("collect_inputs", "ROUTE_TO_COLLECT_INPUTS"),
332
+ route_retry=routing_keys.get("retry", "ROUTE_TO_RETRY"),
333
+ route_create_issue=routing_keys.get("create_issue", "ROUTE_TO_CREATE_ISSUE"),
334
+ route_end=routing_keys.get("end", "ROUTE_TO_END"),
335
+ )
336
+
337
+ response = llm.invoke([HumanMessage(content=analysis_prompt)])
338
+ response_content = response.content.strip()
339
+
340
+ # Store LLM response
341
+ self.memory.add_to_conversation(
342
+ "assistant", response_content,
343
+ {"agent": "demonstrator_agent", "node": "planner"}
344
+ )
345
+
346
+ # Determine routing
347
+ if routing_keys.get("analyze", "ROUTE_TO_ANALYZE") in response_content:
348
+ next_action = "analyze_error"
349
+ elif routing_keys.get("demonstrate", "ROUTE_TO_DEMONSTRATE") in response_content:
350
+ next_action = "demonstrate_issue"
351
+ elif routing_keys.get("collect_inputs", "ROUTE_TO_COLLECT_INPUTS") in response_content:
352
+ next_action = "collect_inputs"
353
+ elif routing_keys.get("retry", "ROUTE_TO_RETRY") in response_content:
354
+ next_action = "retry_operation"
355
+ elif routing_keys.get("create_issue", "ROUTE_TO_CREATE_ISSUE") in response_content:
356
+ next_action = "create_issue"
357
+ else:
358
+ next_action = "analyze_error" # Default: start with analysis
359
+
360
+ self.logger.info(f"DemonstratorAgent planner decision: {next_action}")
361
+
362
+ return {
363
+ **state,
364
+ "final_result": next_action,
365
+ "operation_type": "planning",
366
+ }
367
+
368
+ except Exception as e:
369
+ self.logger.error(f"Error in demonstrator planner: {e}")
370
+ return {
371
+ **state,
372
+ "final_result": "create_issue",
373
+ "operation_type": "planner_error",
374
+ "error_occurred": str(e),
375
+ }
376
+
377
+ def _analyze_error_node(self, state: DemonstratorAgentState) -> DemonstratorAgentState:
378
+ """Analyze the error and provide intelligent guidance."""
379
+ self.logger.info(f"Analyzing error type: {state['error_type']}")
380
+
381
+ try:
382
+ # Analyze the error for fixability and guidance
383
+ error_analysis = self._get_error_analysis(state["error_type"], state["error_message"])
384
+
385
+ self.logger.info(f"Error analysis complete: fixable={error_analysis['fixable']}")
386
+
387
+ return {
388
+ **state,
389
+ "error_analysis": error_analysis,
390
+ "final_result": "demonstrate_issue",
391
+ "operation_type": "analysis_complete",
392
+ }
393
+
394
+ except Exception as e:
395
+ self.logger.error(f"Error in analysis node: {e}")
396
+ return {
397
+ **state,
398
+ "final_result": "create_issue",
399
+ "operation_type": "analysis_error",
400
+ "error_occurred": str(e),
401
+ }
402
+
403
+ def _demonstrate_issue_node(self, state: DemonstratorAgentState) -> DemonstratorAgentState:
404
+ """Demonstrate the issue to the user interactively."""
405
+ self.logger.info("Demonstrating issue to user")
406
+
407
+ try:
408
+ error_analysis = state.get("error_analysis") or self._get_error_analysis(
409
+ state["error_type"], state["error_message"]
410
+ )
411
+
412
+ # Display the demonstration
413
+ print("\n" + "="*80)
414
+ print("🔍 INTELLIGENT DRY RUN: R2D Workflow Error Analysis")
415
+ print("="*80)
416
+
417
+ print(f"\n📍 **Repository:** {state['repo_url']}")
418
+ print(f"🏷️ **Error Type:** {state['error_type']}")
419
+ if state.get("existing_issue_id"):
420
+ print(f"🔗 **Existing Issue:** Found issue #{state['existing_issue_id']} (would update)")
421
+ else:
422
+ print(f"🆕 **New Issue:** Would create new issue")
423
+
424
+ print(f"\n🧠 **Error Analysis:**")
425
+ print(f" {error_analysis['description']}")
426
+
427
+ if error_analysis['fixable']:
428
+ print(f"\n✅ **Good News:** This error can potentially be fixed!")
429
+ print(f" {error_analysis['fix_guidance']}")
430
+
431
+ if error_analysis['required_inputs']:
432
+ print(f"\n📝 **Required Information:**")
433
+ for req in error_analysis['required_inputs']:
434
+ print(f" • {req}")
435
+ else:
436
+ print(f"\n❌ **This error requires manual intervention:**")
437
+ print(f" {error_analysis['manual_steps']}")
438
+
439
+ print(f"\n📝 **Proposed Issue Title:**")
440
+ print(f" {state['issue_title']}")
441
+
442
+ print("\n" + "="*80)
443
+ print("🤔 What would you like to do?")
444
+ print("="*80)
445
+
446
+ if error_analysis['fixable']:
447
+ print("1. 🔧 Fix - Provide missing information and retry")
448
+ print("2. 🚀 Create Issue - Log this error as a GitHub issue")
449
+ print("3. 📋 Details - Show full error details and proposed issue")
450
+ print("4. ❌ Abort - Skip and end workflow")
451
+ max_choice = 4
452
+ else:
453
+ print("1. 🚀 Create Issue - Log this error as a GitHub issue")
454
+ print("2. 📋 Details - Show full error details and proposed issue")
455
+ print("3. ❌ Abort - Skip and end workflow")
456
+ max_choice = 3
457
+
458
+ # Get user choice
459
+ choice = self._get_user_choice(max_choice, error_analysis['fixable'])
460
+
461
+ return {
462
+ **state,
463
+ "error_analysis": error_analysis,
464
+ "user_choice": choice,
465
+ "final_result": self._map_choice_to_action(choice, error_analysis['fixable']),
466
+ "operation_type": "demonstration_complete",
467
+ }
468
+
469
+ except Exception as e:
470
+ self.logger.error(f"Error in demonstration node: {e}")
471
+ return {
472
+ **state,
473
+ "final_result": "create_issue",
474
+ "operation_type": "demonstration_error",
475
+ "error_occurred": str(e),
476
+ }
477
+
478
+ def _collect_inputs_node(self, state: DemonstratorAgentState) -> DemonstratorAgentState:
479
+ """Collect required inputs from user for fixing the error."""
480
+ self.logger.info("Collecting user inputs for error fix")
481
+
482
+ try:
483
+ error_analysis = state["error_analysis"]
484
+
485
+ print(f"\n🛠️ **Error Fix Mode: {state['error_type']}**")
486
+ print(f"📋 {error_analysis['fix_guidance']}")
487
+
488
+ # Collect required inputs from user
489
+ user_inputs = {}
490
+ for requirement in error_analysis['required_inputs']:
491
+ key = requirement.split(':')[0].strip()
492
+ description = requirement.split(':', 1)[1].strip() if ':' in requirement else requirement
493
+
494
+ print(f"\n📝 **{key}:**")
495
+ print(f" {description}")
496
+
497
+ # Handle sensitive inputs differently
498
+ if any(sensitive in key.lower() for sensitive in ['token', 'key', 'password']):
499
+ value = getpass.getpass(f"Enter {key} (will be hidden): ").strip()
500
+ if value:
501
+ user_inputs[key] = "***PROVIDED***" # Don't store actual value
502
+ os.environ[key] = value # Set in environment
503
+ else:
504
+ user_inputs[key] = "***NOT_PROVIDED***"
505
+ else:
506
+ value = input(f"Enter {key}: ").strip()
507
+ user_inputs[key] = value or "***NOT_PROVIDED***"
508
+
509
+ # Ask if user wants to retry
510
+ print(f"\n🔄 **Ready to Retry**")
511
+ print(f"📊 Collected information:")
512
+ for key, value in user_inputs.items():
513
+ print(f" • {key}: {value}")
514
+
515
+ retry_choice = input(f"\nWould you like to retry the operation with this information? (y/N): ").strip().lower()
516
+
517
+ if retry_choice in ['y', 'yes']:
518
+ next_action = "retry_operation"
519
+ else:
520
+ next_action = "create_issue"
521
+
522
+ return {
523
+ **state,
524
+ "user_inputs": user_inputs,
525
+ "final_result": next_action,
526
+ "operation_type": "inputs_collected",
527
+ }
528
+
529
+ except Exception as e:
530
+ self.logger.error(f"Error in collect inputs node: {e}")
531
+ return {
532
+ **state,
533
+ "final_result": "create_issue",
534
+ "operation_type": "collect_inputs_error",
535
+ "error_occurred": str(e),
536
+ }
537
+
538
+ def _retry_operation_node(self, state: DemonstratorAgentState) -> DemonstratorAgentState:
539
+ """Retry the failed operation with user-provided fixes."""
540
+ self.logger.info("Retrying operation with user-provided fixes")
541
+
542
+ try:
543
+ print(f"\n🚀 Retrying the operation...")
544
+
545
+ # Determine what to retry based on error type
546
+ if state["error_type"] in ["auth_failed", "terraform_init"]:
547
+ result = self._retry_terraform_operation(state)
548
+ elif state["error_type"] == "api_key_error":
549
+ result = self._retry_api_operation(state)
550
+ else:
551
+ result = {"success": False, "message": "Retry not supported for this error type"}
552
+
553
+ return {
554
+ **state,
555
+ "retry_attempted": True,
556
+ "retry_successful": result["success"],
557
+ "final_result": "end" if result["success"] else "create_issue",
558
+ "operation_type": "retry_complete",
559
+ }
560
+
561
+ except Exception as e:
562
+ self.logger.error(f"Error in retry node: {e}")
563
+ return {
564
+ **state,
565
+ "retry_attempted": True,
566
+ "retry_successful": False,
567
+ "final_result": "create_issue",
568
+ "operation_type": "retry_error",
569
+ "error_occurred": str(e),
570
+ }
571
+
572
+ def _create_issue_node(self, state: DemonstratorAgentState) -> DemonstratorAgentState:
573
+ """Create GitHub issue using GitAgent."""
574
+ self.logger.info("Creating GitHub issue")
575
+
576
+ try:
577
+ print("\n🚀 Creating GitHub issue...")
578
+
579
+ issue_result = self.git_agent.run(
580
+ GitAgentInput(
581
+ query=f"open issue {state['issue_title']} for repository {state['repo_url']}: {state['issue_body']}",
582
+ issue_id=state.get("existing_issue_id"),
583
+ thread_id=state["thread_id"],
584
+ )
585
+ )
586
+
587
+ if issue_result.error_message:
588
+ self.logger.error(f"Issue creation failed: {issue_result.error_message}")
589
+ success = False
590
+ result_msg = f"Issue creation failed: {issue_result.error_message}"
591
+ else:
592
+ success = True
593
+ result_msg = f"GitHub issue created: {issue_result.result}"
594
+ print(f"\n✅ Success! {result_msg}")
595
+
596
+ return {
597
+ **state,
598
+ "issue_created": success,
599
+ "final_result": result_msg,
600
+ "operation_type": "issue_creation_complete",
601
+ }
602
+
603
+ except Exception as e:
604
+ self.logger.error(f"Error in create issue node: {e}")
605
+ return {
606
+ **state,
607
+ "issue_created": False,
608
+ "final_result": f"Issue creation failed: {str(e)}",
609
+ "operation_type": "issue_creation_error",
610
+ "error_occurred": str(e),
611
+ }
612
+
613
+ # --- Routing Methods ---
614
+
615
+ def _route_after_planner(self, state: DemonstratorAgentState) -> str:
616
+ """Route after planner decision."""
617
+ final_result = state.get("final_result", "")
618
+
619
+ if final_result == "analyze_error":
620
+ return "analyze_error_node"
621
+ elif final_result == "demonstrate_issue":
622
+ return "demonstrate_issue_node"
623
+ elif final_result == "collect_inputs":
624
+ return "collect_inputs_node"
625
+ elif final_result == "retry_operation":
626
+ return "retry_operation_node"
627
+ elif final_result == "create_issue":
628
+ return "create_issue_node"
629
+ else:
630
+ return END
631
+
632
+ def _route_after_analysis(self, state: DemonstratorAgentState) -> str:
633
+ """Route after error analysis."""
634
+ return "demonstrate_issue_node" # Always demonstrate after analysis
635
+
636
+ def _route_after_demonstration(self, state: DemonstratorAgentState) -> str:
637
+ """Route after demonstration based on user choice."""
638
+ final_result = state.get("final_result", "")
639
+
640
+ if final_result == "collect_inputs":
641
+ return "collect_inputs_node"
642
+ elif final_result == "create_issue":
643
+ return "create_issue_node"
644
+ elif final_result == "show_details":
645
+ return "demonstrate_issue_node" # Show details and return to demo
646
+ elif final_result == "abort":
647
+ return END
648
+ else:
649
+ return END
650
+
651
+ def _route_after_collection(self, state: DemonstratorAgentState) -> str:
652
+ """Route after input collection."""
653
+ final_result = state.get("final_result", "")
654
+
655
+ if final_result == "retry_operation":
656
+ return "retry_operation_node"
657
+ else:
658
+ return "create_issue_node"
659
+
660
+ def _route_after_retry(self, state: DemonstratorAgentState) -> str:
661
+ """Route after retry attempt."""
662
+ if state.get("retry_successful", False):
663
+ return END
664
+ else:
665
+ return "create_issue_node"
666
+
667
+ # --- Helper Methods ---
668
+
669
+ def _get_error_analysis(self, error_type: str, error_message: str) -> Dict[str, Any]:
670
+ """Analyze the error and provide guidance."""
671
+ analysis = {
672
+ "description": "Unknown error occurred",
673
+ "fixable": False,
674
+ "fix_guidance": "",
675
+ "required_inputs": [],
676
+ "manual_steps": "Please check the logs and create a GitHub issue",
677
+ }
678
+
679
+ if error_type == "auth_failed" or "missing_terraform_token" in error_message.lower():
680
+ analysis.update({
681
+ "description": "Terraform Cloud authentication is missing. The TFE_TOKEN environment variable is not set.",
682
+ "fixable": True,
683
+ "fix_guidance": "Terraform requires a valid token to authenticate with Terraform Cloud. You can get this token from your Terraform Cloud account.",
684
+ "required_inputs": [
685
+ "TFE_TOKEN: Your Terraform Cloud API token",
686
+ "TF_WORKSPACE: Terraform Cloud workspace name (optional)"
687
+ ],
688
+ })
689
+ elif error_type == "api_key_error" or "401" in error_message:
690
+ analysis.update({
691
+ "description": "API authentication failed. The API key might be missing or invalid.",
692
+ "fixable": True,
693
+ "fix_guidance": "The system needs valid API credentials to function properly.",
694
+ "required_inputs": [
695
+ "OPENAI_API_KEY: Your OpenAI API key (if using OpenAI)",
696
+ "ANTHROPIC_API_KEY: Your Anthropic API key (if using Claude)",
697
+ "GITHUB_TOKEN: Your GitHub Personal Access Token"
698
+ ],
699
+ })
700
+ elif error_type == "terraform_init":
701
+ if "backend" in error_message.lower():
702
+ analysis.update({
703
+ "description": "Terraform backend configuration issue. The backend might not be properly configured.",
704
+ "fixable": True,
705
+ "fix_guidance": "Terraform backend needs proper configuration or credentials.",
706
+ "required_inputs": [
707
+ "Backend configuration details",
708
+ "Access credentials for the backend"
709
+ ],
710
+ })
711
+ else:
712
+ analysis.update({
713
+ "description": "Terraform initialization failed for unknown reasons.",
714
+ "fixable": False,
715
+ "manual_steps": "Check Terraform configuration files, ensure providers are properly specified, and verify network connectivity."
716
+ })
717
+ elif error_type == "network_error":
718
+ analysis.update({
719
+ "description": "Network connectivity issue. Cannot reach external services.",
720
+ "fixable": True,
721
+ "fix_guidance": "Check your internet connection and try again. You may also need to configure proxy settings.",
722
+ "required_inputs": [
723
+ "Network connectivity confirmation",
724
+ "Proxy settings (if behind a corporate firewall)"
725
+ ],
726
+ })
727
+ elif error_type == "permission_error":
728
+ analysis.update({
729
+ "description": "Permission denied. The system lacks necessary permissions.",
730
+ "fixable": False,
731
+ "manual_steps": "Check file permissions, directory access rights, and ensure the process has necessary privileges."
732
+ })
733
+
734
+ return analysis
735
+
736
+ def _get_user_choice(self, max_choice: int, fixable: bool) -> str:
737
+ """Get user choice with error handling."""
738
+ while True:
739
+ try:
740
+ if fixable:
741
+ choice = input(f"\nEnter your choice (1-{max_choice}): ").strip()
742
+ else:
743
+ choice = input(f"\nEnter your choice (1-{max_choice}): ").strip()
744
+
745
+ if choice in [str(i) for i in range(1, max_choice + 1)]:
746
+ return choice
747
+ else:
748
+ print(f"❓ Invalid choice '{choice}'. Please enter a valid option.")
749
+ continue
750
+
751
+ except (KeyboardInterrupt, EOFError):
752
+ print(f"\n\n⚠️ User interrupted! Aborting workflow.")
753
+ return "abort"
754
+
755
+ def _map_choice_to_action(self, choice: str, fixable: bool) -> str:
756
+ """Map user choice to next action."""
757
+ if choice == "abort":
758
+ return "abort"
759
+
760
+ if fixable:
761
+ if choice == "1":
762
+ return "collect_inputs"
763
+ elif choice == "2":
764
+ return "create_issue"
765
+ elif choice == "3":
766
+ return "show_details"
767
+ elif choice == "4":
768
+ return "abort"
769
+ else:
770
+ if choice == "1":
771
+ return "create_issue"
772
+ elif choice == "2":
773
+ return "show_details"
774
+ elif choice == "3":
775
+ return "abort"
776
+
777
+ return "create_issue" # Default
778
+
779
+ def _retry_terraform_operation(self, state: DemonstratorAgentState) -> Dict[str, Any]:
780
+ """Retry Terraform operation with new credentials."""
781
+ try:
782
+ # Extract repo name from URL
783
+ repo_name = state["repo_url"].split('/')[-1].replace('.git', '')
784
+ repo_path = f"/workspace/{repo_name}"
785
+
786
+ # Try terraform init again
787
+ tf_result = self.terraform_agent.run(
788
+ TerraformAgentInput(
789
+ query=f"terraform init in {repo_path}",
790
+ thread_id=state["thread_id"],
791
+ )
792
+ )
793
+
794
+ if tf_result.error_message:
795
+ print(f"❌ Retry failed: {tf_result.error_message}")
796
+ return {"success": False, "message": tf_result.error_message}
797
+ else:
798
+ print(f"✅ Retry successful! Terraform init completed.")
799
+ return {"success": True, "message": "Terraform init completed successfully"}
800
+
801
+ except Exception as e:
802
+ print(f"❌ Retry failed with exception: {e}")
803
+ return {"success": False, "message": str(e)}
804
+
805
+ def _retry_api_operation(self, state: DemonstratorAgentState) -> Dict[str, Any]:
806
+ """Retry API operation with new credentials."""
807
+ try:
808
+ # Test API connection
809
+ llm = self.llm_router.get_llm(
810
+ model_name="gpt-4o-mini",
811
+ temperature=0.1,
812
+ agent_name="demonstrator_agent",
813
+ )
814
+
815
+ test_response = llm.invoke([HumanMessage(content="Test API connection")])
816
+
817
+ if test_response and test_response.content:
818
+ print(f"✅ Retry successful! API connection restored.")
819
+ return {"success": True, "message": "API connection successful"}
820
+ else:
821
+ print(f"❌ Retry failed: No response from API")
822
+ return {"success": False, "message": "No response from API"}
823
+
824
+ except Exception as e:
825
+ print(f"❌ Retry failed with exception: {e}")
826
+ return {"success": False, "message": str(e)}