Coverage for agentos/evolution/learner.py: 25%

110 statements  

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

1""" 

2Learning Engine — Analyzes behavior signals to generate evolution proposals. 

3 

4The Learner sits between the SignalCollector and EvolutionEngine: 

5 1. SignalCollector gathers user behavior signals 

6 2. Learner analyzes signals, detects patterns, and suggests improvements 

7 3. EvolutionEngine manages the proposal lifecycle (pending → approved → applied) 

8 

9Learning strategies: 

10 - Tool recommendation: suggest new tools based on usage patterns 

11 - Parameter tuning: adjust temperature, max_tokens based on feedback 

12 - Format adaptation: learn preferred output formats 

13 - Prompt refinement: improve system prompts based on corrections 

14 - Workflow optimization: suggest shortcuts for repeated tasks 

15""" 

16 

17from __future__ import annotations 

18 

19import time 

20from dataclasses import dataclass, field 

21from typing import Any, Optional 

22 

23from agentos.evolution.engine import EvolutionEngine, EvolutionProposal 

24from agentos.evolution.signals import ( 

25 BehaviorSignal, 

26 SignalCollector, 

27 SignalSummary, 

28 FeedbackPolarity, 

29) 

30 

31 

32@dataclass 

33class LearningInsight: 

34 """A single insight derived from behavior signals.""" 

35 

36 category: str # tool_recommendation, param_tuning, format_adaptation, prompt_refinement, workflow 

37 title: str 

38 description: str 

39 confidence: float # 0.0 - 1.0 

40 evidence_count: int 

41 proposal_id: str = "" 

42 source_signals: list[str] = field(default_factory=list) 

43 

44 

45class Learner: 

46 """Learning engine — from signals to proposals. 

47 

48 Usage: 

49 from agentos.evolution import EvolutionEngine, SignalCollector, Learner 

50 

51 collector = SignalCollector() 

52 engine = EvolutionEngine() 

53 learner = Learner(collector, engine) 

54 

55 # After accumulating signals... 

56 insights = learner.analyze() 

57 for insight in insights: 

58 proposal = learner.propose_from_insight(insight) 

59 print(f"Proposed: {proposal.description}") 

60 """ 

61 

62 def __init__( 

63 self, 

64 collector: SignalCollector, 

65 engine: EvolutionEngine, 

66 min_confidence: float = 0.6, 

67 auto_propose: bool = False, 

68 ): 

69 self._collector = collector 

70 self._engine = engine 

71 self._min_confidence = min_confidence 

72 self._auto_propose = auto_propose 

73 self._insights: list[LearningInsight] = [] 

74 self._applied_count: int = 0 

75 

76 # ── Analysis ── 

77 

78 def analyze(self, hours: float = 168) -> list[LearningInsight]: 

79 """Analyze recent signals and generate learning insights.""" 

80 summary = self._collector.summarize(hours) 

81 signals: list[BehaviorSignal] = self._collector._buffer[:] 

82 insights: list[LearningInsight] = [] 

83 

84 # 1. Tool recommendation 

85 insights.extend(self._analyze_tool_patterns(summary, signals)) 

86 

87 # 2. Parameter tuning suggestions 

88 insights.extend(self._analyze_feedback_for_tuning(summary)) 

89 

90 # 3. Format adaptation 

91 insights.extend(self._analyze_format_preferences(signals)) 

92 

93 # 4. Prompt refinement from corrections 

94 insights.extend(self._analyze_corrections(signals)) 

95 

96 # 5. Workflow optimization 

97 insights.extend(self._analyze_workflow_patterns(signals)) 

98 

99 # 6. General health 

100 insights.extend(self._analyze_health(summary)) 

101 

102 # Filter by confidence 

103 insights = [i for i in insights if i.confidence >= self._min_confidence] 

104 self._insights = insights 

105 

106 # Auto-propose if enabled 

107 if self._auto_propose: 

108 for insight in insights: 

109 self.propose_from_insight(insight) 

110 

111 return insights 

112 

113 # ── Insight → Proposal ── 

114 

115 def propose_from_insight(self, insight: LearningInsight) -> Optional[EvolutionProposal]: 

116 """Convert a learning insight into an evolution proposal.""" 

117 proposal = self._engine.propose( 

118 agent_name="marvis", 

119 change_type=insight.category, 

120 description=f"{insight.title}: {insight.description}", 

121 new_value={ 

122 "category": insight.category, 

123 "title": insight.title, 

124 "description": insight.description, 

125 "confidence": insight.confidence, 

126 }, 

127 confidence=insight.confidence, 

128 risk_level="medium" if insight.confidence > 0.5 else "low", 

129 insight_id=id(insight), 

130 ) 

131 insight.proposal_id = proposal.id 

132 return proposal 

133 

134 def approve_all(self) -> int: 

135 """Approve all pending proposals above confidence threshold.""" 

136 count = 0 

137 for proposal in self._engine.list_proposals(status="pending"): 

138 self._engine.approve(proposal.id, approved_by="learner-auto") 

139 self._engine.apply(proposal.id) 

140 count += 1 

141 self._applied_count += 1 

142 return count 

143 

144 # ── Private Analyzers ── 

145 

146 def _analyze_tool_patterns(self, summary: SignalSummary, 

147 signals: list[BehaviorSignal]) -> list[LearningInsight]: 

148 """Analyze tool usage to suggest new tools or deprecate unused ones.""" 

149 insights = [] 

150 

151 # Low tool success rate → suggest alternatives 

152 if summary.tool_success_rate < 0.7 and summary.total_signals > 10: 

153 failing = [t for t, _ in summary.top_tools[:3]] 

