Coverage for agentos/evolution/engine.py: 37%

115 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-02 09:59 +0800

1""" 

2Evolution Engine for NexusAgent. 

3 

4Approval-based self-evolution system. Agents can propose 

5improvements, but changes require human approval before 

6being applied. 

7""" 

8 

9from __future__ import annotations 

10 

11import os 

12import time 

13import uuid 

14from dataclasses import dataclass, field 

15from enum import Enum 

16from typing import Any, Callable, Optional 

17 

18 

19class EvolutionStatus(str, Enum): 

20 """Status of an evolution proposal.""" 

21 PENDING = "pending" # Waiting for approval 

22 APPROVED = "approved" # Approved, ready to apply 

23 REJECTED = "rejected" # Rejected by human 

24 APPLIED = "applied" # Successfully applied 

25 FAILED = "failed" # Failed to apply 

26 

27 

28@dataclass 

29class EvolutionProposal: 

30 """ 

31 A proposed evolution/improvement. 

32 

33 Attributes: 

34 id: Unique identifier 

35 agent_name: Name of agent to evolve 

36 change_type: Type of change (prompt/tools/params) 

37 description: Human-readable description 

38 old_value: Current value 

39 new_value: Proposed new value 

40 status: Approval status 

41 created_at: Creation timestamp 

42 approved_at: Approval timestamp 

43 approved_by: Who approved 

44 applied_at: Application timestamp 

45 metadata: Additional metadata 

46 """ 

47 id: str = field(default_factory=lambda: uuid.uuid4().hex[:12]) 

48 agent_name: str = "" 

49 change_type: str = "prompt" # prompt, tools, params, behavior 

50 description: str = "" 

51 old_value: Any = None 

52 new_value: Any = None 

53 confidence: float = 0.0 

54 risk_level: str = "low" 

55 target_files: list[str] = field(default_factory=list) 

56 status: EvolutionStatus = EvolutionStatus.PENDING 

57 created_at: float = field(default_factory=time.time) 

58 approved_at: Optional[float] = None 

59 approved_by: Optional[str] = None 

60 applied_at: Optional[float] = None 

61 metadata: dict[str, Any] = field(default_factory=dict) 

62 

63 def to_dict(self) -> dict[str, Any]: 

64 """Convert to dict.""" 

65 return { 

66 "id": self.id, 

67 "agent_name": self.agent_name, 

68 "change_type": self.change_type, 

69 "description": self.description, 

70 "old_value": self.old_value, 

71 "new_value": self.new_value, 

72 "status": self.status.value, 

73 "created_at": self.created_at, 

74 "approved_at": self.approved_at, 

75 "approved_by": self.approved_by, 

76 "applied_at": self.applied_at, 

77 "metadata": self.metadata, 

78 } 

79 

80 

81class EvolutionEngine: 

82 """ 

83 Approval-based self-evolution engine. 

84 

85 Manages the lifecycle of evolution proposals: 

86 1. Agent proposes improvement 

87 2. Human reviews and approves/rejects 

88 3. Approved changes are applied 

89 

90 Usage: 

91 engine = EvolutionEngine() 

92 

93 # Agent proposes improvement 

94 proposal = engine.propose( 

95 agent_name="SupportAgent", 

96 change_type="prompt", 

97 description="Improve greeting", 

98 old_value="Hello!", 

99 new_value="Hi there! How can I help?", 

100 ) 

101 

102 # Human approves 

103 engine.approve(proposal.id, approved_by="human") 

104 

105 # Apply changes 

106 engine.apply(proposal.id) 

107 """ 

108 

109 def __init__(self): 

110 """Initialize evolution engine.""" 

111 self._proposals: dict[str, EvolutionProposal] = {} 

112 self._approvers: dict[str, Callable[[EvolutionProposal], bool]] = {} 

113 

114 def propose( 

115 self, 

116 agent_name: str, 

117 change_type: str, 

118 description: str, 

119 old_value: Any = None, 

120 new_value: Any = None, 

121 confidence: float = 0.0, 

122 risk_level: str = "low", 

123 target_files: Optional[list[str]] = None, 

124 **metadata 

125 ) -> EvolutionProposal: 

126 """ 

127 Create a new evolution proposal. 

128 

129 Args: 

130 agent_name: Name of agent to evolve 

131 change_type: Type of change 

132 description: Human-readable description 

133 old_value: Current value 

134 new_value: Proposed new value 

135 confidence: Confidence score (0-1) 

136 risk_level: Risk assessment (low/medium/high) 

137 target_files: Files to modify 

138 **metadata: Additional metadata 

139 

140 Returns: 

141 Created EvolutionProposal 

142 """ 

143 proposal = EvolutionProposal( 

144 agent_name=agent_name, 

145 change_type=change_type, 

146 description=description, 

147 old_value=old_value, 

148 new_value=new_value, 

149 confidence=confidence, 

150 risk_level=risk_level, 

151 target_files=target_files or [], 

152 metadata=metadata, 

153 ) 

154 

155 self._proposals[proposal.id] = proposal 

156 

157 return proposal 

158 

159 def get_proposal(self, proposal_id: str) -> Optional[EvolutionProposal]: 

