1"""

2 Evolution Engine for NexusAgent.

3

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

5 improvements, but changes require human approval before

6 being applied.

7"""

8

9 from __future__ import annotations

10

11 import os

12 import time

13 import uuid

14 from dataclasses import dataclass, field

15 from enum import Enum

16 from typing import Any, Callable, Optional

17

18

19 class 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

29 class 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

81 class 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