1"""

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

3

4 The 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

9 Learning 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

17 from __future__ import annotations

18

19 import time

20 from dataclasses import dataclass, field

21 from typing import Any, Optional

22

23 from agentos.evolution.engine import EvolutionEngine, EvolutionProposal

24 from agentos.evolution.signals import (

25 BehaviorSignal,

26 SignalCollector,

27 SignalSummary,

28 FeedbackPolarity,

29)

30

31

32@dataclass

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

45 class 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 }