devloop 0.2.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 (55) hide show
  1. devloop/__init__.py +3 -0
  2. devloop/agents/__init__.py +33 -0
  3. devloop/agents/agent_health_monitor.py +105 -0
  4. devloop/agents/ci_monitor.py +237 -0
  5. devloop/agents/code_rabbit.py +248 -0
  6. devloop/agents/doc_lifecycle.py +374 -0
  7. devloop/agents/echo.py +24 -0
  8. devloop/agents/file_logger.py +46 -0
  9. devloop/agents/formatter.py +511 -0
  10. devloop/agents/git_commit_assistant.py +421 -0
  11. devloop/agents/linter.py +399 -0
  12. devloop/agents/performance_profiler.py +284 -0
  13. devloop/agents/security_scanner.py +322 -0
  14. devloop/agents/snyk.py +292 -0
  15. devloop/agents/test_runner.py +484 -0
  16. devloop/agents/type_checker.py +242 -0
  17. devloop/cli/__init__.py +1 -0
  18. devloop/cli/commands/__init__.py +1 -0
  19. devloop/cli/commands/custom_agents.py +144 -0
  20. devloop/cli/commands/feedback.py +161 -0
  21. devloop/cli/commands/summary.py +50 -0
  22. devloop/cli/main.py +430 -0
  23. devloop/cli/main_v1.py +144 -0
  24. devloop/collectors/__init__.py +17 -0
  25. devloop/collectors/base.py +55 -0
  26. devloop/collectors/filesystem.py +126 -0
  27. devloop/collectors/git.py +171 -0
  28. devloop/collectors/manager.py +159 -0
  29. devloop/collectors/process.py +221 -0
  30. devloop/collectors/system.py +195 -0
  31. devloop/core/__init__.py +21 -0
  32. devloop/core/agent.py +206 -0
  33. devloop/core/agent_template.py +498 -0
  34. devloop/core/amp_integration.py +166 -0
  35. devloop/core/auto_fix.py +224 -0
  36. devloop/core/config.py +272 -0
  37. devloop/core/context.py +0 -0
  38. devloop/core/context_store.py +530 -0
  39. devloop/core/contextual_feedback.py +311 -0
  40. devloop/core/custom_agent.py +439 -0
  41. devloop/core/debug_trace.py +289 -0
  42. devloop/core/event.py +105 -0
  43. devloop/core/event_store.py +316 -0
  44. devloop/core/feedback.py +311 -0
  45. devloop/core/learning.py +351 -0
  46. devloop/core/manager.py +219 -0
  47. devloop/core/performance.py +433 -0
  48. devloop/core/proactive_feedback.py +302 -0
  49. devloop/core/summary_formatter.py +159 -0
  50. devloop/core/summary_generator.py +275 -0
  51. devloop-0.2.0.dist-info/METADATA +705 -0
  52. devloop-0.2.0.dist-info/RECORD +55 -0
  53. devloop-0.2.0.dist-info/WHEEL +4 -0
  54. devloop-0.2.0.dist-info/entry_points.txt +3 -0
  55. devloop-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,195 @@
