diagram-to-iac 0.7.0__py3-none-any.whl → 0.8.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/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.8.0.dist-info/METADATA +99 -0
- diagram_to_iac-0.8.0.dist-info/RECORD +64 -0
- {diagram_to_iac-0.7.0.dist-info → diagram_to_iac-0.8.0.dist-info}/WHEEL +1 -1
- diagram_to_iac-0.8.0.dist-info/entry_points.txt +4 -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.8.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)}
|