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.
Files changed (77) 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/services/__init__.py +10 -0
  38. diagram_to_iac/services/observability.py +59 -0
  39. diagram_to_iac/services/step_summary.py +77 -0
  40. diagram_to_iac/tools/__init__.py +11 -0
  41. diagram_to_iac/tools/api_utils.py +108 -26
  42. diagram_to_iac/tools/git/__init__.py +45 -0
  43. diagram_to_iac/tools/git/git.py +956 -0
  44. diagram_to_iac/tools/hello/__init__.py +30 -0
  45. diagram_to_iac/tools/hello/cal_utils.py +31 -0
  46. diagram_to_iac/tools/hello/text_utils.py +97 -0
  47. diagram_to_iac/tools/llm_utils/__init__.py +20 -0
  48. diagram_to_iac/tools/llm_utils/anthropic_driver.py +87 -0
  49. diagram_to_iac/tools/llm_utils/base_driver.py +90 -0
  50. diagram_to_iac/tools/llm_utils/gemini_driver.py +89 -0
  51. diagram_to_iac/tools/llm_utils/openai_driver.py +93 -0
  52. diagram_to_iac/tools/llm_utils/router.py +303 -0
  53. diagram_to_iac/tools/sec_utils.py +4 -2
  54. diagram_to_iac/tools/shell/__init__.py +17 -0
  55. diagram_to_iac/tools/shell/shell.py +415 -0
  56. diagram_to_iac/tools/text_utils.py +277 -0
  57. diagram_to_iac/tools/tf/terraform.py +851 -0
  58. diagram_to_iac-0.8.0.dist-info/METADATA +99 -0
  59. diagram_to_iac-0.8.0.dist-info/RECORD +64 -0
  60. {diagram_to_iac-0.7.0.dist-info → diagram_to_iac-0.8.0.dist-info}/WHEEL +1 -1
  61. diagram_to_iac-0.8.0.dist-info/entry_points.txt +4 -0
  62. diagram_to_iac/agents/codegen_agent.py +0 -0
  63. diagram_to_iac/agents/consensus_agent.py +0 -0
  64. diagram_to_iac/agents/deployment_agent.py +0 -0
  65. diagram_to_iac/agents/github_agent.py +0 -0
  66. diagram_to_iac/agents/interpretation_agent.py +0 -0
  67. diagram_to_iac/agents/question_agent.py +0 -0
  68. diagram_to_iac/agents/supervisor.py +0 -0
  69. diagram_to_iac/agents/vision_agent.py +0 -0
  70. diagram_to_iac/core/config.py +0 -0
  71. diagram_to_iac/tools/cv_utils.py +0 -0
  72. diagram_to_iac/tools/gh_utils.py +0 -0
  73. diagram_to_iac/tools/tf_utils.py +0 -0
  74. diagram_to_iac-0.7.0.dist-info/METADATA +0 -16
  75. diagram_to_iac-0.7.0.dist-info/RECORD +0 -32
  76. diagram_to_iac-0.7.0.dist-info/entry_points.txt +0 -2
  77. {diagram_to_iac-0.7.0.dist-info → diagram_to_iac-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1216 @@
1
+ """
2
+ Terraform Agent - Phase 3 Implementation (Error Handling & GitHub Issues Integration)
3
+
4
+ This is the complete TerraformAgent with full LLM-based routing, tool integration, and
5
+ GitHub issue creation for error handling, following the modular LangGraph pattern
6
+ established by the GitAgent. This agent handles:
7
+ - terraform init operations via tf_init_node
8
+ - terraform plan operations via tf_plan_node
9
+ - terraform apply operations via tf_apply_node
10
+ - GitHub issue creation for critical errors via open_issue_node
11
+ - Error handling and automatic issue reporting throughout the workflow
12
+
13
+ Architecture (following GitAgent pattern):
14
+ 1. Planner LLM analyzes user input and emits routing tokens:
15
+ - "ROUTE_TO_TF_INIT" for terraform init operations
16
+ - "ROUTE_TO_TF_PLAN" for terraform plan operations
17
+ - "ROUTE_TO_TF_APPLY" for terraform apply operations
18
+ - "ROUTE_TO_OPEN_ISSUE" for GitHub issue creation
19
+ - "ROUTE_TO_END" when no action needed
20
+ 2. Router function maps tokens to appropriate tool nodes
21
+ 3. Tool nodes execute real Terraform operations using TerraformExecutor
22
+ 4. State machine handles error paths and automatically creates GitHub issues for critical errors
23
+ 5. Enhanced error detection identifies critical failures that warrant issue creation
24
+
25
+ Phase 3: Complete implementation with GitHub issue integration for comprehensive error handling
26
+ """
27
+
28
+ import os
29
+ import uuid
30
+ import logging
31
+ import re
32
+ from typing import TypedDict, Annotated, Optional, List, Dict, Any
33
+
34
+ import yaml
35
+ from langchain_core.messages import HumanMessage, BaseMessage
36
+ from langgraph.graph import StateGraph, END
37
+ from langgraph.checkpoint.memory import MemorySaver
38
+ from pydantic import BaseModel, Field
39
+
40
+ # Import core infrastructure (following GitAgent pattern)
41
+ from diagram_to_iac.tools.llm_utils.router import get_llm, LLMRouter
42
+ from diagram_to_iac.core.agent_base import AgentBase
43
+ from diagram_to_iac.core.memory import create_memory, LangGraphMemoryAdapter
44
+ from diagram_to_iac.services.observability import log_event
45
+ from .parser import classify_terraform_error
46
+
47
+
48
+ # --- Pydantic Schemas for Agent I/O ---
49
+ class TerraformAgentInput(BaseModel):
50
+ """Input schema for TerraformAgent operations."""
51
+
52
+ query: str = Field(
53
+ ..., description="The Terraform DevOps request (init, plan, apply)"
54
+ )
55
+ thread_id: str | None = Field(
56
+ None, description="Optional thread ID for conversation history"
57
+ )
58
+ context: str | None = Field(
59
+ None, description="Optional context information for error reporting"
60
+ )
61
+
62
+
63
+ class TerraformAgentOutput(BaseModel):
64
+ """Output schema for TerraformAgent operations."""
65
+
66
+ result: str = Field(..., description="The result of the Terraform operation")
67
+ thread_id: str = Field(..., description="Thread ID used for the conversation")
68
+ error_message: Optional[str] = Field(
69
+ None, description="Error message if the operation failed"
70
+ )
71
+ operation_type: Optional[str] = Field(
72
+ None, description="Type of operation performed (init, plan, apply)"
73
+ )
74
+ error_tags: Optional[list[str]] = Field(
75
+ None,
76
+ description="Classification tags describing the error, if any",
77
+ )
78
+
79
+
80
+ # --- Agent State Definition ---
81
+ class TerraformAgentState(TypedDict):
82
+ """State definition for the TerraformAgent LangGraph."""
83
+
84
+ input_message: HumanMessage
85
+ tool_output: Annotated[list[BaseMessage], lambda x, y: x + y]
86
+ final_result: str
87
+ error_message: Optional[str]
88
+ operation_type: Optional[str]
89
+ error_tags: Optional[list[str]]
90
+
91
+
92
+ # --- Main Agent Class ---
93
+ class TerraformAgent(AgentBase):
94
+ """
95
+ TerraformAgent is a LangGraph-based DevOps automation agent that handles:
96
+ - Terraform init operations via tf_init_node
97
+ - Terraform plan operations via tf_plan_node
98
+ - Terraform apply operations via tf_apply_node
99
+ - GitHub issue creation for critical errors via open_issue_node
100
+
101
+ Uses a light LLM planner for routing decisions and delegates to specialized tool nodes.
102
+ Follows the hybrid-agent architecture pattern established by GitAgent.
103
+
104
+ Phase 3: Complete implementation with LLM routing, comprehensive tool integration,
105
+ and GitHub issue creation for automatic error reporting.
106
+ """
107
+
108
+ def __init__(self, config_path: str = None, memory_type: str = "persistent"):
109
+ """
110
+ Initialize the TerraformAgent with configuration and tools.
111
+
112
+ Args:
113
+ config_path: Optional path to YAML configuration file
114
+ memory_type: Type of memory ("persistent", "memory", or "langgraph")
115
+ """
116
+ # Configure logger for this agent instance
117
+ self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
118
+ if not logging.getLogger().hasHandlers():
119
+ logging.basicConfig(
120
+ level=logging.INFO,
121
+ format="%(asctime)s - %(name)s - %(levelname)s - %(threadName)s - %(message)s",
122
+ datefmt="%Y-%m-%d %H:%M:%S",
123
+ )
124
+
125
+ # Store memory type for tool initialization
126
+ self.memory_type = memory_type
127
+
128
+ # Load configuration
129
+ if config_path is None:
130
+ base_dir = os.path.dirname(os.path.abspath(__file__))
131
+ config_path = os.path.join(base_dir, "config.yaml")
132
+ self.logger.debug(f"Default config path set to: {config_path}")
133
+
134
+ try:
135
+ with open(config_path, "r") as f:
136
+ self.config = yaml.safe_load(f)
137
+ if self.config is None:
138
+ self.logger.warning(
139
+ f"Configuration file at {config_path} is empty. Using defaults."
140
+ )
141
+ self._set_default_config()
142
+ else:
143
+ self.logger.info(
144
+ f"Configuration loaded successfully from {config_path}"
145
+ )
146
+ if "terraform_token_env" not in self.config:
147
+ self.config["terraform_token_env"] = "TFE_TOKEN"
148
+ except FileNotFoundError:
149
+ self.logger.warning(
150
+ f"Configuration file not found at {config_path}. Using defaults."
151
+ )
152
+ self._set_default_config()
153
+ except yaml.YAMLError as e:
154
+ self.logger.error(
155
+ f"Error parsing YAML configuration: {e}. Using defaults.", exc_info=True
156
+ )
157
+ self._set_default_config()
158
+
159
+ # Initialize enhanced LLM router
160
+ self.llm_router = LLMRouter()
161
+ self.logger.info("Enhanced LLM router initialized")
162
+
163
+ # Initialize enhanced memory system
164
+ self.memory = create_memory(memory_type)
165
+ self.logger.info(
166
+ f"Enhanced memory system initialized: {type(self.memory).__name__}"
167
+ )
168
+
169
+ # Initialize checkpointer
170
+ self.checkpointer = MemorySaver()
171
+ self.logger.info("MemorySaver checkpointer initialized")
172
+
173
+ # Initialize tools (Phase 1 will implement actual Terraform tools)
174
+ self._initialize_tools()
175
+
176
+ # Build and compile the LangGraph
177
+ self.runnable = self._build_graph()
178
+ self.logger.info("TerraformAgent initialized successfully")
179
+
180
+ def _set_default_config(self):
181
+ """Set default configuration values."""
182
+ self.logger.info("Setting default configuration for TerraformAgent")
183
+ self.config = {
184
+ "llm": {"model_name": "gpt-4o-mini", "temperature": 0.1},
185
+ "routing_keys": {
186
+ "terraform_init": "ROUTE_TO_TF_INIT",
187
+ "terraform_plan": "ROUTE_TO_TF_PLAN",
188
+ "terraform_apply": "ROUTE_TO_TF_APPLY",
189
+ "open_issue": "ROUTE_TO_OPEN_ISSUE",
190
+ "end": "ROUTE_TO_END",
191
+ },
192
+ "terraform_token_env": "TFE_TOKEN",
193
+ }
194
+
195
+ def _initialize_tools(self):
196
+ """Initialize the Terraform tools following the established pattern."""
197
+ try:
198
+ # Import Terraform tools from our comprehensive implementation
199
+ from diagram_to_iac.tools.tf.terraform import (
200
+ TerraformExecutor,
201
+ terraform_init,
202
+ terraform_plan,
203
+ terraform_apply,
204
+ )
205
+ from diagram_to_iac.tools.shell import ShellExecutor, shell_exec
206
+
207
+ # Import GitHub tools from GitAgent for Phase 3 error handling
208
+ from diagram_to_iac.tools.git import GitExecutor, gh_open_issue
209
+
210
+ # Initialize executors
211
+ self.terraform_executor = TerraformExecutor(memory_type=self.memory_type)
212
+ self.shell_executor = ShellExecutor(memory_type=self.memory_type)
213
+ self.git_executor = GitExecutor(memory_type=self.memory_type)
214
+
215
+ # Register tools for easy access
216
+ self.tools = {
217
+ "terraform_init": terraform_init,
218
+ "terraform_plan": terraform_plan,
219
+ "terraform_apply": terraform_apply,
220
+ "shell_exec": shell_exec,
221
+ "gh_open_issue": gh_open_issue,
222
+ }
223
+
224
+ self.logger.info(f"Terraform tools initialized: {list(self.tools.keys())}")
225
+
226
+ except Exception as e:
227
+ self.logger.error(
228
+ f"Failed to initialize Terraform tools: {e}", exc_info=True
229
+ )
230
+ raise
231
+
232
+ def _planner_llm_node(self, state: TerraformAgentState):
233
+ """
234
+ LLM planner node that analyzes input and decides routing.
235
+ Emits routing tokens based on the user's Terraform request.
236
+
237
+ Phase 2: Full LLM-based routing implementation following GitAgent pattern.
238
+ """
239
+ # Get LLM configuration
240
+ llm_config = self.config.get("llm", {})
241
+ model_name = llm_config.get("model_name")
242
+ temperature = llm_config.get("temperature")
243
+
244
+ # Use enhanced LLM router following GitAgent pattern
245
+ try:
246
+ if model_name is not None or temperature is not None:
247
+ actual_model_name = (
248
+ model_name if model_name is not None else "gpt-4o-mini"
249
+ )
250
+ actual_temperature = temperature if temperature is not None else 0.1
251
+ self.logger.debug(
252
+ f"Terraform planner using LLM: {actual_model_name}, Temp: {actual_temperature}"
253
+ )
254
+
255
+ llm = self.llm_router.get_llm(
256
+ model_name=actual_model_name,
257
+ temperature=actual_temperature,
258
+ agent_name="terraform_agent",
259
+ )
260
+ else:
261
+ self.logger.debug(
262
+ "Terraform planner using agent-specific LLM configuration"
263
+ )
264
+ llm = self.llm_router.get_llm_for_agent("terraform_agent")
265
+ except Exception as e:
266
+ self.logger.error(
267
+ f"Failed to get LLM from router: {e}. Falling back to basic get_llm."
268
+ )
269
+ llm = get_llm(model_name=model_name, temperature=temperature)
270
+
271
+ # Store conversation in memory
272
+ query_content = state["input_message"].content
273
+ self.memory.add_to_conversation(
274
+ "user", query_content, {"agent": "terraform_agent", "node": "planner"}
275
+ )
276
+
277
+ try:
278
+ self.logger.debug(f"Terraform planner LLM input: {query_content}")
279
+
280
+ # Build the Terraform-specific analysis prompt
281
+ analysis_prompt_template = self.config.get("prompts", {}).get(
282
+ "planner_prompt",
283
+ """
284
+ User input: "{user_input}"
285
+
286
+ Analyze this Terraform/Infrastructure request and determine the appropriate action:
287
+ 1. If requesting to initialize Terraform (keywords: 'init', 'initialize', 'setup terraform'), respond with "{route_tf_init}"
288
+ 2. If requesting to create/preview Terraform plan (keywords: 'plan', 'preview', 'show changes'), respond with "{route_tf_plan}"
289
+ 3. If requesting to apply/deploy Terraform (keywords: 'apply', 'deploy', 'provision', 'create infrastructure'), respond with "{route_tf_apply}"
290
+ 4. If requesting to create a GitHub issue for errors (keywords: 'open issue', 'create issue', 'report error', 'file bug'), respond with "{route_open_issue}"
291
+ 5. If the request is complete or no action needed, respond with "{route_end}"
292
+
293
+ Important: Only use routing tokens if the input contains actionable Terraform infrastructure requests or error reporting requests.
294
+ """,
295
+ )
296
+
297
+ routing_keys = self.config.get(
298
+ "routing_keys",
299
+ {
300
+ "terraform_init": "ROUTE_TO_TF_INIT",
301
+ "terraform_plan": "ROUTE_TO_TF_PLAN",
302
+ "terraform_apply": "ROUTE_TO_TF_APPLY",
303
+ "open_issue": "ROUTE_TO_OPEN_ISSUE",
304
+ "end": "ROUTE_TO_END",
305
+ },
306
+ )
307
+
308
+ analysis_prompt = analysis_prompt_template.format(
309
+ user_input=query_content,
310
+ route_tf_init=routing_keys["terraform_init"],
311
+ route_tf_plan=routing_keys["terraform_plan"],
312
+ route_tf_apply=routing_keys["terraform_apply"],
313
+ route_open_issue=routing_keys["open_issue"],
314
+ route_end=routing_keys["end"],
315
+ )
316
+
317
+ self.logger.debug(f"Terraform planner LLM prompt: {analysis_prompt}")
318
+
319
+ response = llm.invoke([HumanMessage(content=analysis_prompt)])
320
+ self.logger.debug(f"Terraform planner LLM response: {response.content}")
321
+ response_content = response.content.strip()
322
+
323
+ # Store LLM response in memory
324
+ self.memory.add_to_conversation(
325
+ "assistant",
326
+ response_content,
327
+ {"agent": "terraform_agent", "node": "planner", "model": model_name},
328
+ )
329
+
330
+ # Determine routing based on response content
331
+ new_state_update = {}
332
+ if routing_keys["terraform_init"] in response_content:
333
+ new_state_update = {
334
+ "final_result": "route_to_tf_init",
335
+ "operation_type": "terraform_init",
336
+ "error_message": None,
337
+ }
338
+ elif routing_keys["terraform_plan"] in response_content:
339
+ new_state_update = {
340
+ "final_result": "route_to_tf_plan",
341
+ "operation_type": "terraform_plan",
342
+ "error_message": None,
343
+ }
344
+ elif routing_keys["terraform_apply"] in response_content:
345
+ new_state_update = {
346
+ "final_result": "route_to_tf_apply",
347
+ "operation_type": "terraform_apply",
348
+ "error_message": None,
349
+ }
350
+ elif routing_keys["open_issue"] in response_content:
351
+ new_state_update = {
352
+ "final_result": "route_to_open_issue",
353
+ "operation_type": "open_issue",
354
+ "error_message": None,
355
+ }
356
+ elif routing_keys["end"] in response_content:
357
+ # Direct answer or route to end
358
+ new_state_update = {
359
+ "final_result": response.content,
360
+ "operation_type": "direct_answer",
361
+ "error_message": None,
362
+ }
363
+ else:
364
+ # Default if no specific route token is found
365
+ new_state_update = {
366
+ "final_result": response.content, # Treat as direct answer
367
+ "operation_type": "direct_answer",
368
+ "error_message": None,
369
+ }
370
+
371
+ self.logger.info(
372
+ f"Terraform planner decision: {new_state_update.get('final_result', 'N/A')}"
373
+ )
374
+ return new_state_update
375
+
376
+ except Exception as e:
377
+ self.logger.error(f"LLM error in terraform planner: {e}", exc_info=True)
378
+ self.memory.add_to_conversation(
379
+ "system",
380
+ f"Error in planner: {str(e)}",
381
+ {"agent": "terraform_agent", "node": "planner", "error": True},
382
+ )
383
+ return {
384
+ "final_result": "Sorry, I encountered an issue processing your Terraform request.",
385
+ "error_message": str(e),
386
+ "operation_type": "error",
387
+ }
388
+
389
+ def _should_create_github_issue(self, error_result: str) -> bool:
390
+ """
391
+ Determine if an error should trigger GitHub issue creation.
392
+
393
+ Phase 3: Error detection logic for automatic issue creation.
394
+ """
395
+ critical_error_patterns = [
396
+ "authentication failed",
397
+ "permission denied",
398
+ "terraform crash",
399
+ "backend configuration",
400
+ "provider configuration error",
401
+ "module not found",
402
+ "workspace not found",
403
+ "invalid credentials",
404
+ "terraform cloud error",
405
+ "backend initialization failed",
406
+ # Additional patterns for test compatibility
407
+ "failed to authenticate with terraform cloud",
408
+ "backend configuration failed",
409
+ "please report this bug",
410
+ "invalid credentials for aws provider",
411
+ "resource already exists",
412
+ "panic: runtime error",
413
+ ]
414
+
415
+ error_lower = error_result.lower()
416
+ for pattern in critical_error_patterns:
417
+ if pattern in error_lower:
418
+ self.logger.info(f"Critical error detected: {pattern}")
419
+ return True
420
+
421
+ return False
422
+
423
+ def _check_auth_token(self) -> tuple[bool, str]:
424
+ """Check that required Terraform auth token environment variable exists."""
425
+ env_name = self.config.get("terraform_token_env", "TFE_TOKEN")
426
+ token = os.environ.get(env_name)
427
+ if not token:
428
+ error_msg = f"ERROR_MISSING_TERRAFORM_TOKEN: environment variable '{env_name}' not set"
429
+ self.logger.error(error_msg)
430
+ return False, env_name
431
+ return True, env_name
432
+
433
+ def _route_after_planner(self, state: TerraformAgentState):
434
+ """
435
+ Router function that determines the next node based on planner output.
436
+ Maps routing tokens to appropriate tool nodes or END.
437
+
438
+ Phase 3: Enhanced routing with GitHub issue integration for error handling.
439
+ """
440
+ self.logger.debug(
441
+ f"Terraform routing after planner. State: {state.get('final_result')}, error: {state.get('error_message')}"
442
+ )
443
+
444
+ if state.get("error_message"):
445
+ self.logger.warning(
446
+ f"Error detected in terraform planner, routing to END: {state['error_message']}"
447
+ )
448
+ return END
449
+
450
+ final_result = state.get("final_result", "")
451
+
452
+ # Route based on planner decision
453
+ if final_result == "route_to_tf_init":
454
+ return "tf_init_node"
455
+ elif final_result == "route_to_tf_plan":
456
+ return "tf_plan_node"
457
+ elif final_result == "route_to_tf_apply":
458
+ return "tf_apply_node"
459
+ elif final_result == "route_to_open_issue":
460
+ return "open_issue_node"
461
+ else:
462
+ return END
463
+
464
+ def _tf_init_node(self, state: TerraformAgentState):
465
+ """
466
+ Terraform init tool node that handles terraform initialization operations.
467
+
468
+ Phase 2: Full implementation using TerraformExecutor.
469
+ """
470
+ self.logger.info(
471
+ f"Terraform init node invoked for: {state['input_message'].content}"
472
+ )
473
+
474
+ try:
475
+ text_content = state["input_message"].content
476
+
477
+ # Store tool invocation in memory
478
+ self.memory.add_to_conversation(
479
+ "system",
480
+ f"Terraform init tool invoked",
481
+ {"agent": "terraform_agent", "node": "tf_init", "input": text_content},
482
+ )
483
+
484
+ # Extract repository path from the input or use default workspace
485
+ # In a real implementation, you might use more sophisticated parsing
486
+ import re
487
+
488
+ path_pattern = r"(?:in|from|at)\s+([^\s]+)"
489
+ paths = re.findall(path_pattern, text_content)
490
+
491
+ # Default to current workspace if no path specified
492
+ repo_path = paths[0] if paths else "/workspace"
493
+
494
+ # Token check is required before proceeding with Terraform operations
495
+ token_valid, token_env = self._check_auth_token()
496
+ if not token_valid:
497
+ error_msg = f"❌ Terraform Cloud token required: Please set the {token_env} environment variable to proceed with Terraform operations."
498
+ self.logger.error(error_msg)
499
+ self.memory.add_to_conversation(
500
+ "assistant",
501
+ error_msg,
502
+ {"agent": "terraform_agent", "node": "tf_init", "result": "error", "requires_user_action": True},
503
+ )
504
+ return {
505
+ "final_result": error_msg,
506
+ "operation_type": "terraform_init",
507
+ "error_message": error_msg,
508
+ "error_tags": ["missing_token"],
509
+ }
510
+
511
+ # Use the terraform_init tool
512
+ result = self.tools["terraform_init"].invoke(
513
+ {"repo_path": repo_path, "upgrade": "upgrade" in text_content.lower()}
514
+ )
515
+
516
+ if "✅" in result:
517
+ success_msg = f"Terraform init completed successfully: {result}"
518
+ self.logger.info(success_msg)
519
+ self.memory.add_to_conversation(
520
+ "assistant",
521
+ success_msg,
522
+ {
523
+ "agent": "terraform_agent",
524
+ "node": "tf_init",
525
+ "result": "success",
526
+ },
527
+ )
528
+ return {
529
+ "final_result": result,
530
+ "operation_type": "terraform_init",
531
+ "error_message": None,
532
+ "error_tags": None,
533
+ }
534
+ else:
535
+ error_msg = f"Terraform init failed: {result}"
536
+ self.logger.error(error_msg)
537
+ self.memory.add_to_conversation(
538
+ "assistant",
539
+ error_msg,
540
+ {"agent": "terraform_agent", "node": "tf_init", "result": "error"},
541
+ )
542
+
543
+ # Phase 3: Check if this error should trigger GitHub issue creation
544
+ if self._should_create_github_issue(result):
545
+ # Store error context for potential issue creation
546
+ error_context = {
547
+ "operation": "terraform_init",
548
+ "error": result,
549
+ "repo_path": repo_path,
550
+ "command": "terraform init",
551
+ }
552
+ self.memory.add_to_conversation(
553
+ "system",
554
+ f"Critical Terraform init error detected, consider creating GitHub issue",
555
+ error_context,
556
+ )
557
+
558
+ return {
559
+ "final_result": result,
560
+ "operation_type": "terraform_init",
561
+ "error_message": result,
562
+ "error_tags": list(classify_terraform_error(result)),
563
+ }
564
+
565
+ except Exception as e:
566
+ error_msg = f"Terraform init node error: {str(e)}"
567
+ self.logger.error(error_msg, exc_info=True)
568
+
569
+ # Phase 3: Check if this exception should trigger GitHub issue creation
570
+ if self._should_create_github_issue(str(e)):
571
+ error_context = {
572
+ "operation": "terraform_init",
573
+ "error": str(e),
574
+ "exception": True,
575
+ "command": "terraform init",
576
+ }
577
+ self.memory.add_to_conversation(
578
+ "system",
579
+ f"Critical Terraform init exception detected, consider creating GitHub issue",
580
+ error_context,
581
+ )
582
+
583
+ self.memory.add_to_conversation(
584
+ "system",
585
+ error_msg,
586
+ {"agent": "terraform_agent", "node": "tf_init", "error": True},
587
+ )
588
+ return {
589
+ "final_result": "Sorry, I encountered an issue running terraform init.",
590
+ "error_message": str(e),
591
+ "operation_type": "terraform_init",
592
+ "error_tags": list(classify_terraform_error(str(e))),
593
+ }
594
+
595
+ def _tf_plan_node(self, state: TerraformAgentState):
596
+ """
597
+ Terraform plan tool node that handles terraform plan operations.
598
+
599
+ Phase 2: Full implementation using TerraformExecutor.
600
+ """
601
+ self.logger.info(
602
+ f"Terraform plan node invoked for: {state['input_message'].content}"
603
+ )
604
+
605
+ try:
606
+ text_content = state["input_message"].content
607
+
608
+ # Store tool invocation in memory
609
+ self.memory.add_to_conversation(
610
+ "system",
611
+ f"Terraform plan tool invoked",
612
+ {"agent": "terraform_agent", "node": "tf_plan", "input": text_content},
613
+ )
614
+
615
+ # Extract repository path from the input or use default workspace
616
+ import re
617
+
618
+ path_pattern = r"(?:in|from|at)\s+([^\s]+)"
619
+ paths = re.findall(path_pattern, text_content)
620
+
621
+ # Default to current workspace if no path specified
622
+ repo_path = paths[0] if paths else "/workspace"
623
+
624
+ # Check for destroy flag
625
+ destroy = "destroy" in text_content.lower()
626
+
627
+ # Token check is deferred to the Terraform execution itself.
628
+ # _check_auth_token() still logs an error if called, but does not block.
629
+ self._check_auth_token()
630
+
631
+ # Use the terraform_plan tool
632
+ result = self.tools["terraform_plan"].invoke(
633
+ {"repo_path": repo_path, "destroy": destroy}
634
+ )
635
+
636
+ if "✅" in result:
637
+ success_msg = f"Terraform plan completed successfully: {result}"
638
+ self.logger.info(success_msg)
639
+ self.memory.add_to_conversation(
640
+ "assistant",
641
+ success_msg,
642
+ {
643
+ "agent": "terraform_agent",
644
+ "node": "tf_plan",
645
+ "result": "success",
646
+ },
647
+ )
648
+ return {
649
+ "final_result": result,
650
+ "operation_type": "terraform_plan",
651
+ "error_message": None,
652
+ "error_tags": None,
653
+ }
654
+ else:
655
+ error_msg = f"Terraform plan failed: {result}"
656
+ self.logger.error(error_msg)
657
+ self.memory.add_to_conversation(
658
+ "assistant",
659
+ error_msg,
660
+ {"agent": "terraform_agent", "node": "tf_plan", "result": "error"},
661
+ )
662
+
663
+ # Phase 3: Check if this error should trigger GitHub issue creation
664
+ if self._should_create_github_issue(result):
665
+ error_context = {
666
+ "operation": "terraform_plan",
667
+ "error": result,
668
+ "repo_path": repo_path,
669
+ "command": "terraform plan",
670
+ }
671
+ self.memory.add_to_conversation(
672
+ "system",
673
+ f"Critical Terraform plan error detected, consider creating GitHub issue",
674
+ error_context,
675
+ )
676
+
677
+ return {
678
+ "final_result": result,
679
+ "operation_type": "terraform_plan",
680
+ "error_message": result,
681
+ "error_tags": list(classify_terraform_error(result)),
682
+ }
683
+
684
+ except Exception as e:
685
+ error_msg = f"Terraform plan node error: {str(e)}"
686
+ self.logger.error(error_msg, exc_info=True)
687
+
688
+ # Phase 3: Check if this exception should trigger GitHub issue creation
689
+ if self._should_create_github_issue(str(e)):
690
+ error_context = {
691
+ "operation": "terraform_plan",
692
+ "error": str(e),
693
+ "exception": True,
694
+ "command": "terraform plan",
695
+ }
696
+ self.memory.add_to_conversation(
697
+ "system",
698
+ f"Critical Terraform plan exception detected, consider creating GitHub issue",
699
+ error_context,
700
+ )
701
+
702
+ self.memory.add_to_conversation(
703
+ "system",
704
+ error_msg,
705
+ {"agent": "terraform_agent", "node": "tf_plan", "error": True},
706
+ )
707
+ return {
708
+ "final_result": "Sorry, I encountered an issue running terraform plan.",
709
+ "error_message": str(e),
710
+ "operation_type": "terraform_plan",
711
+ "error_tags": list(classify_terraform_error(str(e))),
712
+ }
713
+
714
+ def _tf_apply_node(self, state: TerraformAgentState):
715
+ """
716
+ Terraform apply tool node that handles terraform apply operations.
717
+
718
+ Phase 2: Full implementation using TerraformExecutor.
719
+ """
720
+ self.logger.info(
721
+ f"Terraform apply node invoked for: {state['input_message'].content}"
722
+ )
723
+
724
+ try:
725
+ text_content = state["input_message"].content
726
+
727
+ # Store tool invocation in memory
728
+ self.memory.add_to_conversation(
729
+ "system",
730
+ f"Terraform apply tool invoked",
731
+ {"agent": "terraform_agent", "node": "tf_apply", "input": text_content},
732
+ )
733
+
734
+ # Extract repository path from the input or use default workspace
735
+ import re
736
+
737
+ path_pattern = r"(?:in|from|at)\s+([^\s]+)"
738
+ paths = re.findall(path_pattern, text_content)
739
+
740
+ # Default to current workspace if no path specified
741
+ repo_path = paths[0] if paths else "/workspace"
742
+
743
+ # Check for auto-approve flag (default to True for automation)
744
+ auto_approve = "no-auto-approve" not in text_content.lower()
745
+
746
+ # Token check is deferred to the Terraform execution itself.
747
+ # _check_auth_token() still logs an error if called, but does not block.
748
+ self._check_auth_token()
749
+
750
+ # Use the terraform_apply tool
751
+ result = self.tools["terraform_apply"].invoke(
752
+ {"repo_path": repo_path, "auto_approve": auto_approve}
753
+ )
754
+
755
+ if "✅" in result:
756
+ success_msg = f"Terraform apply completed successfully: {result}"
757
+ self.logger.info(success_msg)
758
+ self.memory.add_to_conversation(
759
+ "assistant",
760
+ success_msg,
761
+ {
762
+ "agent": "terraform_agent",
763
+ "node": "tf_apply",
764
+ "result": "success",
765
+ },
766
+ )
767
+ return {
768
+ "final_result": result,
769
+ "operation_type": "terraform_apply",
770
+ "error_message": None,
771
+ "error_tags": None,
772
+ }
773
+ else:
774
+ error_msg = f"Terraform apply failed: {result}"
775
+ self.logger.error(error_msg)
776
+ self.memory.add_to_conversation(
777
+ "assistant",
778
+ error_msg,
779
+ {"agent": "terraform_agent", "node": "tf_apply", "result": "error"},
780
+ )
781
+
782
+ # Phase 3: Check if this error should trigger GitHub issue creation
783
+ if self._should_create_github_issue(result):
784
+ error_context = {
785
+ "operation": "terraform_apply",
786
+ "error": result,
787
+ "repo_path": repo_path,
788
+ "command": "terraform apply",
789
+ }
790
+ self.memory.add_to_conversation(
791
+ "system",
792
+ f"Critical Terraform apply error detected, consider creating GitHub issue",
793
+ error_context,
794
+ )
795
+
796
+ return {
797
+ "final_result": result,
798
+ "operation_type": "terraform_apply",
799
+ "error_message": result,
800
+ "error_tags": list(classify_terraform_error(result)),
801
+ }
802
+
803
+ except Exception as e:
804
+ error_msg = f"Terraform apply node error: {str(e)}"
805
+ self.logger.error(error_msg, exc_info=True)
806
+
807
+ # Phase 3: Check if this exception should trigger GitHub issue creation
808
+ if self._should_create_github_issue(str(e)):
809
+ error_context = {
810
+ "operation": "terraform_apply",
811
+ "error": str(e),
812
+ "exception": True,
813
+ "command": "terraform apply",
814
+ }
815
+ self.memory.add_to_conversation(
816
+ "system",
817
+ f"Critical Terraform apply exception detected, consider creating GitHub issue",
818
+ error_context,
819
+ )
820
+
821
+ self.memory.add_to_conversation(
822
+ "system",
823
+ error_msg,
824
+ {"agent": "terraform_agent", "node": "tf_apply", "error": True},
825
+ )
826
+ return {
827
+ "final_result": "Sorry, I encountered an issue running terraform apply.",
828
+ "error_message": str(e),
829
+ "operation_type": "terraform_apply",
830
+ "error_tags": list(classify_terraform_error(str(e))),
831
+ }
832
+
833
+ def _open_issue_node(self, state: TerraformAgentState):
834
+ """
835
+ GitHub issue tool node that handles issue creation for Terraform errors.
836
+
837
+ Phase 3: Error handling integration following GitAgent pattern.
838
+ """
839
+ self.logger.info(
840
+ f"Open issue node invoked for: {state['input_message'].content}"
841
+ )
842
+
843
+ try:
844
+ text_content = state["input_message"].content
845
+
846
+ # Store tool invocation in memory
847
+ self.memory.add_to_conversation(
848
+ "system",
849
+ f"GitHub issue tool invoked",
850
+ {
851
+ "agent": "terraform_agent",
852
+ "node": "open_issue",
853
+ "input": text_content,
854
+ },
855
+ )
856
+
857
+ # Extract repository and issue details from input or use defaults for Terraform errors
858
+ import re
859
+
860
+ repo_pattern = r"(?:in|for|on)\s+(\w+/\w+)"
861
+ title_pattern = r"(?:issue|bug|problem|error):\s*(.+?)(?:\.|$)"
862
+
863
+ repo_match = re.search(repo_pattern, text_content, re.IGNORECASE)
864
+ title_match = re.search(title_pattern, text_content, re.IGNORECASE)
865
+
866
+ # Use defaults for Terraform-specific errors if not specified
867
+ if not repo_match:
868
+ # Default to a placeholder repo for terraform issues
869
+ repo = "terraform/infrastructure"
870
+ self.logger.info(
871
+ "No repository specified, using default: terraform/infrastructure"
872
+ )
873
+ else:
874
+ repo = repo_match.group(1)
875
+
876
+ if not title_match:
877
+ # Create a default title from the content
878
+ title = f"Terraform Error: {text_content[:50]}..."
879
+ self.logger.info(f"No explicit title found, using default: {title}")
880
+ else:
881
+ title = title_match.group(1)
882
+
883
+ # Build issue body with context from memory if available
884
+ body = f"Terraform Agent Error Report:\n\n{text_content}\n\n"
885
+
886
+ # Include previous error context from memory
887
+ conversation_history = self.memory.get_conversation_history()
888
+ error_context = []
889
+ for entry in conversation_history[-10:]: # Look at last 10 entries
890
+ if (
891
+ entry.get("metadata", {}).get("error")
892
+ or "error" in entry.get("content", "").lower()
893
+ or "failed" in entry.get("content", "").lower()
894
+ ):
895
+ error_context.append(entry.get("content", ""))
896
+
897
+ if error_context:
898
+ body += "Previous Error Context:\n"
899
+ for context in error_context[-3:]: # Include last 3 error contexts
900
+ body += f"- {context}\n"
901
+ body += "\n"
902
+
903
+ body += "Generated automatically by TerraformAgent."
904
+
905
+ # Use the gh_open_issue tool with correct parameter name
906
+ result = self.tools["gh_open_issue"].invoke(
907
+ {
908
+ "repo": repo, # Fixed: use 'repo' instead of 'repo_url'
909
+ "title": title,
910
+ "body": body,
911
+ }
912
+ )
913
+
914
+ # Store successful result in memory
915
+ self.memory.add_to_conversation(
916
+ "system",
917
+ f"GitHub issue created: {result}",
918
+ {
919
+ "agent": "terraform_agent",
920
+ "node": "open_issue",
921
+ "repo": repo,
922
+ "title": title,
923
+ "result": result,
924
+ },
925
+ )
926
+
927
+ if "ERROR:" in result:
928
+ error_msg = f"Failed to create GitHub issue: {result}"
929
+ self.logger.error(error_msg)
930
+ return {
931
+ "final_result": error_msg,
932
+ "error_message": result,
933
+ "operation_type": "open_issue",
934
+ "error_tags": None,
935
+ }
936
+ else:
937
+ success_msg = (
938
+ f"Successfully created GitHub issue for Terraform error: {result}"
939
+ )
940
+ self.logger.info(success_msg)
941
+ return {
942
+ "final_result": success_msg,
943
+ "error_message": None,
944
+ "operation_type": "open_issue",
945
+ "error_tags": None,
946
+ }
947
+
948
+ except Exception as e:
949
+ error_msg = f"Error in open issue node: {str(e)}"
950
+ self.logger.error(error_msg, exc_info=True)
951
+ self.memory.add_to_conversation(
952
+ "system",
953
+ error_msg,
954
+ {"agent": "terraform_agent", "node": "open_issue", "error": True},
955
+ )
956
+ return {
957
+ "final_result": "Sorry, I couldn't create the GitHub issue due to an error.",
958
+ "error_message": str(e),
959
+ "operation_type": "open_issue",
960
+ "error_tags": list(classify_terraform_error(str(e))),
961
+ }
962
+
963
+ def _build_graph(self):
964
+ """
965
+ Build and compile the LangGraph state machine.
966
+
967
+ Phase 3: Enhanced graph with GitHub issue integration for error handling.
968
+ """
969
+ graph_builder = StateGraph(TerraformAgentState)
970
+
971
+ # Add nodes
972
+ graph_builder.add_node("planner_llm", self._planner_llm_node)
973
+ graph_builder.add_node("tf_init_node", self._tf_init_node)
974
+ graph_builder.add_node("tf_plan_node", self._tf_plan_node)
975
+ graph_builder.add_node("tf_apply_node", self._tf_apply_node)
976
+ graph_builder.add_node("open_issue_node", self._open_issue_node)
977
+
978
+ # Set entry point
979
+ graph_builder.set_entry_point("planner_llm")
980
+
981
+ # Configure routing map
982
+ routing_map = self.config.get(
983
+ "routing_map",
984
+ {
985
+ "tf_init_node": "tf_init_node",
986
+ "tf_plan_node": "tf_plan_node",
987
+ "tf_apply_node": "tf_apply_node",
988
+ "open_issue_node": "open_issue_node",
989
+ END: END,
990
+ },
991
+ )
992
+
993
+ # Add conditional edges from planner
994
+ graph_builder.add_conditional_edges(
995
+ "planner_llm", self._route_after_planner, routing_map
996
+ )
997
+
998
+ # Add edges from tool nodes to END
999
+ graph_builder.add_edge("tf_init_node", END)
1000
+ graph_builder.add_edge("tf_plan_node", END)
1001
+ graph_builder.add_edge("tf_apply_node", END)
1002
+ graph_builder.add_edge("open_issue_node", END)
1003
+
1004
+ # Compile with checkpointer
1005
+ return graph_builder.compile(checkpointer=self.checkpointer)
1006
+
1007
+ def run(self, agent_input: TerraformAgentInput) -> TerraformAgentOutput:
1008
+ """
1009
+ Run the agent with the given input.
1010
+
1011
+ Args:
1012
+ agent_input: TerraformAgentInput with query and optional thread_id
1013
+
1014
+ Returns:
1015
+ TerraformAgentOutput with result, thread_id, and optional error
1016
+ """
1017
+ current_thread_id = (
1018
+ agent_input.thread_id if agent_input.thread_id else str(uuid.uuid4())
1019
+ )
1020
+ self.logger.info(
1021
+ f"Run invoked with query: '{agent_input.query}', thread_id: {current_thread_id}"
1022
+ )
1023
+ log_event(
1024
+ "terraform_agent_run_start",
1025
+ query=agent_input.query,
1026
+ thread_id=current_thread_id,
1027
+ )
1028
+
1029
+ # Store context in memory if provided
1030
+ if agent_input.context:
1031
+ self.memory.add_to_conversation(
1032
+ "system",
1033
+ f"Context: {agent_input.context}",
1034
+ {
1035
+ "agent": "terraform_agent",
1036
+ "type": "context",
1037
+ "thread_id": current_thread_id,
1038
+ },
1039
+ )
1040
+
1041
+ # Initial state for LangGraph
1042
+ initial_state = {
1043
+ "input_message": HumanMessage(content=agent_input.query),
1044
+ "tool_output": [],
1045
+ "error_message": None,
1046
+ "operation_type": None,
1047
+ "error_tags": None,
1048
+ }
1049
+
1050
+ langgraph_config = {"configurable": {"thread_id": current_thread_id}}
1051
+
1052
+ try:
1053
+ # Run the graph
1054
+ result_state = self.runnable.invoke(initial_state, config=langgraph_config)
1055
+
1056
+ # Extract results
1057
+ final_result = result_state.get("final_result", "No result found.")
1058
+ error_message = result_state.get("error_message")
1059
+ operation_type = result_state.get("operation_type")
1060
+ error_tags = result_state.get("error_tags")
1061
+
1062
+ if (
1063
+ error_message
1064
+ and not error_tags
1065
+ and operation_type in {
1066
+ "terraform_init",
1067
+ "terraform_plan",
1068
+ "terraform_apply",
1069
+ }
1070
+ ):
1071
+ error_tags = list(classify_terraform_error(error_message))
1072
+
1073
+ if error_message:
1074
+ self.logger.error(f"Run completed with error: {error_message}")
1075
+ else:
1076
+ self.logger.info(f"Run completed successfully: {final_result}")
1077
+
1078
+ log_event(
1079
+ "terraform_agent_run_end",
1080
+ thread_id=current_thread_id,
1081
+ error=error_message,
1082
+ result=final_result,
1083
+ )
1084
+
1085
+ output = TerraformAgentOutput(
1086
+ result=final_result,
1087
+ thread_id=current_thread_id,
1088
+ error_message=error_message,
1089
+ operation_type=operation_type,
1090
+ error_tags=error_tags,
1091
+ )
1092
+
1093
+ return output
1094
+
1095
+ except Exception as e:
1096
+ self.logger.error(f"Error during agent run: {e}", exc_info=True)
1097
+ log_event(
1098
+ "terraform_agent_run_exception",
1099
+ thread_id=current_thread_id,
1100
+ error=str(e),
1101
+ )
1102
+ return TerraformAgentOutput(
1103
+ result="An unexpected error occurred during execution.",
1104
+ thread_id=current_thread_id,
1105
+ error_message=str(e),
1106
+ operation_type="error",
1107
+ error_tags=list(classify_terraform_error(str(e))),
1108
+ )
1109
+
1110
+ def get_conversation_history(self) -> List[Dict[str, Any]]:
1111
+ """Get conversation history from memory."""
1112
+ return self.memory.get_conversation_history()
1113
+
1114
+ def get_memory_state(self) -> Dict[str, Any]:
1115
+ """Get current memory state."""
1116
+ return self.memory.get_state()
1117
+
1118
+ def plan(self, query: str, **kwargs):
1119
+ """
1120
+ Generates a plan for the agent to execute (required by AgentBase).
1121
+
1122
+ Phase 2: Comprehensive Terraform planning logic.
1123
+
1124
+ Args:
1125
+ query: The Terraform query to plan for
1126
+ **kwargs: Additional parameters (e.g., thread_id)
1127
+
1128
+ Returns:
1129
+ dict: A plan containing the input and predicted action
1130
+ """
1131
+ self.logger.info(f"Planning for Terraform query: '{query}'")
1132
+
1133
+ plan = {
1134
+ "input_query": query,
1135
+ "predicted_action": "analyze_terraform_request",
1136
+ "description": "Analyze Terraform request to determine appropriate infrastructure operation",
1137
+ }
1138
+
1139
+ # Comprehensive analysis to predict the route
1140
+ query_lower = query.lower()
1141
+ if any(
1142
+ word in query_lower
1143
+ for word in ["init", "initialize", "setup terraform", "terraform init"]
1144
+ ):
1145
+ plan["predicted_route"] = "terraform_init"
1146
+ plan["description"] = "Initialize Terraform working directory"
1147
+ elif any(
1148
+ word in query_lower
1149
+ for word in ["plan", "preview", "show changes", "terraform plan"]
1150
+ ):
1151
+ plan["predicted_route"] = "terraform_plan"
1152
+ plan["description"] = "Create Terraform execution plan"
1153
+ elif any(
1154
+ word in query_lower
1155
+ for word in [
1156
+ "apply",
1157
+ "deploy",
1158
+ "provision",
1159
+ "create infrastructure",
1160
+ "terraform apply",
1161
+ ]
1162
+ ):
1163
+ plan["predicted_route"] = "terraform_apply"
1164
+ plan["description"] = (
1165
+ "Apply Terraform configuration and provision infrastructure"
1166
+ )
1167
+ elif any(
1168
+ phrase in query_lower
1169
+ for phrase in [
1170
+ "open issue",
1171
+ "create issue",
1172
+ "report error",
1173
+ "file bug",
1174
+ "open github issue",
1175
+ ]
1176
+ ):
1177
+ plan["predicted_route"] = "open_issue"
1178
+ plan["description"] = "Create GitHub issue for error reporting"
1179
+ else:
1180
+ plan["predicted_route"] = "direct_answer"
1181
+ plan["description"] = "Provide direct answer without Terraform operation"
1182
+
1183
+ self.logger.debug(f"Generated Terraform plan: {plan}")
1184
+ return plan
1185
+
1186
+ def report(self, result=None, **kwargs):
1187
+ """
1188
+ Reports the results or progress of the agent's execution (required by AgentBase).
1189
+
1190
+ Args:
1191
+ result: The result to report (TerraformAgentOutput or string)
1192
+ **kwargs: Additional parameters
1193
+
1194
+ Returns:
1195
+ dict: A report containing execution details
1196
+ """
1197
+ if isinstance(result, TerraformAgentOutput):
1198
+ report = {
1199
+ "status": "completed",
1200
+ "result": result.result,
1201
+ "thread_id": result.thread_id,
1202
+ "error": result.error_message,
1203
+ "operation_type": result.operation_type,
1204
+ "success": result.error_message is None,
1205
+ }
1206
+ elif isinstance(result, str):
1207
+ report = {"status": "completed", "result": result, "success": True}
1208
+ else:
1209
+ report = {"status": "no_result", "message": "No result provided to report"}
1210
+
1211
+ self.logger.info(f"TerraformAgent execution report: {report}")
1212
+ return report
1213
+
1214
+
1215
+ # Alias for backward compatibility
1216
+ TerraformAgent = TerraformAgent