1
+ """System event collector using psutil for resource monitoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from typing import Any, Dict, Optional
8
+
9
+ try:
10
+ import psutil
11
+
12
+ HAS_PSUTIL = True
13
+ except ImportError:
14
+ HAS_PSUTIL = False
15
+ psutil = None
16
+
17
+ from devloop.collectors.base import BaseCollector
18
+
19
+
20
+ class SystemCollector(BaseCollector):
21
+ """Collects system-related events like resource usage and idle time."""
22
+
23
+ def __init__(
24
+ self,
25
+ event_bus: Any, # EventBus type (avoiding circular import)
26
+ config: Optional[Dict[str, Any]] = None,
27
+ ):
28
+ super().__init__("system", event_bus, config)
29
+
30
+ if not HAS_PSUTIL:
31
+ self.logger.warning("psutil not available - system monitoring disabled")
32
+ self._psutil_available = False
33
+ else:
34
+ self._psutil_available = True
35
+
36
+ # Configuration
37
+ self.check_interval = self.config.get("check_interval", 30) # seconds
38
+ self.cpu_threshold = self.config.get("cpu_threshold", 80) # percent
39
+ self.memory_threshold = self.config.get("memory_threshold", 85) # percent
40
+ self.idle_threshold = self.config.get("idle_threshold", 300) # seconds
41
+
42
+ # State tracking
43
+ self._last_cpu_percent = 0
44
+ self._last_memory_percent = 0
45
+ self._last_idle_time = time.time()
46
+ self._is_idle = False
47
+ self._monitoring_task: Optional[asyncio.Task] = None
48
+
49
+ def _get_system_stats(self) -> Dict[str, Any]:
50
+ """Get current system statistics."""
51
+ if not self._psutil_available:
52
+ return {}
53
+
54
+ try:
55
+ return {
56
+ "cpu_percent": psutil.cpu_percent(interval=1),
57
+ "memory_percent": psutil.virtual_memory().percent,
58
+ "memory_used": psutil.virtual_memory().used,
59
+ "memory_total": psutil.virtual_memory().total,
60
+ "disk_usage": psutil.disk_usage("/").percent,
61
+ "load_average": (
62
+ psutil.getloadavg() if hasattr(psutil, "getloadavg") else None
63
+ ),
64
+ "timestamp": time.time(),
65
+ }
66
+ except Exception as e:
67
+ self.logger.error(f"Error getting system stats: {e}")
68
+ return {}
69
+
70
+ async def _check_system_resources(self) -> None:
71
+ """Check system resources and emit events if thresholds are exceeded."""
72
+ if not self._psutil_available:
73
+ return
74
+
75
+ stats = self._get_system_stats()
76
+ if not stats:
77
+ return
78
+
79
+ # Check CPU usage
80
+ cpu_percent = stats["cpu_percent"]
81
+ if (
82
+ cpu_percent > self.cpu_threshold
83
+ and self._last_cpu_percent <= self.cpu_threshold
84
+ ):
85
+ await self._emit_event(
86
+ "system:high_cpu",
87
+ {
88
+ "cpu_percent": cpu_percent,
89
+ "threshold": self.cpu_threshold,
90
+ "timestamp": stats["timestamp"],
91
+ },
92
+ "high",
93
+ "system",
94
+ )
95
+
96
+ self._last_cpu_percent = cpu_percent
97
+
98
+ # Check memory usage
99
+ memory_percent = stats["memory_percent"]
100
+ if (
101
+ memory_percent > self.memory_threshold
102
+ and self._last_memory_percent <= self.memory_threshold
103
+ ):
104
+ await self._emit_event(
105
+ "system:low_memory",
106
+ {
107
+ "memory_percent": memory_percent,
108
+ "memory_used": stats["memory_used"],
109
+ "memory_total": stats["memory_total"],
110
+ "threshold": self.memory_threshold,
111
+ "timestamp": stats["timestamp"],
112
+ },
113
+ "critical",
114
+ "system",
115
+ )
116
+
117
+ self._last_memory_percent = memory_percent
118
+
119
+ # Check idle time (simplified - in a real implementation you'd use more sophisticated idle detection)
120
+ current_time = time.time()
121
+ # For demo purposes, consider system idle if no significant CPU usage for a period
122
+ if cpu_percent < 5: # Very low CPU usage
123
+ if not self._is_idle:
124
+ idle_duration = current_time - self._last_idle_time
125
+ if idle_duration > self.idle_threshold:
126
+ await self._emit_event(
127
+ "system:idle",
128
+ {"idle_duration": idle_duration, "timestamp": current_time},
129
+ "normal",
130
+ "system",
131
+ )
132
+ self._is_idle = True
133
+ else:
134
+ if self._is_idle:
135
+ await self._emit_event(
136
+ "system:active", {"timestamp": current_time}, "normal", "system"
137
+ )
138
+ self._is_idle = False
139
+ self._last_idle_time = current_time
140
+
141
+ async def _monitor_system(self) -> None:
142
+ """Main monitoring loop."""
143
+ self.logger.info(
144
+ f"Starting system monitoring (interval: {self.check_interval}s)"
145
+ )
146
+
147
+ while self.is_running:
148
+ try:
149
+ await self._check_system_resources()
150
+ await asyncio.sleep(self.check_interval)
151
+ except Exception as e:
152
+ self.logger.error(f"Error in system monitoring loop: {e}")
153
+ await asyncio.sleep(self.check_interval)
154
+
155
+ async def start(self) -> None:
156
+ """Start the system collector."""
157
+ if self.is_running:
158
+ return
159
+
160
+ if not self._psutil_available:
161
+ self.logger.error("Cannot start system collector - psutil not available")
162
+ return
163
+
164
+ self._set_running(True)
165
+
166
+ # Start monitoring task
167
+ self._monitoring_task = asyncio.create_task(self._monitor_system())
168
+
169
+ # Emit initial system status
170
+ stats = self._get_system_stats()
171
+ if stats:
172
+ await self._emit_event("system:status", stats, "normal", "system")
173
+
174
+ self.logger.info("System collector started")
175
+
176
+ async def stop(self) -> None:
177
+ """Stop the system collector."""
178
+ if not self.is_running:
179
+ return
180
+
181
+ self._set_running(False)
182
+
183
+ # Cancel monitoring task
184
+ if self._monitoring_task:
185
+ self._monitoring_task.cancel()
186
+ try:
187
+ await self._monitoring_task
188
+ except asyncio.CancelledError:
189
+ pass
190
+
191
+ self.logger.info("System collector stopped")
192
+
193
+ async def emit_system_event(self, event_type: str, payload: Dict[str, Any]) -> None:
194
+ """Manually emit a system event (for testing or external triggers)."""
195
+ await self._emit_event(f"system:{event_type}", payload, "normal", "system")
@@ -0,0 +1,21 @@
1
+ """Core framework components."""
2
+
3
+ from .agent import Agent, AgentResult
4
+ from .config import Config, ConfigWrapper
5
+ from .context_store import context_store
6
+ from .event import Event, EventBus, Priority
7
+ from .event_store import event_store
8
+ from .manager import AgentManager
9
+
10
+ __all__ = [
11
+ "Agent",
12
+ "AgentResult",
13
+ "Config",
14
+ "ConfigWrapper",
15
+ "context_store",
16
+ "Event",
17
+ "EventBus",
18
+ "event_store",
19
+ "Priority",
20
+ "AgentManager",
21
+ ]
devloop/core/agent.py ADDED
@@ -0,0 +1,206 @@
1
+ """Base agent class with performance monitoring and feedback support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from .event import Event, EventBus
13
+ from .feedback import FeedbackAPI
14
+ from .performance import PerformanceMonitor
15
+
16
+
17
+ @dataclass
18
+ class AgentResult:
19
+ """Agent execution result."""
20
+
21
+ agent_name: str
22
+ success: bool
23
+ duration: float
24
+ message: str = ""
25
+ data: Dict[str, Any] | None = None
26
+ error: str | None = None
27
+
28
+ def __post_init__(self):
29
+ """Validate AgentResult parameters."""
30
+ # Validate agent_name
31
+ if not isinstance(self.agent_name, str):
32
+ raise TypeError(
33
+ f"agent_name must be a string, got {type(self.agent_name).__name__}"
34
+ )
35
+ if not self.agent_name:
36
+ raise ValueError("agent_name cannot be empty")
37
+
38
+ # Validate success
39
+ if not isinstance(self.success, bool):
40
+ raise TypeError(
41
+ f"success must be a boolean, got {type(self.success).__name__}"
42
+ )
43
+
44
+ # Validate duration
45
+ if not isinstance(self.duration, (int, float)):
46
+ raise TypeError(
47
+ f"duration must be a number, got {type(self.duration).__name__}. "
48
+ "Did you forget to include duration parameter in AgentResult creation?"
49
+ )
50
+ if self.duration < 0:
51
+ raise ValueError(f"duration must be non-negative, got {self.duration}")
52
+
53
+ # Validate message
54
+ if not isinstance(self.message, str):
55
+ raise TypeError(
56
+ f"message must be a string, got {type(self.message).__name__}"
57
+ )
58
+
59
+ # Validate data
60
+ if self.data is not None and not isinstance(self.data, dict):
61
+ raise TypeError(
62
+ f"data must be a dict or None, got {type(self.data).__name__}"
63
+ )
64
+
65
+ # Validate error
66
+ if self.error is not None and not isinstance(self.error, str):
67
+ raise TypeError(
68
+ f"error must be a string or None, got {type(self.error).__name__}"
69
+ )
70
+
71
+
72
+ class Agent(ABC):
73
+ """Base agent class with performance monitoring and feedback."""
74
+
75
+ def __init__(
76
+ self,
77
+ name: str,
78
+ triggers: List[str],
79
+ event_bus: EventBus,
80
+ feedback_api: Optional[FeedbackAPI] = None,
81
+ performance_monitor: Optional[PerformanceMonitor] = None,
82
+ ):
83
+ self.name = name
84
+ self.triggers = triggers
85
+ self.event_bus = event_bus
86
+ self.feedback_api = feedback_api
87
+ self.performance_monitor = performance_monitor
88
+ self.enabled = True
89
+ self.logger = logging.getLogger(f"agent.{name}")
90
+ self._running = False
91
+ self._event_queue: asyncio.Queue[Event] = asyncio.Queue()
92
+
93
+ @abstractmethod
94
+ async def handle(self, event: Event) -> AgentResult:
95
+ """Handle an event. Must be implemented by subclasses."""
96
+ pass
97
+
98
+ async def start(self) -> None:
99
+ """Start the agent."""
100
+ if self._running:
101
+ return
102
+
103
+ self._running = True
104
+
105
+ # Subscribe to configured triggers
106
+ for trigger in self.triggers:
107
+ await self.event_bus.subscribe(trigger, self._event_queue)
108
+
109
+ # Start event processing loop
110
+ asyncio.create_task(self._process_events())
111
+ self.logger.info(f"Agent {self.name} started, listening to {self.triggers}")
112
+
113
+ async def stop(self) -> None:
114
+ """Stop the agent."""
115
+ if not self._running:
116
+ return
117
+
118
+ self._running = False
119
+
120
+ # Unsubscribe from events
121
+ for trigger in self.triggers:
122
+ await self.event_bus.unsubscribe(trigger, self._event_queue)
123
+
124
+ self.logger.info(f"Agent {self.name} stopped")
125
+
126
+ async def _process_events(self) -> None:
127
+ """Process events from the queue with performance monitoring."""
128
+ while self._running:
129
+ try:
130
+ # Wait for event with timeout to allow checking _running
131
+ event = await asyncio.wait_for(self._event_queue.get(), timeout=1.0)
132
+ except asyncio.TimeoutError:
133
+ continue
134
+
135
+ if not self.enabled:
136
+ continue
137
+
138
+ # Execute handler with performance monitoring
139
+ try:
140
+ operation_name = f"agent.{self.name}.handle"
141
+
142
+ if self.performance_monitor:
143
+ async with self.performance_monitor.monitor_operation(
144
+ operation_name,
145
+ metadata={"event_type": event.type, "agent_name": self.name},
146
+ ) as metrics:
147
+ result = await self.handle(event)
148
+ metrics.complete(result.success, result.error)
149
+
150
+ # Update result duration from metrics
151
+ if metrics.duration:
152
+ result.duration = metrics.duration
153
+ else:
154
+ start_time = time.time()
155
+ result = await self.handle(event)
156
+ result.duration = time.time() - start_time
157
+
158
+ # Update performance store if available
159
+ if self.feedback_api:
160
+ await self.feedback_api.feedback_store.update_performance(
161
+ self.name, result.success, result.duration
162
+ )
163
+
164
+ # Publish result
165
+ await self._publish_result(result)
166
+
167
+ # Log result
168
+ status = "✓" if result.success else "✗"
169
+ self.logger.info(
170
+ f"{status} {self.name}: {result.message} ({result.duration:.2f}s)"
171
+ )
172
+
173
+ except Exception as e:
174
+ self.logger.error(f"Error in {self.name}: {e}", exc_info=True)
175
+
176
+ error_result = AgentResult(
177
+ agent_name=self.name,
178
+ success=False,
179
+ duration=0.1, # Default duration for errors
180
+ error=str(e),
181
+ )
182
+
183
+ # Update performance store for failed operations
184
+ if self.feedback_api:
185
+ await self.feedback_api.feedback_store.update_performance(
186
+ self.name, False, error_result.duration
187
+ )
188
+
189
+ await self._publish_result(error_result)
190
+
191
+ async def _publish_result(self, result: AgentResult) -> None:
192
+ """Publish agent result as an event."""
193
+ await self.event_bus.emit(
194
+ Event(
195
+ type=f"agent:{self.name}:completed",
196
+ payload={
197
+ "agent_name": result.agent_name,
198
+ "success": result.success,
199
+ "duration": result.duration,
200
+ "message": result.message,
201
+ "data": result.data,
202
+ "error": result.error,
203
+ },
204
+ source=self.name,
205
+ )
206
+ )