154 insights.append(LearningInsight( 

155 category="tool_recommendation", 

156 title="Tool Success Rate Low", 

157 description=f"Tools {failing} have low success rate ({summary.tool_success_rate:.0%}). Consider alternatives or improve error handling.", 

158 confidence=0.75, 

159 evidence_count=summary.total_signals, 

160 )) 

161 

162 # High undo rate → tool is confusing 

163 if summary.undo_count >= 3: 

164 insights.append(LearningInsight( 

165 category="tool_recommendation", 

166 title="High Undo Rate Detected", 

167 description=f"Users undo actions frequently ({summary.undo_count} times). Tool UX may need improvement.", 

168 confidence=0.65, 

169 evidence_count=summary.undo_count, 

170 )) 

171 

172 return insights 

173 

174 def _analyze_feedback_for_tuning(self, summary: SignalSummary) -> list[LearningInsight]: 

175 """Analyze feedback to suggest parameter tuning.""" 

176 insights = [] 

177 

178 total_feedback = summary.positive_feedback + summary.negative_feedback 

179 if total_feedback < 5: 

180 return insights 

181 

182 ratio = summary.positive_feedback / max(total_feedback, 1) 

183 

184 if ratio < 0.4: 

185 insights.append(LearningInsight( 

186 category="param_tuning", 

187 title="Low Satisfaction Ratio", 

188 description=f"Positive feedback ratio is {ratio:.0%}. Consider adjusting agent temperature or personality.", 

189 confidence=0.8, 

190 evidence_count=total_feedback, 

191 )) 

192 

193 if ratio > 0.9: 

194 insights.append(LearningInsight( 

195 category="param_tuning", 

196 title="High Satisfaction — Lock Settings", 

197 description=f"Positive feedback ratio is {ratio:.0%}. Current settings work well; consider locking as default.", 

198 confidence=0.7, 

199 evidence_count=total_feedback, 

200 )) 

201 

202 return insights 

203 

204 def _analyze_format_preferences(self, signals: list[BehaviorSignal]) -> list[LearningInsight]: 

205 """Learn preferred output formats.""" 

206 preferences = [s for s in signals if s.type_.value == "format_preference"] 

207 if not preferences: 

208 return [] 

209 

210 format_counts = {} 

211 for s in preferences: 

212 fmt = s.feedback_type or "unknown" 

213 format_counts[fmt] = format_counts.get(fmt, 0) + 1 

214 

215 top = max(format_counts, key=format_counts.get) 

216 if format_counts[top] >= 3: 

217 return [LearningInsight( 

218 category="format_adaptation", 

219 title=f"Format Preference: {top}", 

220 description=f"User prefers {top} format ({format_counts[top]} signals). Default to this format.", 

221 confidence=0.85, 

222 evidence_count=format_counts[top], 

223 )] 

224 

225 return [] 

226 

227 def _analyze_corrections(self, signals: list[BehaviorSignal]) -> list[LearningInsight]: 

228 """Analyze corrections to suggest prompt refinements.""" 

229 corrections = [s for s in signals if s.type_.value == "correction"] 

230 if len(corrections) < 2: 

231 return [] 

232 

233 # Group corrections by topic 

234 return [LearningInsight( 

235 category="prompt_refinement", 

236 title="Frequent Corrections Detected", 

237 description=f"User made {len(corrections)} corrections. Review agent responses for accuracy improvements.", 

238 confidence=0.7, 

239 evidence_count=len(corrections), 

240 )] 

241 

242 def _analyze_workflow_patterns(self, signals: list[BehaviorSignal]) -> list[LearningInsight]: 

243 """Detect repeated tool sequences to suggest workflow shortcuts.""" 

244 tool_sequences = [] 

245 current_seq = [] 

246 

247 for s in signals: 

248 if s.type_.value == "tool_usage" and s.tool_name: 

249 current_seq.append(s.tool_name) 

250 if len(current_seq) >= 2: 

251 tool_sequences.append(tuple(current_seq[-2:])) 

252 

253 if len(tool_sequences) < 5: 

254 return [] 

255 

256 from collections import Counter 

257 seq_counter = Counter(tool_sequences) 

258 top_seq = seq_counter.most_common(1)[0] 

259 

260 if top_seq[1] >= 3: 

261 return [LearningInsight( 

262 category="workflow", 

263 title="Repeated Tool Sequence", 

264 description=f"Sequence {' → '.join(top_seq[0])} repeated {top_seq[1]} times. Consider creating a shortcut or composite tool.", 

265 confidence=0.65, 

266 evidence_count=top_seq[1], 

267 )] 

268 

269 return [] 

270 

271 def _analyze_health(self, summary: SignalSummary) -> list[LearningInsight]: 

272 """General health check insights.""" 

273 insights = [] 

274 

275 if summary.re_prompt_count >= 3: 

276 insights.append(LearningInsight( 

277 category="prompt_refinement", 

278 title="Clarify Intent Better", 

279 description=f"User re-asked {summary.re_prompt_count} times. First responses may not understand user intent.", 

280 confidence=0.6, 

281 evidence_count=summary.re_prompt_count, 

282 )) 

283 

284 return insights 

285 

286 # ── Stats ── 

287 

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

289 return { 

290 "total_insights": len(self._insights), 

291 "total_applied": self._applied_count, 

292 "auto_propose": self._auto_propose, 

293 "min_confidence": self._min_confidence, 

294 "latest_insights": [ 

295 { 

296 "category": i.category, 

297 "title": i.title, 

298 "confidence": i.confidence, 

299 "proposal_id": i.proposal_id, 

300 } 

301 for i in self._insights[-5:] 

302 ], 

303 }