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,76 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Any, Set
|
3
|
+
|
4
|
+
|
5
|
+
def _flatten(obj: Any) -> str:
|
6
|
+
"""Recursively convert a JSON object into a space separated string."""
|
7
|
+
if isinstance(obj, dict):
|
8
|
+
return " ".join(_flatten(v) for v in obj.values())
|
9
|
+
if isinstance(obj, list):
|
10
|
+
return " ".join(_flatten(i) for i in obj)
|
11
|
+
return str(obj)
|
12
|
+
|
13
|
+
|
14
|
+
def classify_terraform_error(output: str) -> Set[str]:
|
15
|
+
"""Classify Terraform JSON error output into common categories.
|
16
|
+
|
17
|
+
Parameters
|
18
|
+
----------
|
19
|
+
output: str
|
20
|
+
Raw JSON error string returned by Terraform.
|
21
|
+
|
22
|
+
Returns
|
23
|
+
-------
|
24
|
+
Set[str]
|
25
|
+
A set of tags describing the error. Possible tags include
|
26
|
+
``syntax_fmt``, ``needs_pat``, ``missing_backend`` and
|
27
|
+
``policy_block``.
|
28
|
+
"""
|
29
|
+
text = output
|
30
|
+
try:
|
31
|
+
data = json.loads(output)
|
32
|
+
except json.JSONDecodeError:
|
33
|
+
# Try newline separated JSON objects
|
34
|
+
parts = []
|
35
|
+
for line in output.splitlines():
|
36
|
+
line = line.strip()
|
37
|
+
if not line:
|
38
|
+
continue
|
39
|
+
try:
|
40
|
+
parts.append(json.loads(line))
|
41
|
+
except json.JSONDecodeError:
|
42
|
+
continue
|
43
|
+
if parts:
|
44
|
+
text = " ".join(_flatten(p) for p in parts)
|
45
|
+
else:
|
46
|
+
text = output
|
47
|
+
else:
|
48
|
+
text = _flatten(data)
|
49
|
+
|
50
|
+
lowered = text.lower()
|
51
|
+
tags: Set[str] = set()
|
52
|
+
|
53
|
+
if any(
|
54
|
+
keyword in lowered for keyword in ["syntax", "invalid", "parse", "unexpected"]
|
55
|
+
):
|
56
|
+
tags.add("syntax_fmt")
|
57
|
+
|
58
|
+
if "token" in lowered and (
|
59
|
+
"unauthorized" in lowered
|
60
|
+
or "auth" in lowered
|
61
|
+
or "permission" in lowered
|
62
|
+
or "credential" in lowered
|
63
|
+
):
|
64
|
+
tags.add("needs_pat")
|
65
|
+
|
66
|
+
if "backend" in lowered and any(
|
67
|
+
k in lowered for k in ["required", "missing", "no", "not configured"]
|
68
|
+
):
|
69
|
+
tags.add("missing_backend")
|
70
|
+
|
71
|
+
if "policy" in lowered and any(
|
72
|
+
k in lowered for k in ["fail", "denied", "violation"]
|
73
|
+
):
|
74
|
+
tags.add("policy_block")
|
75
|
+
|
76
|
+
return tags
|
diagram_to_iac/core/__init__.py
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
class AgentBase(ABC):
|
4
|
+
"""Abstract base class for all agents."""
|
5
|
+
|
6
|
+
@abstractmethod
|
7
|
+
def plan(self, *args, **kwargs):
|
8
|
+
"""Generates a plan for the agent to execute."""
|
9
|
+
pass
|
10
|
+
|
11
|
+
@abstractmethod
|
12
|
+
def run(self, *args, **kwargs):
|
13
|
+
"""Executes the agent's plan or a given task."""
|
14
|
+
pass
|
15
|
+
|
16
|
+
@abstractmethod
|
17
|
+
def report(self, *args, **kwargs):
|
18
|
+
"""Reports the results or progress of the agent's execution."""
|
19
|
+
pass
|
@@ -0,0 +1,302 @@
|
|
1
|
+
from typing import Any, Dict, Optional, List
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
# Abstract base for memory implementations
|
8
|
+
class MemoryInterface(ABC):
|
9
|
+
"""Abstract interface for agent memory systems."""
|
10
|
+
|
11
|
+
@abstractmethod
|
12
|
+
def get_state(self) -> Dict[str, Any]:
|
13
|
+
"""Retrieve the current state."""
|
14
|
+
pass
|
15
|
+
|
16
|
+
@abstractmethod
|
17
|
+
def update_state(self, key: str, value: Any) -> None:
|
18
|
+
"""Update a specific key in the state."""
|
19
|
+
pass
|
20
|
+
|
21
|
+
@abstractmethod
|
22
|
+
def replace_state(self, new_state: Dict[str, Any]) -> None:
|
23
|
+
"""Replace the entire state with a new one."""
|
24
|
+
pass
|
25
|
+
|
26
|
+
@abstractmethod
|
27
|
+
def clear_state(self) -> None:
|
28
|
+
"""Clear the state."""
|
29
|
+
pass
|
30
|
+
|
31
|
+
@abstractmethod
|
32
|
+
def get_conversation_history(self) -> List[Dict[str, Any]]:
|
33
|
+
"""Get conversation history."""
|
34
|
+
pass
|
35
|
+
|
36
|
+
@abstractmethod
|
37
|
+
def add_to_conversation(self, role: str, content: str, metadata: Optional[Dict] = None) -> None:
|
38
|
+
"""Add a message to conversation history."""
|
39
|
+
pass
|
40
|
+
|
41
|
+
|
42
|
+
class InMemoryMemory(MemoryInterface):
|
43
|
+
"""
|
44
|
+
Simple in-memory implementation of the memory interface.
|
45
|
+
Data is lost when the process ends.
|
46
|
+
"""
|
47
|
+
|
48
|
+
def __init__(self):
|
49
|
+
self._state: Dict[str, Any] = {}
|
50
|
+
self._conversation_history: List[Dict[str, Any]] = []
|
51
|
+
|
52
|
+
def get_state(self) -> Dict[str, Any]:
|
53
|
+
"""Retrieve the current state."""
|
54
|
+
return self._state.copy()
|
55
|
+
|
56
|
+
def update_state(self, key: str, value: Any) -> None:
|
57
|
+
"""Update a specific key in the state."""
|
58
|
+
self._state[key] = value
|
59
|
+
|
60
|
+
def replace_state(self, new_state: Dict[str, Any]) -> None:
|
61
|
+
"""Replace the entire state with a new one."""
|
62
|
+
self._state = new_state.copy()
|
63
|
+
|
64
|
+
def clear_state(self) -> None:
|
65
|
+
"""Clear the state."""
|
66
|
+
self._state = {}
|
67
|
+
self._conversation_history = []
|
68
|
+
|
69
|
+
def get_conversation_history(self) -> List[Dict[str, Any]]:
|
70
|
+
"""Get conversation history."""
|
71
|
+
return self._conversation_history.copy()
|
72
|
+
|
73
|
+
def add_to_conversation(self, role: str, content: str, metadata: Optional[Dict] = None) -> None:
|
74
|
+
"""Add a message to conversation history."""
|
75
|
+
message = {
|
76
|
+
"role": role,
|
77
|
+
"content": content,
|
78
|
+
"timestamp": self._get_timestamp(),
|
79
|
+
"metadata": metadata or {}
|
80
|
+
}
|
81
|
+
self._conversation_history.append(message)
|
82
|
+
|
83
|
+
def _get_timestamp(self) -> str:
|
84
|
+
"""Get current timestamp as ISO string."""
|
85
|
+
from datetime import datetime
|
86
|
+
return datetime.now().isoformat()
|
87
|
+
|
88
|
+
|
89
|
+
class PersistentFileMemory(MemoryInterface):
|
90
|
+
"""
|
91
|
+
File-based memory implementation that persists state to disk.
|
92
|
+
Useful for maintaining state across agent restarts.
|
93
|
+
"""
|
94
|
+
|
95
|
+
def __init__(self, file_path: Optional[str] = None):
|
96
|
+
if file_path is None:
|
97
|
+
# Default to data/db directory
|
98
|
+
base_dir = Path(__file__).parent.parent.parent.parent
|
99
|
+
data_dir = base_dir / "data" / "db"
|
100
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
101
|
+
file_path = data_dir / "agent_memory.json"
|
102
|
+
|
103
|
+
self.file_path = Path(file_path)
|
104
|
+
self._state: Dict[str, Any] = {}
|
105
|
+
self._conversation_history: List[Dict[str, Any]] = []
|
106
|
+
self._load_data()
|
107
|
+
|
108
|
+
def _load_data(self) -> None:
|
109
|
+
"""Load state from the file."""
|
110
|
+
if self.file_path.exists():
|
111
|
+
try:
|
112
|
+
with open(self.file_path, 'r') as f:
|
113
|
+
data = json.load(f)
|
114
|
+
self._state = data.get('state', {})
|
115
|
+
self._conversation_history = data.get('conversation_history', [])
|
116
|
+
except (json.JSONDecodeError, KeyError) as e:
|
117
|
+
print(f"Warning: Could not load memory from {self.file_path}: {e}")
|
118
|
+
self._state = {}
|
119
|
+
self._conversation_history = []
|
120
|
+
else:
|
121
|
+
self._state = {}
|
122
|
+
self._conversation_history = []
|
123
|
+
|
124
|
+
def _save_data(self) -> None:
|
125
|
+
"""Save current state to the file."""
|
126
|
+
try:
|
127
|
+
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
128
|
+
data = {
|
129
|
+
'state': self._state,
|
130
|
+
'conversation_history': self._conversation_history
|
131
|
+
}
|
132
|
+
with open(self.file_path, 'w') as f:
|
133
|
+
json.dump(data, f, indent=2)
|
134
|
+
except Exception as e:
|
135
|
+
print(f"Warning: Could not save memory to {self.file_path}: {e}")
|
136
|
+
|
137
|
+
def get_state(self) -> Dict[str, Any]:
|
138
|
+
"""Retrieve the current state."""
|
139
|
+
return self._state.copy()
|
140
|
+
|
141
|
+
def update_state(self, key: str, value: Any) -> None:
|
142
|
+
"""Update a specific key in the state."""
|
143
|
+
self._state[key] = value
|
144
|
+
self._save_data()
|
145
|
+
|
146
|
+
def replace_state(self, new_state: Dict[str, Any]) -> None:
|
147
|
+
"""Replace the entire state with a new one."""
|
148
|
+
self._state = new_state.copy()
|
149
|
+
self._save_data()
|
150
|
+
|
151
|
+
def clear_state(self) -> None:
|
152
|
+
"""Clear the state and remove the file."""
|
153
|
+
self._state = {}
|
154
|
+
self._conversation_history = []
|
155
|
+
# Remove the file completely
|
156
|
+
if self.file_path.exists():
|
157
|
+
try:
|
158
|
+
self.file_path.unlink()
|
159
|
+
except Exception as e:
|
160
|
+
print(f"Warning: Could not remove memory file {self.file_path}: {e}")
|
161
|
+
|
162
|
+
def get_conversation_history(self) -> List[Dict[str, Any]]:
|
163
|
+
"""Get conversation history."""
|
164
|
+
return self._conversation_history.copy()
|
165
|
+
|
166
|
+
def add_to_conversation(self, role: str, content: str, metadata: Optional[Dict] = None) -> None:
|
167
|
+
"""Add a message to conversation history."""
|
168
|
+
message = {
|
169
|
+
"role": role,
|
170
|
+
"content": content,
|
171
|
+
"timestamp": self._get_timestamp(),
|
172
|
+
"metadata": metadata or {}
|
173
|
+
}
|
174
|
+
self._conversation_history.append(message)
|
175
|
+
self._save_data()
|
176
|
+
|
177
|
+
def _get_timestamp(self) -> str:
|
178
|
+
"""Get current timestamp as ISO string."""
|
179
|
+
from datetime import datetime
|
180
|
+
return datetime.now().isoformat()
|
181
|
+
|
182
|
+
|
183
|
+
class LangGraphMemoryAdapter(MemoryInterface):
|
184
|
+
"""
|
185
|
+
Adapter to integrate custom memory with LangGraph's checkpointer system.
|
186
|
+
Provides a bridge between our memory interface and LangGraph's state management.
|
187
|
+
"""
|
188
|
+
|
189
|
+
def __init__(self, base_memory: MemoryInterface):
|
190
|
+
self.base_memory = base_memory
|
191
|
+
|
192
|
+
def get_state(self) -> Dict[str, Any]:
|
193
|
+
"""Retrieve the current state."""
|
194
|
+
return self.base_memory.get_state()
|
195
|
+
|
196
|
+
def update_state(self, key: str, value: Any) -> None:
|
197
|
+
"""Update a specific key in the state."""
|
198
|
+
self.base_memory.update_state(key, value)
|
199
|
+
|
200
|
+
def replace_state(self, new_state: Dict[str, Any]) -> None:
|
201
|
+
"""Replace the entire state with a new one."""
|
202
|
+
self.base_memory.replace_state(new_state)
|
203
|
+
|
204
|
+
def clear_state(self) -> None:
|
205
|
+
"""Clear the state."""
|
206
|
+
self.base_memory.clear_state()
|
207
|
+
|
208
|
+
def get_conversation_history(self) -> List[Dict[str, Any]]:
|
209
|
+
"""Get conversation history."""
|
210
|
+
return self.base_memory.get_conversation_history()
|
211
|
+
|
212
|
+
def add_to_conversation(self, role: str, content: str, metadata: Optional[Dict] = None) -> None:
|
213
|
+
"""Add a message to conversation history."""
|
214
|
+
self.base_memory.add_to_conversation(role, content, metadata)
|
215
|
+
|
216
|
+
def sync_from_langgraph_state(self, langgraph_state: Dict[str, Any]) -> None:
|
217
|
+
"""
|
218
|
+
Sync state from LangGraph state format to memory.
|
219
|
+
|
220
|
+
Args:
|
221
|
+
langgraph_state: State dictionary from LangGraph
|
222
|
+
"""
|
223
|
+
# Store the entire LangGraph state
|
224
|
+
self.base_memory.update_state("langgraph_state", langgraph_state)
|
225
|
+
|
226
|
+
# Also extract and store specific components for convenience
|
227
|
+
if "tool_output" in langgraph_state and langgraph_state["tool_output"]:
|
228
|
+
self.base_memory.update_state("last_tool_outputs", langgraph_state["tool_output"])
|
229
|
+
|
230
|
+
# Add timestamp for sync tracking
|
231
|
+
from datetime import datetime
|
232
|
+
self.base_memory.update_state("last_sync", datetime.now().isoformat())
|
233
|
+
|
234
|
+
def sync_to_langgraph_state(self, base_langgraph_state: Dict[str, Any]) -> Dict[str, Any]:
|
235
|
+
"""
|
236
|
+
Sync state from our memory to LangGraph state format.
|
237
|
+
|
238
|
+
Args:
|
239
|
+
base_langgraph_state: Base LangGraph state to merge memory context into
|
240
|
+
|
241
|
+
Returns:
|
242
|
+
Updated LangGraph state with memory context
|
243
|
+
"""
|
244
|
+
# Start with the base state
|
245
|
+
updated_state = base_langgraph_state.copy()
|
246
|
+
|
247
|
+
# Get current memory state and conversation
|
248
|
+
state = self.get_state()
|
249
|
+
conversation = self.get_conversation_history()
|
250
|
+
|
251
|
+
# Create memory context
|
252
|
+
memory_context = {}
|
253
|
+
|
254
|
+
# Add conversation history to memory context
|
255
|
+
if conversation:
|
256
|
+
memory_context["conversation_history"] = conversation
|
257
|
+
|
258
|
+
# Add other memory state (excluding special keys)
|
259
|
+
for key, value in state.items():
|
260
|
+
if key not in ["langgraph_state", "last_sync"]:
|
261
|
+
memory_context[key] = value
|
262
|
+
|
263
|
+
# Add memory context to the updated state
|
264
|
+
updated_state["memory_context"] = memory_context
|
265
|
+
|
266
|
+
return updated_state
|
267
|
+
|
268
|
+
def get_checkpoint_metadata(self) -> Dict[str, Any]:
|
269
|
+
"""Get metadata for LangGraph checkpoint."""
|
270
|
+
return {
|
271
|
+
"conversation_length": len(self.get_conversation_history()),
|
272
|
+
"state_keys": list(self.get_state().keys()),
|
273
|
+
"last_updated": self.base_memory._get_timestamp() if hasattr(self.base_memory, '_get_timestamp') else None
|
274
|
+
}
|
275
|
+
|
276
|
+
|
277
|
+
# Factory function for easy memory creation
|
278
|
+
def create_memory(memory_type: str = "in_memory", **kwargs) -> MemoryInterface:
|
279
|
+
"""
|
280
|
+
Factory function to create memory instances.
|
281
|
+
|
282
|
+
Args:
|
283
|
+
memory_type: Type of memory to create ("in_memory", "memory", "persistent", or "langgraph")
|
284
|
+
**kwargs: Additional arguments for memory initialization
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
MemoryInterface: Configured memory instance
|
288
|
+
"""
|
289
|
+
if memory_type in ("in_memory", "memory"):
|
290
|
+
return InMemoryMemory()
|
291
|
+
elif memory_type == "persistent":
|
292
|
+
return PersistentFileMemory(**kwargs)
|
293
|
+
elif memory_type == "langgraph":
|
294
|
+
# Create LangGraph adapter with specified base memory type
|
295
|
+
base_memory_type = kwargs.pop('base_memory_type', 'in_memory')
|
296
|
+
if base_memory_type == "persistent":
|
297
|
+
base_memory = PersistentFileMemory(**kwargs)
|
298
|
+
else:
|
299
|
+
base_memory = InMemoryMemory()
|
300
|
+
return LangGraphMemoryAdapter(base_memory)
|
301
|
+
else:
|
302
|
+
raise ValueError(f"Unknown memory type: {memory_type}")
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import json
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Dict, Optional
|
4
|
+
|
5
|
+
class IssueTracker:
|
6
|
+
"""Simple persistent tracker for GitHub issues keyed by repo and error type."""
|
7
|
+
|
8
|
+
def __init__(self, file_path: Optional[str] = None):
|
9
|
+
if file_path is None:
|
10
|
+
base_dir = Path(__file__).resolve().parents[3]
|
11
|
+
file_path = base_dir / "data" / "db" / "issue_tracker.json"
|
12
|
+
self.file_path = Path(file_path)
|
13
|
+
self._table: Dict[str, Dict[str, int]] = {}
|
14
|
+
self._load()
|
15
|
+
|
16
|
+
def _load(self) -> None:
|
17
|
+
if self.file_path.exists():
|
18
|
+
try:
|
19
|
+
with open(self.file_path, "r") as f:
|
20
|
+
self._table = json.load(f)
|
21
|
+
except Exception:
|
22
|
+
self._table = {}
|
23
|
+
else:
|
24
|
+
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
25
|
+
self._table = {}
|
26
|
+
|
27
|
+
def _save(self) -> None:
|
28
|
+
try:
|
29
|
+
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
30
|
+
with open(self.file_path, "w") as f:
|
31
|
+
json.dump(self._table, f, indent=2)
|
32
|
+
except Exception:
|
33
|
+
pass
|
34
|
+
|
35
|
+
def get_issue(self, repo_url: str, error_type: str) -> Optional[int]:
|
36
|
+
return self._table.get(repo_url, {}).get(error_type)
|
37
|
+
|
38
|
+
def record_issue(self, repo_url: str, error_type: str, issue_id: int) -> None:
|
39
|
+
repo_map = self._table.setdefault(repo_url, {})
|
40
|
+
repo_map[error_type] = issue_id
|
41
|
+
self._save()
|
42
|
+
|
43
|
+
def clear(self) -> None:
|
44
|
+
self._table = {}
|
45
|
+
if self.file_path.exists():
|
46
|
+
try:
|
47
|
+
self.file_path.unlink()
|
48
|
+
except Exception:
|
49
|
+
pass
|
diagram_to_iac/core/memory.py
CHANGED
@@ -0,0 +1,132 @@
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
from subprocess import check_output, CalledProcessError
|
6
|
+
# Intended to be a thin wrapper over LangGraph's StateGraph node store.
|
7
|
+
# For now, a simple dictionary-based state.
|
8
|
+
|
9
|
+
class Memory:
|
10
|
+
"""
|
11
|
+
Thin wrapper over LangGraph’s StateGraph node store;
|
12
|
+
later could be swapped for Redis or another persistent store.
|
13
|
+
LangGraph nodes can read/write via graph.state.
|
14
|
+
This class provides a conceptual placeholder for that state management.
|
15
|
+
"""
|
16
|
+
def __init__(self):
|
17
|
+
self._state: Dict[str, Any] = {}
|
18
|
+
|
19
|
+
def get_state(self) -> Dict[str, Any]:
|
20
|
+
"""Retrieves the current state."""
|
21
|
+
return self._state
|
22
|
+
|
23
|
+
def update_state(self, key: str, value: Any) -> None:
|
24
|
+
"""Updates a specific key in the state."""
|
25
|
+
self._state[key] = value
|
26
|
+
|
27
|
+
def replace_state(self, new_state: Dict[str, Any]) -> None:
|
28
|
+
"""Replaces the entire state with a new one."""
|
29
|
+
self._state = new_state
|
30
|
+
|
31
|
+
def clear_state(self) -> None:
|
32
|
+
"""Clears the state."""
|
33
|
+
self._state = {}
|
34
|
+
|
35
|
+
# Enhanced memory system - import the new capabilities
|
36
|
+
from .enhanced_memory import (
|
37
|
+
MemoryInterface,
|
38
|
+
InMemoryMemory,
|
39
|
+
PersistentFileMemory,
|
40
|
+
LangGraphMemoryAdapter,
|
41
|
+
create_memory
|
42
|
+
)
|
43
|
+
|
44
|
+
# Add conversation tracking to the original Memory class
|
45
|
+
class EnhancedMemory(Memory):
|
46
|
+
"""
|
47
|
+
Enhanced version of the original Memory class with conversation tracking.
|
48
|
+
Provides backward compatibility while adding new features.
|
49
|
+
"""
|
50
|
+
|
51
|
+
def __init__(self):
|
52
|
+
super().__init__()
|
53
|
+
self._conversation_history: List[Dict[str, Any]] = []
|
54
|
+
|
55
|
+
def get_conversation_history(self) -> List[Dict[str, Any]]:
|
56
|
+
"""Get conversation history."""
|
57
|
+
return self._conversation_history.copy()
|
58
|
+
|
59
|
+
def add_to_conversation(self, role: str, content: str, metadata: Optional[Dict] = None) -> None:
|
60
|
+
"""Add a message to conversation history."""
|
61
|
+
from datetime import datetime
|
62
|
+
message = {
|
63
|
+
"role": role,
|
64
|
+
"content": content,
|
65
|
+
"timestamp": datetime.now().isoformat(),
|
66
|
+
"metadata": metadata or {}
|
67
|
+
}
|
68
|
+
self._conversation_history.append(message)
|
69
|
+
|
70
|
+
def clear_state(self) -> None:
|
71
|
+
"""Clear both state and conversation history."""
|
72
|
+
super().clear_state()
|
73
|
+
self._conversation_history = []
|
74
|
+
|
75
|
+
# Example of how it might be used with LangGraph (conceptual)
|
76
|
+
# from langgraph.graph import StateGraph
|
77
|
+
#
|
78
|
+
# class AgentState(TypedDict):
|
79
|
+
# input_query: str
|
80
|
+
# processed_data: Any
|
81
|
+
# final_result: str
|
82
|
+
#
|
83
|
+
# graph = StateGraph(AgentState)
|
84
|
+
# memory_instance = Memory() # This would be integrated with the graph's actual state mechanism
|
85
|
+
|
86
|
+
# --- Agent state persistence helpers ---
|
87
|
+
|
88
|
+
# Default path for the persistent agent state JSON file. Allows override via
|
89
|
+
# `AGENT_STATE_FILE` environment variable for testing.
|
90
|
+
_DEFAULT_AGENT_STATE_PATH = (
|
91
|
+
Path(__file__).resolve().parents[3]
|
92
|
+
/ "state"
|
93
|
+
/ ".agent_state"
|
94
|
+
/ "agent_state.json"
|
95
|
+
)
|
96
|
+
AGENT_STATE_PATH = Path(os.environ.get("AGENT_STATE_FILE", _DEFAULT_AGENT_STATE_PATH))
|
97
|
+
|
98
|
+
|
99
|
+
def agent_state_enabled() -> bool:
|
100
|
+
"""Return ``True`` if persistent agent state should be used."""
|
101
|
+
return os.environ.get("AGENT_STATE_ENABLED", "0") == "1"
|
102
|
+
|
103
|
+
|
104
|
+
def load_agent_state() -> Dict[str, Any]:
|
105
|
+
"""Load agent state from ``AGENT_STATE_PATH`` if it exists and persistence is enabled."""
|
106
|
+
if not agent_state_enabled():
|
107
|
+
return {}
|
108
|
+
if AGENT_STATE_PATH.exists():
|
109
|
+
try:
|
110
|
+
with open(AGENT_STATE_PATH, "r") as f:
|
111
|
+
return json.load(f)
|
112
|
+
except json.JSONDecodeError:
|
113
|
+
return {}
|
114
|
+
return {}
|
115
|
+
|
116
|
+
|
117
|
+
def save_agent_state(state: Dict[str, Any]) -> None:
|
118
|
+
"""Persist agent state to ``AGENT_STATE_PATH`` if persistence is enabled."""
|
119
|
+
if not agent_state_enabled():
|
120
|
+
return
|
121
|
+
AGENT_STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
122
|
+
with open(AGENT_STATE_PATH, "w") as f:
|
123
|
+
json.dump(state, f, indent=2)
|
124
|
+
|
125
|
+
|
126
|
+
def current_git_sha() -> Optional[str]:
|
127
|
+
"""Return the current git commit SHA, or ``None`` if unavailable."""
|
128
|
+
try:
|
129
|
+
return check_output(["git", "rev-parse", "HEAD"], text=True).strip()
|
130
|
+
except Exception:
|
131
|
+
return None
|
132
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
from datetime import datetime, timezone
|
5
|
+
from pathlib import Path
|
6
|
+
import threading
|
7
|
+
from typing import Any, Dict
|
8
|
+
|
9
|
+
|
10
|
+
class LogBus:
|
11
|
+
"""Simple JSONL logging service."""
|
12
|
+
|
13
|
+
def __init__(self, log_dir: str | Path | None = None) -> None:
|
14
|
+
self.log_dir = Path(log_dir) if log_dir else Path(__file__).resolve().parents[3] / "logs"
|
15
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
16
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
17
|
+
self.log_path = self.log_dir / f"run-{timestamp}.jsonl"
|
18
|
+
self._lock = threading.Lock()
|
19
|
+
|
20
|
+
def log(self, event: Dict[str, Any]) -> None:
|
21
|
+
"""Append an event as a JSON line with timestamp."""
|
22
|
+
payload = event.copy()
|
23
|
+
payload.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
|
24
|
+
line = json.dumps(payload)
|
25
|
+
with self._lock:
|
26
|
+
try:
|
27
|
+
with open(self.log_path, "a", encoding="utf-8") as f:
|
28
|
+
f.write(line + "\n")
|
29
|
+
except Exception as exc: # noqa: BLE001
|
30
|
+
# Logging should never raise; print error and continue
|
31
|
+
print(f"LogBus write failed: {exc}")
|
32
|
+
|
33
|
+
|
34
|
+
_global_bus: LogBus | None = None
|
35
|
+
|
36
|
+
|
37
|
+
def _get_bus() -> LogBus:
|
38
|
+
global _global_bus
|
39
|
+
if _global_bus is None:
|
40
|
+
_global_bus = LogBus()
|
41
|
+
return _global_bus
|
42
|
+
|
43
|
+
|
44
|
+
def log_event(event_type: str, **kwargs: Any) -> None:
|
45
|
+
"""Write a structured log event using the global bus."""
|
46
|
+
event = {"type": event_type, **kwargs}
|
47
|
+
_get_bus().log(event)
|
48
|
+
|
49
|
+
|
50
|
+
def get_log_path() -> Path:
|
51
|
+
"""Return the path of the current log file."""
|
52
|
+
return _get_bus().log_path
|
53
|
+
|
54
|
+
|
55
|
+
def reset_log_bus() -> None:
|
56
|
+
"""Create a fresh global log bus with a new log file."""
|
57
|
+
global _global_bus
|
58
|
+
# Nothing to close since LogBus opens files on demand
|
59
|
+
_global_bus = LogBus()
|