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

116 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-09 07:12 +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 collections.abc import Callable 

15from dataclasses import dataclass, field 

16from enum import StrEnum 

17from typing import Any 

18 

19 

20class EvolutionStatus(StrEnum): 

21 """Status of an evolution proposal.""" 

22 

23 PENDING = "pending" # Waiting for approval 

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

25 REJECTED = "rejected" # Rejected by human 

26 APPLIED = "applied" # Successfully applied 

27 FAILED = "failed" # Failed to apply 

28 

29 

30@dataclass 

31class EvolutionProposal: 

32 """ 

33 A proposed evolution/improvement. 

34 

35 Attributes: 

36 id: Unique identifier 

37 agent_name: Name of agent to evolve 

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

39 description: Human-readable description 

40 old_value: Current value 

41 new_value: Proposed new value 

42 status: Approval status 

43 created_at: Creation timestamp 

44 approved_at: Approval timestamp 

45 approved_by: Who approved 

46 applied_at: Application timestamp 

47 metadata: Additional metadata 

48 """ 

49 

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

51 agent_name: str = "" 

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

53 description: str = "" 

54 old_value: Any = None 

55 new_value: Any = None 

56 confidence: float = 0.0 

57 risk_level: str = "low" 

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

59 status: EvolutionStatus = EvolutionStatus.PENDING 

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

61 approved_at: float | None = None 

62 approved_by: str | None = None 

63 applied_at: float | None = None 

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

65 

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

67 """Convert to dict.""" 

68 return { 

69 "id": self.id, 

70 "agent_name": self.agent_name, 

71 "change_type": self.change_type, 

72 "description": self.description, 

73 "old_value": self.old_value, 

74 "new_value": self.new_value, 

75 "status": self.status.value, 

76 "created_at": self.created_at, 

77 "approved_at": self.approved_at, 

78 "approved_by": self.approved_by, 

79 "applied_at": self.applied_at, 

80 "metadata": self.metadata, 

81 } 

82 

83 

84class EvolutionEngine: 

85 """ 

86 Approval-based self-evolution engine. 

87 

88 Manages the lifecycle of evolution proposals: 

89 1. Agent proposes improvement 

90 2. Human reviews and approves/rejects 

91 3. Approved changes are applied 

92 

93 Usage: 

94 engine = EvolutionEngine() 

95 

96 # Agent proposes improvement 

97 proposal = engine.propose( 

98 agent_name="SupportAgent", 

99 change_type="prompt", 

100 description="Improve greeting", 

101 old_value="Hello!", 

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

103 ) 

104 

105 # Human approves 

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

107 

108 # Apply changes 

109 engine.apply(proposal.id) 

110 """ 

111 

112 def __init__(self): 

113 """Initialize evolution engine.""" 

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

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

116 

117 def propose( 

118 self, 

119 agent_name: str, 

120 change_type: str, 

121 description: str, 

122 old_value: Any = None, 

123 new_value: Any = None, 

124 confidence: float = 0.0, 

125 risk_level: str = "low", 

126 target_files: list[str] | None = None, 

127 **metadata, 

128 ) -> EvolutionProposal: 

129 """ 

130 Create a new evolution proposal. 

131 

132 Args: 

133 agent_name: Name of agent to evolve 

134 change_type: Type of change 

135 description: Human-readable description 

136 old_value: Current value 

137 new_value: Proposed new value 

138 confidence: Confidence score (0-1) 

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

140 target_files: Files to modify 

141 **metadata: Additional metadata 

142 

143 Returns: 

144 Created EvolutionProposal 

145 """ 

146 proposal = EvolutionProposal( 

147 agent_name=agent_name, 

148 change_type=change_type, 

149 description=description, 

150 old_value=old_value, 

151 new_value=new_value, 

152 confidence=confidence, 

153 risk_level=risk_level, 

154 target_files=target_files or [], 

155 metadata=metadata, 

156 ) 

157 

158 self._proposals[proposal.id] = proposal 

159 

160 return proposal 

161 

162 def get_proposal(self, proposal_id: str) -> EvolutionProposal | None: 

163 """ 

164 Get a proposal by ID. 

165 

166 Args: 

167 proposal_id: Proposal ID 

168 

169 Returns: 

170 EvolutionProposal if found, None otherwise 

171 """ 

