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,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
@@ -0,0 +1,7 @@
1
+ from .issue_tracker import IssueTracker
2
+ from .errors import MissingSecretError
3
+
4
+ __all__ = [
5
+ "IssueTracker",
6
+ "MissingSecretError",
7
+ ]
@@ -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,4 @@
1
+ class MissingSecretError(RuntimeError):
2
+ """Raised when a required secret environment variable is missing."""
3
+
4
+
@@ -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
@@ -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,10 @@
1
+ from .observability import LogBus, log_event, get_log_path, reset_log_bus
2
+ from .step_summary import generate_step_summary
3
+
4
+ __all__ = [
5
+ "LogBus",
6
+ "log_event",
7
+ "get_log_path",
8
+ "reset_log_bus",
9
+ "generate_step_summary",
10
+ ]
@@ -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()