160 """ 

161 Get a proposal by ID. 

162 

163 Args: 

164 proposal_id: Proposal ID 

165 

166 Returns: 

167 EvolutionProposal if found, None otherwise 

168 """ 

169 return self._proposals.get(proposal_id) 

170 

171 def list_proposals( 

172 self, 

173 status: Optional[EvolutionStatus] = None, 

174 agent_name: Optional[str] = None, 

175 ) -> list[EvolutionProposal]: 

176 """ 

177 List proposals. 

178 

179 Args: 

180 status: Filter by status 

181 agent_name: Filter by agent name 

182 

183 Returns: 

184 List of matching proposals 

185 """ 

186 proposals = list(self._proposals.values()) 

187 

188 if status: 

189 proposals = [p for p in proposals if p.status == status] 

190 

191 if agent_name: 

192 proposals = [p for p in proposals if p.agent_name == agent_name] 

193 

194 return proposals 

195 

196 def approve( 

197 self, 

198 proposal_id: str, 

199 approved_by: str = "human", 

200 ) -> bool: 

201 """ 

202 Approve a proposal. 

203 

204 Args: 

205 proposal_id: Proposal ID 

206 approved_by: Who approved 

207 

208 Returns: 

209 True if approved, False if not found 

210 """ 

211 proposal = self._proposals.get(proposal_id) 

212 if not proposal: 

213 return False 

214 

215 if proposal.status != EvolutionStatus.PENDING: 

216 return False 

217 

218 proposal.status = EvolutionStatus.APPROVED 

219 proposal.approved_at = time.time() 

220 proposal.approved_by = approved_by 

221 

222 return True 

223 

224 def reject( 

225 self, 

226 proposal_id: str, 

227 reason: str = "", 

228 ) -> bool: 

229 """ 

230 Reject a proposal. 

231 

232 Args: 

233 proposal_id: Proposal ID 

234 reason: Reason for rejection 

235 

236 Returns: 

237 True if rejected, False if not found 

238 """ 

239 proposal = self._proposals.get(proposal_id) 

240 if not proposal: 

241 return False 

242 

243 if proposal.status != EvolutionStatus.PENDING: 

244 return False 

245 

246 proposal.status = EvolutionStatus.REJECTED 

247 proposal.metadata["rejection_reason"] = reason 

248 

249 return True 

250 

251 def apply(self, proposal_id: str) -> bool: 

252 """ 

253 Apply an approved proposal. Performs actual file modifications. 

254 

255 Args: 

256 proposal_id: Proposal ID 

257 

258 Returns: 

259 True if applied, False otherwise 

260 """ 

261 proposal = self._proposals.get(proposal_id) 

262 if not proposal: 

263 return False 

264 

265 if proposal.status != EvolutionStatus.APPROVED: 

266 return False 

267 

268 try: 

269 target_files = proposal.target_files or [] 

270 old_val = proposal.old_value 

271 new_val = proposal.new_value 

272 

273 if target_files and old_val is not None and new_val is not None: 

274 for fpath in target_files: 

275 if not os.path.exists(fpath): 

276 continue 

277 with open(fpath, "r") as f: 

278 content = f.read() 

279 old_str = str(old_val) 

280 new_str = str(new_val) 

281 if old_str in content: 

282 content = content.replace(old_str, new_str, 1) 

283 with open(fpath, "w") as f: 

284 f.write(content) 

285 proposal.metadata["modified_file"] = fpath 

286 

287 proposal.status = EvolutionStatus.APPLIED 

288 proposal.applied_at = time.time() 

289 return True 

290 except Exception as e: 

291 proposal.status = EvolutionStatus.FAILED 

292 proposal.metadata["error"] = str(e) 

293 return False 

294 

295 def register_approver( 

296 self, 

297 agent_name: str, 

298 approver: Callable[[EvolutionProposal], bool], 

299 ) -> None: 

300 """ 

301 Register an approver for an agent. 

302 

303 Args: 

304 agent_name: Agent name 

305 approver: Approval function 

306 """ 

307 self._approvers[agent_name] = approver 

308 

309 def auto_approve(self, proposal_id: str) -> bool: 

310 """ 

311 Auto-approve using registered approver. 

312 

313 Args: 

314 proposal_id: Proposal ID 

315 

316 Returns: 

317 True if approved, False otherwise 

318 """ 

319 proposal = self._proposals.get(proposal_id) 

320 if not proposal: 

321 return False 

322 

323 approver = self._approvers.get(proposal.agent_name) 

324 if not approver: 

325 return False 

326 

327 if approver(proposal): 

328 return self.approve(proposal_id, approved_by="auto") 

329 

330 return False 

331 

332 def get_stats(self) -> dict[str, Any]: 

333 """ 

334 Get evolution statistics. 

335 

336 Returns: 

337 Dict with proposal counts by status 

338 """ 

339 stats = { 

340 "pending": 0, 

341 "approved": 0, 

342 "rejected": 0, 

343 "applied": 0, 

344 "failed": 0, 

345 "total": len(self._proposals), 

346 } 

347 

348 for proposal in self._proposals.values(): 

349 stats[proposal.status.value] += 1 

350 

351 return stats