172 return self._proposals.get(proposal_id) 

173 

174 def list_proposals( 

175 self, 

176 status: EvolutionStatus | None = None, 

177 agent_name: str | None = None, 

178 ) -> list[EvolutionProposal]: 

179 """ 

180 List proposals. 

181 

182 Args: 

183 status: Filter by status 

184 agent_name: Filter by agent name 

185 

186 Returns: 

187 List of matching proposals 

188 """ 

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

190 

191 if status: 

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

193 

194 if agent_name: 

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

196 

197 return proposals 

198 

199 def approve( 

200 self, 

201 proposal_id: str, 

202 approved_by: str = "human", 

203 ) -> bool: 

204 """ 

205 Approve a proposal. 

206 

207 Args: 

208 proposal_id: Proposal ID 

209 approved_by: Who approved 

210 

211 Returns: 

212 True if approved, False if not found 

213 """ 

214 proposal = self._proposals.get(proposal_id) 

215 if not proposal: 

216 return False 

217 

218 if proposal.status != EvolutionStatus.PENDING: 

219 return False 

220 

221 proposal.status = EvolutionStatus.APPROVED 

222 proposal.approved_at = time.time() 

223 proposal.approved_by = approved_by 

224 

225 return True 

226 

227 def reject( 

228 self, 

229 proposal_id: str, 

230 reason: str = "", 

231 ) -> bool: 

232 """ 

233 Reject a proposal. 

234 

235 Args: 

236 proposal_id: Proposal ID 

237 reason: Reason for rejection 

238 

239 Returns: 

240 True if rejected, False if not found 

241 """ 

242 proposal = self._proposals.get(proposal_id) 

243 if not proposal: 

244 return False 

245 

246 if proposal.status != EvolutionStatus.PENDING: 

247 return False 

248 

249 proposal.status = EvolutionStatus.REJECTED 

250 proposal.metadata["rejection_reason"] = reason 

251 

252 return True 

253 

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

255 """ 

256 Apply an approved proposal. Performs actual file modifications. 

257 

258 Args: 

259 proposal_id: Proposal ID 

260 

261 Returns: 

262 True if applied, False otherwise 

263 """ 

264 proposal = self._proposals.get(proposal_id) 

265 if not proposal: 

266 return False 

267 

268 if proposal.status != EvolutionStatus.APPROVED: 

269 return False 

270 

271 try: 

272 target_files = proposal.target_files or [] 

273 old_val = proposal.old_value 

274 new_val = proposal.new_value 

275 

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

277 for fpath in target_files: 

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

279 continue 

280 with open(fpath) as f: 

281 content = f.read() 

282 old_str = str(old_val) 

283 new_str = str(new_val) 

284 if old_str in content: 

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

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

287 f.write(content) 

288 proposal.metadata["modified_file"] = fpath 

289 

290 proposal.status = EvolutionStatus.APPLIED 

291 proposal.applied_at = time.time() 

292 return True 

293 except Exception as e: 

294 proposal.status = EvolutionStatus.FAILED 

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

296 return False 

297 

298 def register_approver( 

299 self, 

300 agent_name: str, 

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

302 ) -> None: 

303 """ 

304 Register an approver for an agent. 

305 

306 Args: 

307 agent_name: Agent name 

308 approver: Approval function 

309 """ 

310 self._approvers[agent_name] = approver 

311 

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

313 """ 

314 Auto-approve using registered approver. 

315 

316 Args: 

317 proposal_id: Proposal ID 

318 

319 Returns: 

320 True if approved, False otherwise 

321 """ 

322 proposal = self._proposals.get(proposal_id) 

323 if not proposal: 

324 return False 

325 

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

327 if not approver: 

328 return False 

329 

330 if approver(proposal): 

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

332 

333 return False 

334 

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

336 """ 

337 Get evolution statistics. 

338 

339 Returns: 

340 Dict with proposal counts by status 

341 """ 

342 stats = { 

343 "pending": 0, 

344 "approved": 0, 

345 "rejected": 0, 

346 "applied": 0, 

347 "failed": 0, 

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

349 } 

350 

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

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

353 

354 return stats