diagram-to-iac 0.6.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.6.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.6.0.dist-info/METADATA +0 -16
- diagram_to_iac-0.6.0.dist-info/RECORD +0 -32
- diagram_to_iac-0.6.0.dist-info/entry_points.txt +0 -2
- {diagram_to_iac-0.6.0.dist-info → diagram_to_iac-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1018 @@
|
|
1
|
+
"""
|
2
|
+
Git Agent - Phase 4 Implementation
|
3
|
+
|
4
|
+
Hybrid-agent architecture for DevOps automation featuring:
|
5
|
+
- Light LLM "planner" node that decides the next action via routing tokens
|
6
|
+
- Small, single-purpose tool agents (Shell, Git, GitHub) that execute real commands
|
7
|
+
- LangGraph state machine that orchestrates the control flow
|
8
|
+
- Configuration-driven behavior with robust error handling
|
9
|
+
- Memory integration for operation tracking
|
10
|
+
|
11
|
+
This agent demonstrates the orchestration pattern before scaling to Terraform, Ansible, etc.
|
12
|
+
|
13
|
+
Architecture:
|
14
|
+
1. Planner LLM analyzes user input and emits routing tokens:
|
15
|
+
- "ROUTE_TO_CLONE" for git clone operations
|
16
|
+
- "ROUTE_TO_ISSUE" for GitHub issue creation
|
17
|
+
- "ROUTE_TO_SHELL" for shell command execution
|
18
|
+
- "ROUTE_TO_END" when no action needed
|
19
|
+
2. Router function maps tokens to appropriate tool nodes
|
20
|
+
3. Tool nodes execute real operations using battle-tested tools
|
21
|
+
4. State machine handles error paths and success flows
|
22
|
+
"""
|
23
|
+
|
24
|
+
import os
|
25
|
+
import uuid
|
26
|
+
import logging
|
27
|
+
from typing import TypedDict, Annotated, Optional, List, Dict, Any
|
28
|
+
|
29
|
+
import yaml
|
30
|
+
from langchain_core.messages import HumanMessage, BaseMessage
|
31
|
+
from langgraph.graph import StateGraph, END
|
32
|
+
from langgraph.checkpoint.memory import MemorySaver
|
33
|
+
from pydantic import BaseModel, Field
|
34
|
+
|
35
|
+
# Import our battle-tested tools
|
36
|
+
from diagram_to_iac.tools.git import (
|
37
|
+
GitExecutor,
|
38
|
+
git_clone,
|
39
|
+
gh_open_issue,
|
40
|
+
GitCloneInput,
|
41
|
+
)
|
42
|
+
# Shell tools were moved from the shell_langgraph agent package into the common
|
43
|
+
# tools module. Import from the new location to keep the agent functional.
|
44
|
+
from diagram_to_iac.tools.shell import ShellExecutor, shell_exec
|
45
|
+
from diagram_to_iac.tools.llm_utils.router import get_llm, LLMRouter
|
46
|
+
from diagram_to_iac.core.agent_base import AgentBase
|
47
|
+
from diagram_to_iac.core.memory import create_memory, LangGraphMemoryAdapter
|
48
|
+
from diagram_to_iac.services.observability import log_event
|
49
|
+
|
50
|
+
from diagram_to_iac.services.observability import log_event
|
51
|
+
|
52
|
+
from .pr import GitPrCreator
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
# --- Pydantic Schemas for Agent I/O ---
|
57
|
+
class GitAgentInput(BaseModel):
|
58
|
+
"""Input schema for GitAgent operations."""
|
59
|
+
query: str = Field(..., description="The DevOps request (git clone, GitHub issue, shell command)")
|
60
|
+
thread_id: str | None = Field(None, description="Optional thread ID for conversation history")
|
61
|
+
workspace_path: str | None = Field(None, description="Optional workspace directory for shell commands")
|
62
|
+
issue_id: int | None = Field(None, description="Existing issue number to comment on")
|
63
|
+
trigger_pr_creation_for_error_type: Optional[str] = Field(None, description="If set, triggers PR creation for this error type (e.g., 'syntax_fmt', 'missing_backend').")
|
64
|
+
pr_title: Optional[str] = Field(None, description="Title for the auto-created PR.")
|
65
|
+
pr_body: Optional[str] = Field(None, description="Body for the auto-created PR.")
|
66
|
+
repo_local_path: Optional[str] = Field(None, description="Local path to the git repository for PR creation.")
|
67
|
+
|
68
|
+
|
69
|
+
class GitAgentOutput(BaseModel):
|
70
|
+
"""Output schema for GitAgent operations."""
|
71
|
+
result: str = Field(..., description="The result of the DevOps operation")
|
72
|
+
thread_id: str = Field(..., description="Thread ID used for the conversation")
|
73
|
+
repo_path: Optional[str] = Field(None, description="Repository path for clone operations")
|
74
|
+
error_message: Optional[str] = Field(None, description="Error message if the operation failed")
|
75
|
+
operation_type: Optional[str] = Field(None, description="Type of operation performed (clone, issue, shell)")
|
76
|
+
pr_url: Optional[str] = Field(None, description="URL of the created pull request, if any.")
|
77
|
+
|
78
|
+
@property
|
79
|
+
def answer(self) -> str:
|
80
|
+
"""Alias for result to match learning guide tests."""
|
81
|
+
return self.result
|
82
|
+
|
83
|
+
|
84
|
+
# --- Agent State Definition ---
|
85
|
+
class GitAgentState(TypedDict):
|
86
|
+
"""State definition for the GitAgent LangGraph."""
|
87
|
+
input_message: HumanMessage
|
88
|
+
tool_output: Annotated[list[BaseMessage], lambda x, y: x + y]
|
89
|
+
final_result: str
|
90
|
+
error_message: Optional[str]
|
91
|
+
operation_type: Optional[str]
|
92
|
+
workspace_path: Optional[str]
|
93
|
+
repo_path: Optional[str]
|
94
|
+
issue_id: Optional[int]
|
95
|
+
trigger_pr_creation_for_error_type: Optional[str]
|
96
|
+
pr_title: Optional[str]
|
97
|
+
pr_body: Optional[str]
|
98
|
+
pr_url: Optional[str]
|
99
|
+
repo_local_path: Optional[str]
|
100
|
+
|
101
|
+
|
102
|
+
# --- Main Agent Class ---
|
103
|
+
class GitAgent(AgentBase):
|
104
|
+
"""
|
105
|
+
GitAgent is a LangGraph-based DevOps automation agent that can:
|
106
|
+
- Clone Git repositories via git_clone tool
|
107
|
+
- Create GitHub issues via gh_open_issue tool
|
108
|
+
- Execute shell commands via shell_exec tool
|
109
|
+
|
110
|
+
Uses a light LLM planner for routing decisions and delegates to specialized tool nodes.
|
111
|
+
Demonstrates hybrid-agent architecture with configuration-driven behavior.
|
112
|
+
"""
|
113
|
+
|
114
|
+
def __init__(self, config_path: str = None, memory_type: str = "persistent"):
|
115
|
+
"""
|
116
|
+
Initialize the GitAgent with configuration and tools.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
config_path: Optional path to YAML configuration file
|
120
|
+
memory_type: Type of memory ("persistent", "memory", or "langgraph")
|
121
|
+
"""
|
122
|
+
# Configure logger for this agent instance
|
123
|
+
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
124
|
+
if not logging.getLogger().hasHandlers():
|
125
|
+
logging.basicConfig(
|
126
|
+
level=logging.INFO,
|
127
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(threadName)s - %(message)s',
|
128
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
129
|
+
)
|
130
|
+
|
131
|
+
# Store memory type for tool initialization
|
132
|
+
self.memory_type = memory_type
|
133
|
+
|
134
|
+
# Load configuration
|
135
|
+
if config_path is None:
|
136
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
137
|
+
config_path = os.path.join(base_dir, 'config.yaml')
|
138
|
+
self.logger.debug(f"Default config path set to: {config_path}")
|
139
|
+
|
140
|
+
try:
|
141
|
+
with open(config_path, 'r') as f:
|
142
|
+
self.config = yaml.safe_load(f)
|
143
|
+
if self.config is None:
|
144
|
+
self.logger.warning(f"Configuration file at {config_path} is empty. Using defaults.")
|
145
|
+
self._set_default_config()
|
146
|
+
else:
|
147
|
+
self.logger.info(f"Configuration loaded successfully from {config_path}")
|
148
|
+
except FileNotFoundError:
|
149
|
+
self.logger.warning(f"Configuration file not found at {config_path}. Using defaults.")
|
150
|
+
self._set_default_config()
|
151
|
+
except yaml.YAMLError as e:
|
152
|
+
self.logger.error(f"Error parsing YAML configuration: {e}. Using defaults.", exc_info=True)
|
153
|
+
self._set_default_config()
|
154
|
+
|
155
|
+
# Initialize enhanced LLM router
|
156
|
+
self.llm_router = LLMRouter()
|
157
|
+
self.logger.info("Enhanced LLM router initialized")
|
158
|
+
|
159
|
+
# Initialize enhanced memory system
|
160
|
+
self.memory = create_memory(memory_type)
|
161
|
+
self.logger.info(f"Enhanced memory system initialized: {type(self.memory).__name__}")
|
162
|
+
|
163
|
+
# Initialize checkpointer
|
164
|
+
self.checkpointer = MemorySaver()
|
165
|
+
self.logger.info("MemorySaver checkpointer initialized")
|
166
|
+
|
167
|
+
# Initialize tools (using our battle-tested implementations)
|
168
|
+
self._initialize_tools()
|
169
|
+
|
170
|
+
# Build and compile the LangGraph
|
171
|
+
self.runnable = self._build_graph()
|
172
|
+
self.logger.info("GitAgent initialized successfully")
|
173
|
+
|
174
|
+
def _set_default_config(self):
|
175
|
+
"""Set default configuration values."""
|
176
|
+
self.logger.info("Setting default configuration for GitAgent")
|
177
|
+
self.config = {
|
178
|
+
'llm': {
|
179
|
+
'model_name': 'gpt-4o-mini',
|
180
|
+
'temperature': 0.1
|
181
|
+
},
|
182
|
+
'routing_keys': {
|
183
|
+
'git_clone': 'ROUTE_TO_GIT_CLONE', # Changed from 'clone'
|
184
|
+
'github_cli': 'ROUTE_TO_GITHUB_CLI', # Changed from 'issue'
|
185
|
+
'shell_exec': 'ROUTE_TO_SHELL_EXEC', # Changed from 'shell'
|
186
|
+
'create_pr': 'ROUTE_TO_CREATE_PR',
|
187
|
+
'end': 'ROUTE_TO_END'
|
188
|
+
},
|
189
|
+
'git_pr_creator': {
|
190
|
+
'copilot_assignee': "CopilotUser", # Placeholder
|
191
|
+
'default_assignees': ["team-infra"], # Placeholder
|
192
|
+
'remote_name': "origin"
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
def _initialize_tools(self):
|
197
|
+
"""Initialize the DevOps tools."""
|
198
|
+
try:
|
199
|
+
# Initialize GitExecutor (handles both git clone and GitHub CLI)
|
200
|
+
self.git_executor = GitExecutor(memory_type=self.memory_type)
|
201
|
+
|
202
|
+
# Initialize ShellExecutor
|
203
|
+
self.shell_executor = ShellExecutor(memory_type=self.memory_type)
|
204
|
+
|
205
|
+
# GitPrCreator is initialized on-demand in _create_pr_node
|
206
|
+
# self.git_pr_creator can be removed if not used elsewhere, or kept as None
|
207
|
+
self.git_pr_creator = None
|
208
|
+
|
209
|
+
# Register tools for easy access
|
210
|
+
self.tools = {
|
211
|
+
"git_clone": git_clone,
|
212
|
+
"gh_open_issue": gh_open_issue,
|
213
|
+
"shell_exec": shell_exec
|
214
|
+
}
|
215
|
+
|
216
|
+
self.logger.info(f"Tools initialized: {list(self.tools.keys())}")
|
217
|
+
|
218
|
+
except Exception as e:
|
219
|
+
self.logger.error(f"Failed to initialize tools: {e}", exc_info=True)
|
220
|
+
raise
|
221
|
+
|
222
|
+
def _planner_llm_node(self, state: GitAgentState):
|
223
|
+
"""
|
224
|
+
LLM planner node that analyzes input and decides routing.
|
225
|
+
Emits routing tokens based on the user's DevOps request.
|
226
|
+
"""
|
227
|
+
# Check for direct PR creation trigger
|
228
|
+
if state.get('trigger_pr_creation_for_error_type') and state.get('repo_local_path'):
|
229
|
+
self.logger.info(f"PR creation triggered directly for error type: {state.get('trigger_pr_creation_for_error_type')}")
|
230
|
+
return {
|
231
|
+
"final_result": self.config['routing_keys']['create_pr'],
|
232
|
+
"operation_type": "create_pr",
|
233
|
+
"error_message": None
|
234
|
+
}
|
235
|
+
|
236
|
+
# Get LLM configuration
|
237
|
+
llm_config = self.config.get('llm', {})
|
238
|
+
model_name = llm_config.get('model_name')
|
239
|
+
temperature = llm_config.get('temperature')
|
240
|
+
|
241
|
+
# Use enhanced LLM router
|
242
|
+
try:
|
243
|
+
if model_name is not None or temperature is not None:
|
244
|
+
actual_model_name = model_name if model_name is not None else 'gpt-4o-mini'
|
245
|
+
actual_temperature = temperature if temperature is not None else 0.1
|
246
|
+
self.logger.debug(f"Planner using LLM: {actual_model_name}, Temp: {actual_temperature}")
|
247
|
+
|
248
|
+
llm = self.llm_router.get_llm(
|
249
|
+
model_name=actual_model_name,
|
250
|
+
temperature=actual_temperature,
|
251
|
+
agent_name="git_agent"
|
252
|
+
)
|
253
|
+
else:
|
254
|
+
self.logger.debug("Planner using agent-specific LLM configuration")
|
255
|
+
llm = self.llm_router.get_llm_for_agent("git_agent")
|
256
|
+
except Exception as e:
|
257
|
+
self.logger.error(f"Failed to get LLM from router: {e}. Falling back to basic get_llm.")
|
258
|
+
llm = get_llm(model_name=model_name, temperature=temperature)
|
259
|
+
|
260
|
+
# Store conversation in memory
|
261
|
+
query_content = state['input_message'].content
|
262
|
+
self.memory.add_to_conversation("user", query_content, {
|
263
|
+
"agent": "git_agent",
|
264
|
+
"node": "planner"
|
265
|
+
})
|
266
|
+
|
267
|
+
try:
|
268
|
+
self.logger.debug(f"Planner LLM input: {query_content}")
|
269
|
+
|
270
|
+
# Build the analysis prompt
|
271
|
+
analysis_prompt_template = self.config.get('prompts', {}).get('planner_prompt', """
|
272
|
+
User input: "{user_input}"
|
273
|
+
Analyze this DevOps request and determine the appropriate action:
|
274
|
+
1. If requesting to clone a Git repository (keywords: 'clone', 'download repo', 'git clone'), respond with "{route_git_clone}"
|
275
|
+
2. If requesting to open a GitHub issue (keywords: 'open issue', 'create issue', 'report bug'), respond with "{route_github_cli}"
|
276
|
+
3. If requesting a shell command (keywords: 'run command', 'execute', 'shell'), respond with "{route_shell_exec}"
|
277
|
+
4. If the request is complete or no action needed, respond with "{route_end}"
|
278
|
+
|
279
|
+
Important: Only use routing tokens if the input contains actionable DevOps requests.
|
280
|
+
""")
|
281
|
+
|
282
|
+
routing_keys = self.config.get('routing_keys', {
|
283
|
+
"git_clone": "ROUTE_TO_GIT_CLONE",
|
284
|
+
"github_cli": "ROUTE_TO_GITHUB_CLI",
|
285
|
+
"shell_exec": "ROUTE_TO_SHELL_EXEC",
|
286
|
+
"end": "ROUTE_TO_END"
|
287
|
+
})
|
288
|
+
|
289
|
+
analysis_prompt = analysis_prompt_template.format(
|
290
|
+
user_input=query_content,
|
291
|
+
route_git_clone=routing_keys['git_clone'],
|
292
|
+
route_github_cli=routing_keys['github_cli'],
|
293
|
+
route_shell_exec=routing_keys['shell_exec'],
|
294
|
+
route_end=routing_keys['end']
|
295
|
+
)
|
296
|
+
|
297
|
+
self.logger.debug(f"Planner LLM prompt: {analysis_prompt}")
|
298
|
+
|
299
|
+
response = llm.invoke([HumanMessage(content=analysis_prompt)])
|
300
|
+
self.logger.debug(f"Planner LLM response: {response.content}")
|
301
|
+
response_content = response.content.strip()
|
302
|
+
|
303
|
+
# Store LLM response in memory
|
304
|
+
self.memory.add_to_conversation("assistant", response_content, {
|
305
|
+
"agent": "git_agent",
|
306
|
+
"node": "planner",
|
307
|
+
"model": model_name
|
308
|
+
})
|
309
|
+
|
310
|
+
# Determine routing based on response content
|
311
|
+
new_state_update = {}
|
312
|
+
if routing_keys['git_clone'] in response_content:
|
313
|
+
new_state_update = {
|
314
|
+
"final_result": "route_to_clone",
|
315
|
+
"operation_type": "clone",
|
316
|
+
"error_message": None
|
317
|
+
}
|
318
|
+
elif routing_keys['github_cli'] in response_content:
|
319
|
+
new_state_update = {
|
320
|
+
"final_result": "route_to_issue",
|
321
|
+
"operation_type": "issue",
|
322
|
+
"error_message": None
|
323
|
+
}
|
324
|
+
elif routing_keys['shell_exec'] in response_content:
|
325
|
+
new_state_update = {
|
326
|
+
"final_result": "route_to_shell",
|
327
|
+
"operation_type": "shell",
|
328
|
+
"error_message": None
|
329
|
+
}
|
330
|
+
elif routing_keys['end'] in response_content: # Explicitly check for 'end'
|
331
|
+
# Direct answer or route to end
|
332
|
+
new_state_update = {
|
333
|
+
"final_result": response.content,
|
334
|
+
"operation_type": "direct_answer",
|
335
|
+
"error_message": None
|
336
|
+
}
|
337
|
+
|
338
|
+
else: # Default if no specific route token is found
|
339
|
+
new_state_update = {
|
340
|
+
"final_result": response.content, # Treat as direct answer
|
341
|
+
"operation_type": "direct_answer",
|
342
|
+
"error_message": None
|
343
|
+
}
|
344
|
+
|
345
|
+
self.logger.info(f"Planner decision: {new_state_update.get('final_result', 'N/A')}")
|
346
|
+
return new_state_update
|
347
|
+
|
348
|
+
except Exception as e:
|
349
|
+
self.logger.error(f"LLM error in planner: {e}", exc_info=True)
|
350
|
+
self.memory.add_to_conversation("system", f"Error in planner: {str(e)}", {
|
351
|
+
"agent": "git_agent",
|
352
|
+
"node": "planner",
|
353
|
+
"error": True
|
354
|
+
})
|
355
|
+
return {
|
356
|
+
"final_result": "Sorry, I encountered an issue processing your request.",
|
357
|
+
"error_message": str(e),
|
358
|
+
"operation_type": "error"
|
359
|
+
}
|
360
|
+
|
361
|
+
def _clone_repo_node(self, state: GitAgentState):
|
362
|
+
"""
|
363
|
+
Git clone tool node that handles repository cloning operations.
|
364
|
+
"""
|
365
|
+
self.logger.info(f"Clone repo node invoked for: {state['input_message'].content}")
|
366
|
+
|
367
|
+
try:
|
368
|
+
text_content = state['input_message'].content
|
369
|
+
|
370
|
+
# Store tool invocation in memory
|
371
|
+
self.memory.add_to_conversation("system", f"Git clone tool invoked", {
|
372
|
+
"agent": "git_agent",
|
373
|
+
"node": "clone_repo",
|
374
|
+
"input": text_content
|
375
|
+
})
|
376
|
+
|
377
|
+
# For now, extract a simple repo URL from the input
|
378
|
+
# In a real implementation, you might use more sophisticated parsing
|
379
|
+
import re
|
380
|
+
url_pattern = r'https?://github\.com/[\w\-\.]+/[\w\-\.]+'
|
381
|
+
urls = re.findall(url_pattern, text_content)
|
382
|
+
|
383
|
+
if not urls:
|
384
|
+
words = text_content.split()
|
385
|
+
repo_url = words[-1]
|
386
|
+
else:
|
387
|
+
repo_url = urls[0]
|
388
|
+
|
389
|
+
# Use the git executor directly to allow invalid URLs
|
390
|
+
git_input = GitCloneInput.model_construct(
|
391
|
+
repo_url=repo_url,
|
392
|
+
workspace=state.get("workspace_path") or None,
|
393
|
+
)
|
394
|
+
result_obj = self.git_executor.git_clone(git_input)
|
395
|
+
|
396
|
+
# Organically handle different result statuses
|
397
|
+
if result_obj.status == "SUCCESS":
|
398
|
+
result = result_obj.repo_path
|
399
|
+
state["repo_path"] = result_obj.repo_path
|
400
|
+
success_msg = f"Successfully cloned repository: {result}"
|
401
|
+
|
402
|
+
self.memory.add_to_conversation("system", f"Repository cloned: {result}", {
|
403
|
+
"agent": "git_agent",
|
404
|
+
"node": "clone_repo",
|
405
|
+
"repo_url": repo_url,
|
406
|
+
"result": "SUCCESS"
|
407
|
+
})
|
408
|
+
|
409
|
+
return {
|
410
|
+
"final_result": success_msg,
|
411
|
+
"error_message": None,
|
412
|
+
"operation_type": "clone",
|
413
|
+
"repo_path": result_obj.repo_path
|
414
|
+
}
|
415
|
+
|
416
|
+
elif result_obj.status == "AUTH_FAILED":
|
417
|
+
# Organically handle authentication failures with helpful context
|
418
|
+
github_token = os.environ.get('GITHUB_TOKEN')
|
419
|
+
if github_token and github_token.strip():
|
420
|
+
auth_msg = f"Git executor: Authentication failed for repository '{repo_url}'. GitHub token is present but may be invalid or expired. Please check token permissions."
|
421
|
+
else:
|
422
|
+
auth_msg = f"Git executor: Authentication failed for repository '{repo_url}'. For private repositories, please set the GITHUB_TOKEN environment variable with a valid personal access token."
|
423
|
+
|
424
|
+
self.memory.add_to_conversation("system", f"Repository cloned: {auth_msg}", {
|
425
|
+
"agent": "git_agent",
|
426
|
+
"node": "clone_repo",
|
427
|
+
"repo_url": repo_url,
|
428
|
+
"result": "AUTH_FAILED"
|
429
|
+
})
|
430
|
+
|
431
|
+
return {
|
432
|
+
"final_result": f"Clone failed: {auth_msg}",
|
433
|
+
"error_message": result_obj.error_message or "Authentication failed",
|
434
|
+
"operation_type": "clone",
|
435
|
+
"repo_path": None
|
436
|
+
}
|
437
|
+
|
438
|
+
else:
|
439
|
+
# Handle other errors (timeout, invalid URL, etc.)
|
440
|
+
error_msg = result_obj.error_message or f"Git executor: Failed to clone repository '{repo_url}'"
|
441
|
+
|
442
|
+
self.memory.add_to_conversation("system", f"Repository cloned: {error_msg}", {
|
443
|
+
"agent": "git_agent",
|
444
|
+
"node": "clone_repo",
|
445
|
+
"repo_url": repo_url,
|
446
|
+
"result": result_obj.status
|
447
|
+
})
|
448
|
+
|
449
|
+
return {
|
450
|
+
"final_result": f"Clone failed: {error_msg}",
|
451
|
+
"error_message": result_obj.error_message or error_msg,
|
452
|
+
"operation_type": "clone",
|
453
|
+
"repo_path": None
|
454
|
+
}
|
455
|
+
|
456
|
+
except Exception as e:
|
457
|
+
self.logger.error(f"Error in clone repo node: {e}", exc_info=True)
|
458
|
+
self.memory.add_to_conversation("system", f"Clone error: {str(e)}", {
|
459
|
+
"agent": "git_agent",
|
460
|
+
"node": "clone_repo",
|
461
|
+
"error": True
|
462
|
+
})
|
463
|
+
return {
|
464
|
+
"final_result": "Sorry, I couldn't clone the repository due to an error.",
|
465
|
+
"error_message": str(e),
|
466
|
+
"operation_type": "clone",
|
467
|
+
"repo_path": None
|
468
|
+
}
|
469
|
+
|
470
|
+
def _open_issue_node(self, state: GitAgentState):
|
471
|
+
"""
|
472
|
+
GitHub issue tool node that handles issue creation operations.
|
473
|
+
"""
|
474
|
+
self.logger.info(f"Open issue node invoked for: {state['input_message'].content}")
|
475
|
+
|
476
|
+
try:
|
477
|
+
text_content = state['input_message'].content
|
478
|
+
|
479
|
+
# Store tool invocation in memory
|
480
|
+
self.memory.add_to_conversation("system", f"GitHub issue tool invoked", {
|
481
|
+
"agent": "git_agent",
|
482
|
+
"node": "open_issue",
|
483
|
+
"input": text_content
|
484
|
+
})
|
485
|
+
|
486
|
+
# Enhanced parsing for both test format and supervisor format
|
487
|
+
import re
|
488
|
+
|
489
|
+
# Pattern 1: Enhanced supervisor format - "open issue TITLE for repository URL: BODY"
|
490
|
+
supervisor_pattern = r'^open issue\s+(.+?)\s+for repository\s+(https://github\.com/[\w\-\.]+/[\w\-\.]+):\s*(.+)$'
|
491
|
+
supervisor_match = re.search(supervisor_pattern, text_content, re.IGNORECASE | re.DOTALL)
|
492
|
+
|
493
|
+
if supervisor_match:
|
494
|
+
title = supervisor_match.group(1).strip()
|
495
|
+
repo_url = supervisor_match.group(2).strip()
|
496
|
+
body = supervisor_match.group(3).strip()
|
497
|
+
repo = repo_url.replace('https://github.com/', '')
|
498
|
+
|
499
|
+
# Clean ANSI codes from title and body for better presentation
|
500
|
+
from diagram_to_iac.tools.text_utils import clean_ansi_codes, enhance_error_message_for_issue
|
501
|
+
title = clean_ansi_codes(title)
|
502
|
+
body = enhance_error_message_for_issue(body)
|
503
|
+
|
504
|
+
self.logger.info(f"Parsed enhanced supervisor format - Title: {title}, Repo: {repo}")
|
505
|
+
|
506
|
+
else:
|
507
|
+
# Pattern 2: Legacy supervisor format - "open issue TITLE: BODY"
|
508
|
+
legacy_supervisor_pattern = r'^open issue\s+([^:]+):\s*(.+)$'
|
509
|
+
legacy_supervisor_match = re.search(legacy_supervisor_pattern, text_content, re.IGNORECASE | re.DOTALL)
|
510
|
+
|
511
|
+
if legacy_supervisor_match:
|
512
|
+
title = legacy_supervisor_match.group(1).strip()
|
513
|
+
body = legacy_supervisor_match.group(2).strip()
|
514
|
+
|
515
|
+
# Clean ANSI codes from title and body for better presentation
|
516
|
+
from diagram_to_iac.tools.text_utils import clean_ansi_codes, enhance_error_message_for_issue
|
517
|
+
title = clean_ansi_codes(title)
|
518
|
+
body = enhance_error_message_for_issue(body)
|
519
|
+
|
520
|
+
# Extract repository from the body if present
|
521
|
+
repo_in_body = re.search(r'\*?\*?Repository:\*?\*?\s*(https://github\.com/[\w\-\.]+/[\w\-\.]+)', body)
|
522
|
+
if repo_in_body:
|
523
|
+
repo_url = repo_in_body.group(1)
|
524
|
+
repo = repo_url.replace('https://github.com/', '')
|
525
|
+
else:
|
526
|
+
# No fallback - require explicit repository specification
|
527
|
+
error_msg = "Could not extract repository from issue request. Repository must be specified in body as 'Repository: https://github.com/owner/repo'"
|
528
|
+
self.logger.warning(error_msg)
|
529
|
+
self.memory.add_to_conversation("system", error_msg, {
|
530
|
+
"agent": "git_agent",
|
531
|
+
"node": "open_issue",
|
532
|
+
"error": True
|
533
|
+
})
|
534
|
+
return {
|
535
|
+
"final_result": error_msg,
|
536
|
+
"error_message": "Missing repository specification",
|
537
|
+
"operation_type": "issue"
|
538
|
+
}
|
539
|
+
|
540
|
+
self.logger.info(f"Parsed legacy supervisor format - Title: {title}, Repo: {repo}")
|
541
|
+
|
542
|
+
else:
|
543
|
+
# Pattern 3: Test format - "titled 'X' with body 'Y' in repository user/repo"
|
544
|
+
repo_pattern = r'(?:in|for|on|repository)\s+(?:repository\s+)?(\w+/\w+)'
|
545
|
+
title_pattern = r"titled\s*['\"]([^'\"]+)['\"]"
|
546
|
+
body_pattern = r'(?:with\s+body|body|content|description)\s*[\'"]?([^\'"\n]+)[\'"]?'
|
547
|
+
|
548
|
+
repo_match = re.search(repo_pattern, text_content, re.IGNORECASE)
|
549
|
+
title_match = re.search(title_pattern, text_content, re.IGNORECASE)
|
550
|
+
body_match = re.search(body_pattern, text_content, re.IGNORECASE)
|
551
|
+
|
552
|
+
if not repo_match or not title_match:
|
553
|
+
error_msg = "Could not extract repository name and issue title from the request."
|
554
|
+
self.logger.warning(error_msg)
|
555
|
+
self.memory.add_to_conversation("system", error_msg, {
|
556
|
+
"agent": "git_agent",
|
557
|
+
"node": "open_issue",
|
558
|
+
"error": True
|
559
|
+
})
|
560
|
+
return {
|
561
|
+
"final_result": error_msg,
|
562
|
+
"error_message": "Missing repository or issue details",
|
563
|
+
"operation_type": "issue"
|
564
|
+
}
|
565
|
+
|
566
|
+
repo = repo_match.group(1)
|
567
|
+
title = title_match.group(1).strip()
|
568
|
+
body = body_match.group(1).strip() if body_match else f"Issue created via GitAgent: {text_content}"
|
569
|
+
repo_url = f"https://github.com/{repo}"
|
570
|
+
|
571
|
+
# Clean ANSI codes from title and body for better presentation
|
572
|
+
from diagram_to_iac.tools.text_utils import clean_ansi_codes, enhance_error_message_for_issue
|
573
|
+
title = clean_ansi_codes(title)
|
574
|
+
body = enhance_error_message_for_issue(body)
|
575
|
+
|
576
|
+
self.logger.info(f"Parsed test format - Title: {title}, Repo: {repo}")
|
577
|
+
|
578
|
+
# Convert repo to full URL format expected by the tool
|
579
|
+
gh_params = {
|
580
|
+
"repo_url": repo_url,
|
581
|
+
"title": title,
|
582
|
+
"body": body,
|
583
|
+
}
|
584
|
+
if state.get("issue_id") is not None:
|
585
|
+
gh_params["issue_id"] = state["issue_id"]
|
586
|
+
|
587
|
+
result = self.tools['gh_open_issue'].invoke(gh_params)
|
588
|
+
|
589
|
+
# Store successful result in memory
|
590
|
+
self.memory.add_to_conversation("system", f"Issue created: {result}", {
|
591
|
+
"agent": "git_agent",
|
592
|
+
"node": "open_issue",
|
593
|
+
"repo": repo,
|
594
|
+
"title": title,
|
595
|
+
"result": result
|
596
|
+
})
|
597
|
+
|
598
|
+
action = "added comment to" if state.get("issue_id") is not None else "created"
|
599
|
+
return {
|
600
|
+
"final_result": f"Successfully {action} GitHub issue: {result}",
|
601
|
+
"error_message": None,
|
602
|
+
"operation_type": "issue"
|
603
|
+
}
|
604
|
+
|
605
|
+
except Exception as e:
|
606
|
+
self.logger.error(f"Error in open issue node: {e}", exc_info=True)
|
607
|
+
self.memory.add_to_conversation("system", f"Issue creation error: {str(e)}", {
|
608
|
+
"agent": "git_agent",
|
609
|
+
"node": "open_issue",
|
610
|
+
"error": True
|
611
|
+
})
|
612
|
+
return {
|
613
|
+
"final_result": "Sorry, I couldn't create the GitHub issue due to an error.",
|
614
|
+
"error_message": str(e),
|
615
|
+
"operation_type": "issue"
|
616
|
+
}
|
617
|
+
|
618
|
+
def _shell_exec_node(self, state: GitAgentState):
|
619
|
+
"""
|
620
|
+
Shell execution tool node that handles command execution operations.
|
621
|
+
"""
|
622
|
+
self.logger.info(f"Shell exec node invoked for: {state['input_message'].content}")
|
623
|
+
|
624
|
+
try:
|
625
|
+
text_content = state['input_message'].content
|
626
|
+
|
627
|
+
# Store tool invocation in memory
|
628
|
+
self.memory.add_to_conversation("system", f"Shell exec tool invoked", {
|
629
|
+
"agent": "git_agent",
|
630
|
+
"node": "shell_exec",
|
631
|
+
"input": text_content
|
632
|
+
})
|
633
|
+
|
634
|
+
# Extract command from input (simplified parsing)
|
635
|
+
import re
|
636
|
+
|
637
|
+
# More flexible patterns to match test inputs
|
638
|
+
command_patterns = [
|
639
|
+
r'(?:run|execute|command):\s*[\'"]?(.+?)[\'"]?(?:\.|$)', # "execute: command"
|
640
|
+
r'(?:run|execute)\s+[\'"]([^\'\"]+)[\'"]', # "execute 'command'"
|
641
|
+
r'(?:run|execute)\s+([^\'\"\n\.]+)', # "execute command"
|
642
|
+
]
|
643
|
+
|
644
|
+
command = None
|
645
|
+
for pattern in command_patterns:
|
646
|
+
command_match = re.search(pattern, text_content, re.IGNORECASE)
|
647
|
+
if command_match:
|
648
|
+
command = command_match.group(1).strip()
|
649
|
+
break
|
650
|
+
|
651
|
+
if not command:
|
652
|
+
# Try to extract anything that looks like a command
|
653
|
+
words = text_content.split()
|
654
|
+
command_candidates = []
|
655
|
+
for word in words:
|
656
|
+
if word in ['git', 'ls', 'pwd', 'echo', 'cat', 'grep', 'find', 'mkdir', 'rm']:
|
657
|
+
# Find the rest of the command
|
658
|
+
idx = words.index(word)
|
659
|
+
# Look for quoted command or take words until common sentence endings
|
660
|
+
remaining = words[idx:]
|
661
|
+
command_end = len(remaining)
|
662
|
+
for i, w in enumerate(remaining):
|
663
|
+
if w.lower() in ['in', 'to', 'for', 'directory', 'files', 'folder']:
|
664
|
+
command_end = i
|
665
|
+
break
|
666
|
+
command_candidates.append(' '.join(remaining[:command_end]))
|
667
|
+
break
|
668
|
+
|
669
|
+
if not command_candidates:
|
670
|
+
error_msg = "Could not extract a shell command from the request."
|
671
|
+
self.logger.warning(error_msg)
|
672
|
+
self.memory.add_to_conversation("system", error_msg, {
|
673
|
+
"agent": "git_agent",
|
674
|
+
"node": "shell_exec",
|
675
|
+
"error": True
|
676
|
+
})
|
677
|
+
return {
|
678
|
+
"final_result": error_msg,
|
679
|
+
"error_message": "No command found",
|
680
|
+
"operation_type": "shell"
|
681
|
+
}
|
682
|
+
command = command_candidates[0]
|
683
|
+
|
684
|
+
# Use the shell_exec tool
|
685
|
+
tool_input = {"command": command}
|
686
|
+
if state.get("workspace_path"):
|
687
|
+
tool_input["cwd"] = state["workspace_path"]
|
688
|
+
result = self.tools['shell_exec'].invoke(tool_input)
|
689
|
+
|
690
|
+
# Store successful result in memory
|
691
|
+
self.memory.add_to_conversation("system", f"Command executed: {result}", {
|
692
|
+
"agent": "git_agent",
|
693
|
+
"node": "shell_exec",
|
694
|
+
"command": command,
|
695
|
+
"result": result
|
696
|
+
})
|
697
|
+
|
698
|
+
return {
|
699
|
+
"final_result": f"Command executed successfully: {result}",
|
700
|
+
"error_message": None,
|
701
|
+
"operation_type": "shell"
|
702
|
+
}
|
703
|
+
|
704
|
+
except Exception as e:
|
705
|
+
self.logger.error(f"Error in shell exec node: {e}", exc_info=True)
|
706
|
+
self.memory.add_to_conversation("system", f"Shell exec error: {str(e)}", {
|
707
|
+
"agent": "git_agent",
|
708
|
+
"node": "shell_exec",
|
709
|
+
"error": True
|
710
|
+
})
|
711
|
+
return {
|
712
|
+
"final_result": "Sorry, I couldn't execute the command due to an error.",
|
713
|
+
"error_message": str(e),
|
714
|
+
"operation_type": "shell"
|
715
|
+
}
|
716
|
+
|
717
|
+
def _create_pr_node(self, state: GitAgentState):
|
718
|
+
self.logger.info(f"Create PR node invoked for error type: {state.get('trigger_pr_creation_for_error_type')}")
|
719
|
+
error_type = state.get('trigger_pr_creation_for_error_type')
|
720
|
+
title = state.get('pr_title') or f"Auto-fix for {error_type}"
|
721
|
+
body = state.get('pr_body') or f"Automated PR to address {error_type}."
|
722
|
+
repo_local_path = state.get('repo_local_path')
|
723
|
+
|
724
|
+
if not error_type or not repo_local_path:
|
725
|
+
error_msg = "Missing error_type or repo_local_path for PR creation."
|
726
|
+
self.logger.error(error_msg)
|
727
|
+
return {
|
728
|
+
"final_result": f"PR creation failed: {error_msg}",
|
729
|
+
"error_message": error_msg,
|
730
|
+
"operation_type": "create_pr",
|
731
|
+
"pr_url": None,
|
732
|
+
"repo_path": repo_local_path # Keep repo_path consistent
|
733
|
+
}
|
734
|
+
|
735
|
+
# Initialize GitPrCreator with the specific repo_local_path for this operation
|
736
|
+
git_pr_config = self.config.get('git_pr_creator', {})
|
737
|
+
copilot_assignee = git_pr_config.get('copilot_assignee')
|
738
|
+
default_assignees = git_pr_config.get('default_assignees', [])
|
739
|
+
remote_name = git_pr_config.get('remote_name', 'origin')
|
740
|
+
|
741
|
+
pr_creator = GitPrCreator(
|
742
|
+
repo_path=repo_local_path,
|
743
|
+
remote_name=remote_name,
|
744
|
+
copilot_assignee=copilot_assignee,
|
745
|
+
default_assignees=default_assignees
|
746
|
+
)
|
747
|
+
|
748
|
+
pr_result = pr_creator.create_draft_pr(
|
749
|
+
error_type=error_type,
|
750
|
+
title=title,
|
751
|
+
body=body
|
752
|
+
)
|
753
|
+
|
754
|
+
if pr_result and pr_result.get("status") == "success" and pr_result.get("pr_url"):
|
755
|
+
self.memory.add_to_conversation("system", f"Draft PR created: {pr_result['pr_url']}", {
|
756
|
+
"agent": "git_agent", "node": "create_pr", "result": "SUCCESS", "pr_details": pr_result
|
757
|
+
})
|
758
|
+
return {
|
759
|
+
"final_result": f"Successfully created draft PR: {pr_result['pr_url']}",
|
760
|
+
"error_message": None,
|
761
|
+
"operation_type": "create_pr",
|
762
|
+
"pr_url": pr_result["pr_url"],
|
763
|
+
"repo_path": repo_local_path
|
764
|
+
}
|
765
|
+
else:
|
766
|
+
error_msg = "Failed to create draft PR."
|
767
|
+
if pr_result and pr_result.get("message"):
|
768
|
+
error_msg = pr_result.get("message")
|
769
|
+
self.logger.error(f"PR creation failed: {error_msg}")
|
770
|
+
self.memory.add_to_conversation("system", f"PR creation failed: {error_msg}", {
|
771
|
+
"agent": "git_agent", "node": "create_pr", "result": "FAILURE", "error_details": pr_result
|
772
|
+
})
|
773
|
+
return {
|
774
|
+
"final_result": f"PR creation failed: {error_msg}",
|
775
|
+
"error_message": error_msg,
|
776
|
+
"operation_type": "create_pr",
|
777
|
+
"pr_url": None,
|
778
|
+
"repo_path": repo_local_path
|
779
|
+
}
|
780
|
+
|
781
|
+
def _route_after_planner(self, state: GitAgentState):
|
782
|
+
"""
|
783
|
+
Router function that determines the next node based on planner output.
|
784
|
+
Maps routing tokens to appropriate tool nodes or END.
|
785
|
+
"""
|
786
|
+
self.logger.debug(f"Routing after planner. State: {state.get('final_result')}, error: {state.get('error_message')}")
|
787
|
+
|
788
|
+
if state.get("error_message"):
|
789
|
+
self.logger.warning(f"Error detected in planner, routing to END: {state['error_message']}")
|
790
|
+
return END
|
791
|
+
|
792
|
+
final_result = state.get("final_result", "").strip()
|
793
|
+
|
794
|
+
# Enhanced routing logic with multiple token support
|
795
|
+
if final_result in ["route_to_clone", "ROUTE_TO_GIT_CLONE"]:
|
796
|
+
return "clone_repo"
|
797
|
+
elif final_result in ["route_to_issue", "ROUTE_TO_GITHUB_ISSUE"]:
|
798
|
+
return "open_issue"
|
799
|
+
elif final_result in ["route_to_shell", "ROUTE_TO_SHELL"]:
|
800
|
+
return "shell_exec"
|
801
|
+
elif final_result == self.config['routing_keys']['create_pr']:
|
802
|
+
return "create_pr_node"
|
803
|
+
elif final_result in ["route_to_end", "ROUTE_TO_END"]:
|
804
|
+
return END
|
805
|
+
else:
|
806
|
+
self.logger.warning(f"Unknown routing token: {final_result}, routing to END")
|
807
|
+
return END
|
808
|
+
|
809
|
+
def _build_graph(self):
|
810
|
+
"""
|
811
|
+
Build and compile the LangGraph state machine.
|
812
|
+
Creates nodes for planner and each tool, sets up routing.
|
813
|
+
"""
|
814
|
+
graph_builder = StateGraph(GitAgentState)
|
815
|
+
|
816
|
+
# Add nodes
|
817
|
+
graph_builder.add_node("planner_llm", self._planner_llm_node)
|
818
|
+
graph_builder.add_node("clone_repo", self._clone_repo_node)
|
819
|
+
graph_builder.add_node("open_issue", self._open_issue_node)
|
820
|
+
graph_builder.add_node("shell_exec", self._shell_exec_node)
|
821
|
+
graph_builder.add_node("create_pr_node", self._create_pr_node)
|
822
|
+
|
823
|
+
# Set entry point
|
824
|
+
graph_builder.set_entry_point("planner_llm")
|
825
|
+
|
826
|
+
# Configure routing map
|
827
|
+
routing_map = self.config.get('routing_map', {
|
828
|
+
"clone_repo": "clone_repo",
|
829
|
+
"open_issue": "open_issue",
|
830
|
+
"shell_exec": "shell_exec",
|
831
|
+
"create_pr_node": "create_pr_node", # Added for PR creation
|
832
|
+
END: END
|
833
|
+
})
|
834
|
+
|
835
|
+
# Add conditional edges from planner
|
836
|
+
graph_builder.add_conditional_edges(
|
837
|
+
"planner_llm",
|
838
|
+
self._route_after_planner,
|
839
|
+
routing_map
|
840
|
+
)
|
841
|
+
|
842
|
+
# Add edges from tool nodes to END
|
843
|
+
graph_builder.add_edge("clone_repo", END)
|
844
|
+
graph_builder.add_edge("open_issue", END)
|
845
|
+
graph_builder.add_edge("shell_exec", END)
|
846
|
+
graph_builder.add_edge("create_pr_node", END) # New edge for PR node
|
847
|
+
|
848
|
+
# Compile with checkpointer
|
849
|
+
return graph_builder.compile(checkpointer=self.checkpointer)
|
850
|
+
|
851
|
+
def run(self, agent_input: GitAgentInput) -> GitAgentOutput:
|
852
|
+
"""
|
853
|
+
Run the agent with the given input.
|
854
|
+
|
855
|
+
Args:
|
856
|
+
agent_input: GitAgentInput with query and optional thread_id
|
857
|
+
|
858
|
+
Returns:
|
859
|
+
GitAgentOutput with result, thread_id, and optional error
|
860
|
+
"""
|
861
|
+
current_thread_id = agent_input.thread_id if agent_input.thread_id else str(uuid.uuid4())
|
862
|
+
self.logger.info(
|
863
|
+
f"Run invoked with query: '{agent_input.query}', thread_id: {current_thread_id}"
|
864
|
+
)
|
865
|
+
log_event(
|
866
|
+
"git_agent_run_start",
|
867
|
+
query=agent_input.query,
|
868
|
+
thread_id=current_thread_id,
|
869
|
+
)
|
870
|
+
|
871
|
+
# Initial state for LangGraph
|
872
|
+
initial_state = {
|
873
|
+
"input_message": HumanMessage(content=agent_input.query),
|
874
|
+
"tool_output": [],
|
875
|
+
"error_message": None,
|
876
|
+
"operation_type": None,
|
877
|
+
"workspace_path": agent_input.workspace_path,
|
878
|
+
"repo_path": None, # This is for clone output, repo_local_path is for PR input
|
879
|
+
"issue_id": agent_input.issue_id,
|
880
|
+
"trigger_pr_creation_for_error_type": agent_input.trigger_pr_creation_for_error_type,
|
881
|
+
"pr_title": agent_input.pr_title,
|
882
|
+
"pr_body": agent_input.pr_body,
|
883
|
+
"repo_local_path": agent_input.repo_local_path,
|
884
|
+
"pr_url": None,
|
885
|
+
}
|
886
|
+
|
887
|
+
langgraph_config = {"configurable": {"thread_id": current_thread_id}}
|
888
|
+
|
889
|
+
try:
|
890
|
+
# Run the graph
|
891
|
+
result_state = self.runnable.invoke(initial_state, config=langgraph_config)
|
892
|
+
|
893
|
+
# Extract results
|
894
|
+
final_result = result_state.get("final_result", "No result found.")
|
895
|
+
error_message = result_state.get("error_message")
|
896
|
+
operation_type = result_state.get("operation_type")
|
897
|
+
repo_path = result_state.get("repo_path") # Output from clone
|
898
|
+
pr_url = result_state.get("pr_url") # Output from PR creation
|
899
|
+
|
900
|
+
if error_message:
|
901
|
+
self.logger.error(f"Run completed with error: {error_message}")
|
902
|
+
else:
|
903
|
+
self.logger.info(f"Run completed successfully: {final_result}")
|
904
|
+
|
905
|
+
log_event(
|
906
|
+
"git_agent_run_end",
|
907
|
+
thread_id=current_thread_id,
|
908
|
+
error=error_message,
|
909
|
+
result=final_result,
|
910
|
+
)
|
911
|
+
|
912
|
+
output = GitAgentOutput(
|
913
|
+
result=final_result,
|
914
|
+
thread_id=current_thread_id,
|
915
|
+
repo_path=repo_path,
|
916
|
+
error_message=error_message,
|
917
|
+
operation_type=operation_type,
|
918
|
+
pr_url=pr_url
|
919
|
+
)
|
920
|
+
return output
|
921
|
+
|
922
|
+
except Exception as e:
|
923
|
+
self.logger.error(f"Error during agent run: {e}", exc_info=True)
|
924
|
+
log_event(
|
925
|
+
"git_agent_run_exception",
|
926
|
+
thread_id=current_thread_id,
|
927
|
+
error=str(e),
|
928
|
+
)
|
929
|
+
return GitAgentOutput(
|
930
|
+
result="An unexpected error occurred during execution.",
|
931
|
+
thread_id=current_thread_id,
|
932
|
+
repo_path=None, # Or more specifically result_state.get("repo_path") if available
|
933
|
+
error_message=str(e),
|
934
|
+
operation_type="error",
|
935
|
+
pr_url=None
|
936
|
+
)
|
937
|
+
|
938
|
+
def get_conversation_history(self) -> List[Dict[str, Any]]:
|
939
|
+
"""Get conversation history from memory."""
|
940
|
+
return self.memory.get_conversation_history()
|
941
|
+
|
942
|
+
def get_memory_state(self) -> Dict[str, Any]:
|
943
|
+
"""Get current memory state."""
|
944
|
+
return self.memory.get_state()
|
945
|
+
|
946
|
+
def plan(self, query: str, **kwargs):
|
947
|
+
"""
|
948
|
+
Generates a plan for the agent to execute (required by AgentBase).
|
949
|
+
For GitAgent, the plan analyzes the DevOps request to predict routing.
|
950
|
+
|
951
|
+
Args:
|
952
|
+
query: The DevOps query to plan for
|
953
|
+
**kwargs: Additional parameters (e.g., thread_id)
|
954
|
+
|
955
|
+
Returns:
|
956
|
+
dict: A plan containing the input and predicted action
|
957
|
+
"""
|
958
|
+
self.logger.info(f"Planning for DevOps query: '{query}'")
|
959
|
+
|
960
|
+
plan = {
|
961
|
+
"input_query": query,
|
962
|
+
"predicted_action": "analyze_and_route",
|
963
|
+
"description": "Analyze DevOps request to determine appropriate tool routing"
|
964
|
+
}
|
965
|
+
|
966
|
+
# Simple analysis to predict the route
|
967
|
+
query_lower = query.lower()
|
968
|
+
if any(word in query_lower for word in ['clone', 'download repo', 'git clone']):
|
969
|
+
plan["predicted_route"] = "git_clone"
|
970
|
+
elif any(word in query_lower for word in ['open issue', 'create issue', 'report bug', 'issue']) or \
|
971
|
+
('open' in query_lower and 'issue' in query_lower):
|
972
|
+
plan["predicted_route"] = "github_issue"
|
973
|
+
elif any(word in query_lower for word in ['run command', 'execute', 'shell']):
|
974
|
+
plan["predicted_route"] = "shell_command"
|
975
|
+
else:
|
976
|
+
plan["predicted_route"] = "direct_response"
|
977
|
+
|
978
|
+
self.logger.debug(f"Generated plan: {plan}")
|
979
|
+
return plan
|
980
|
+
|
981
|
+
def report(self, result=None, **kwargs):
|
982
|
+
"""
|
983
|
+
Reports the results or progress of the agent's execution (required by AgentBase).
|
984
|
+
|
985
|
+
Args:
|
986
|
+
result: The result to report (GitAgentOutput or string)
|
987
|
+
**kwargs: Additional parameters
|
988
|
+
|
989
|
+
Returns:
|
990
|
+
dict: A report containing execution details
|
991
|
+
"""
|
992
|
+
if isinstance(result, GitAgentOutput):
|
993
|
+
report = {
|
994
|
+
"status": "completed",
|
995
|
+
"result": result.result,
|
996
|
+
"thread_id": result.thread_id,
|
997
|
+
"error": result.error_message,
|
998
|
+
"operation_type": result.operation_type,
|
999
|
+
"success": result.error_message is None
|
1000
|
+
}
|
1001
|
+
elif isinstance(result, str):
|
1002
|
+
report = {
|
1003
|
+
"status": "completed",
|
1004
|
+
"result": result,
|
1005
|
+
"success": True
|
1006
|
+
}
|
1007
|
+
else:
|
1008
|
+
report = {
|
1009
|
+
"status": "no_result",
|
1010
|
+
"message": "No result provided to report"
|
1011
|
+
}
|
1012
|
+
|
1013
|
+
self.logger.info(f"GitAgent execution report: {report}")
|
1014
|
+
return report
|
1015
|
+
|
1016
|
+
|
1017
|
+
# Alias for backward compatibility
|
1018
|
+
GitAgent = GitAgent
|