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

109 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-06 10: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 

19from dataclasses import dataclass, field 

20from typing import Any, Optional 

21 

22from agentos.evolution.engine import EvolutionEngine, EvolutionProposal 

23from agentos.evolution.signals import ( 

24 BehaviorSignal, 

25 SignalCollector, 

26 SignalSummary, 

27) 

28 

29 

30@dataclass 

31class LearningInsight: 

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

33 

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

35 title: str 

36 description: str 

37 confidence: float # 0.0 - 1.0 

38 evidence_count: int 

39 proposal_id: str = "" 

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

41 

42 

43class Learner: 

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

45 

46 Usage: 

47 from agentos.evolution import EvolutionEngine, SignalCollector, Learner 

48 

49 collector = SignalCollector() 

50 engine = EvolutionEngine() 

51 learner = Learner(collector, engine) 

52 

53 # After accumulating signals... 

54 insights = learner.analyze() 

55 for insight in insights: 

56 proposal = learner.propose_from_insight(insight) 

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

58 """ 

59 

60 def __init__( 

61 self, 

62 collector: SignalCollector, 

63 engine: EvolutionEngine, 

64 min_confidence: float = 0.6, 

65 auto_propose: bool = False, 

66 ): 

67 self._collector = collector 

68 self._engine = engine 

69 self._min_confidence = min_confidence 

70 self._auto_propose = auto_propose 

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

72 self._applied_count: int = 0 

73 

74 # ── Analysis ── 

75 

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

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

78 summary = self._collector.summarize(hours) 

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

80 insights: list[LearningInsight] = [] 

81 

82 # 1. Tool recommendation 

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

84 

85 # 2. Parameter tuning suggestions 

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

87 

88 # 3. Format adaptation 

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

90 

91 # 4. Prompt refinement from corrections 

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

93 

94 # 5. Workflow optimization 

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

96 

97 # 6. General health 

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

99 

100 # Filter by confidence 

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

102 self._insights = insights 

103 

104 # Auto-propose if enabled 

105 if self._auto_propose: 

106 for insight in insights: 

107 self.propose_from_insight(insight) 

108 

109 return insights 

110 

111 # ── Insight → Proposal ── 

112 

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

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

115 proposal = self._engine.propose( 

116 agent_name="marvis", 

117 change_type=insight.category, 

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

119 new_value={ 

120 "category": insight.category, 

121 "title": insight.title, 

122 "description": insight.description, 

123 "confidence": insight.confidence, 

124 }, 

125 confidence=insight.confidence, 

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

127 insight_id=id(insight), 

128 ) 

129 insight.proposal_id = proposal.id 

130 return proposal 

131 

132 def approve_all(self) -> int: 

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

134 count = 0 

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

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

137 self._engine.apply(proposal.id) 

138 count += 1 

139 self._applied_count += 1 

140 return count 

141 

142 # ── Private Analyzers ── 

143 

144 def _analyze_tool_patterns(self, summary: SignalSummary, 

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

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

147 insights = [] 

148 

149 # Low tool success rate → suggest alternatives 

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

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

152 insights.append(LearningInsight( 

153 category="tool_recommendation", 

154 title="Tool Success Rate Low", 

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

156 confidence=0.75, 

157 evidence_count=summary.total_signals, 

158 )) 

159 

160 # High undo rate → tool is confusing 

161 if summary.undo_count >= 3: 

162 insights.append(LearningInsight( 

163 category="tool_recommendation", 

164 title="High Undo Rate Detected", 

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

166 confidence=0.65, 

167 evidence_count=summary.undo_count, 

168 )) 

169 

170 return insights 

171 

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

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

174 insights = [] 

175 

176 total_feedback = summary.positive_feedback + summary.negative_feedback 

177 if total_feedback < 5: 

178 return insights 

179 

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

181 

182 if ratio < 0.4: 

183 insights.append(LearningInsight( 

184 category="param_tuning", 

185 title="Low Satisfaction Ratio", 

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

187 confidence=0.8, 

188 evidence_count=total_feedback, 

189 )) 

190 

191 if ratio > 0.9: 

192 insights.append(LearningInsight( 

193 category="param_tuning", 

194 title="High Satisfaction — Lock Settings", 

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

196 confidence=0.7, 

197 evidence_count=total_feedback, 

198 )) 

199 

200 return insights 

201 

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

203 """Learn preferred output formats.""" 

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

205 if not preferences: 

206 return [] 

207 

208 format_counts = {} 

209 for s in preferences: 

210 fmt = s.feedback_type or "unknown" 

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

212 

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

214 if format_counts[top] >= 3: 

215 return [LearningInsight( 

216 category="format_adaptation", 

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

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

219 confidence=0.85, 

220 evidence_count=format_counts[top], 

221 )] 

222 

223 return [] 

224 

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

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

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

228 if len(corrections) < 2: 

229 return [] 

230 

231 # Group corrections by topic 

232 return [LearningInsight( 

233 category="prompt_refinement", 

234 title="Frequent Corrections Detected", 

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

236 confidence=0.7, 

237 evidence_count=len(corrections), 

238 )] 

239 

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

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

242 tool_sequences = [] 

243 current_seq = [] 

244 

245 for s in signals: 

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

247 current_seq.append(s.tool_name) 

248 if len(current_seq) >= 2: 

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

250 

251 if len(tool_sequences) < 5: 

252 return [] 

253 

254 from collections import Counter 

255 seq_counter = Counter(tool_sequences) 

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

257 

258 if top_seq[1] >= 3: 

259 return [LearningInsight( 

260 category="workflow", 

261 title="Repeated Tool Sequence", 

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

263 confidence=0.65, 

264 evidence_count=top_seq[1], 

265 )] 

266 

267 return [] 

268 

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

270 """General health check insights.""" 

271 insights = [] 

272 

273 if summary.re_prompt_count >= 3: 

274 insights.append(LearningInsight( 

275 category="prompt_refinement", 

276 title="Clarify Intent Better", 

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

278 confidence=0.6, 

279 evidence_count=summary.re_prompt_count, 

280 )) 

281 

282 return insights 

283 

284 # ── Stats ── 

285 

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

287 return { 

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

289 "total_applied": self._applied_count, 

290 "auto_propose": self._auto_propose, 

291 "min_confidence": self._min_confidence, 

292 "latest_insights": [ 

293 { 

294 "category": i.category, 

295 "title": i.title, 

296 "confidence": i.confidence, 

297 "proposal_id": i.proposal_id, 

298 } 

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

300 ], 

301 }