learning.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. """
  2. agentpaas.services.learning — Adaptive learning service
  3. Records tool call patterns (success/failure), extracts strategy rules,
  4. and auto-applies learned strategies in future similar scenarios.
  5. """
  6. from __future__ import annotations
  7. import json
  8. import os
  9. import time
  10. from dataclasses import dataclass, field
  11. from typing import Any, Dict, List
  12. @dataclass
  13. class LearningEntry:
  14. tool_name: str
  15. input_preview: str
  16. success: bool
  17. error: str = ""
  18. strategy_used: str = "" # e.g. "bash_python3_fallback"
  19. timestamp: float = 0
  20. def __post_init__(self):
  21. if self.timestamp == 0:
  22. self.timestamp = time.time()
  23. @dataclass
  24. class StrategyRule:
  25. pattern: str # e.g. "WriteFile with markdown code blocks"
  26. strategy: str # e.g. "Use Bash+python3 instead of WriteFile"
  27. confidence: float = 0.0 # 0.0 - 1.0
  28. evidence_count: int = 0
  29. last_applied: float = 0
  30. def to_dict(self):
  31. return {"pattern": self.pattern, "strategy": self.strategy,
  32. "confidence": self.confidence, "evidence_count": self.evidence_count}
  33. LEARNING_PATH = os.path.expanduser("~/.lambdagent/learning.json")
  34. class LearningService:
  35. """Track tool patterns and extract strategies."""
  36. def __init__(self, path: str = ""):
  37. self.path = path or LEARNING_PATH
  38. self._entries: List[LearningEntry] = []
  39. self._strategies: List[StrategyRule] = []
  40. self._load()
  41. def record(self, tool_name: str, input_preview: str, success: bool,
  42. error: str = "", strategy: str = ""):
  43. """Record a tool call outcome."""
  44. self._entries.append(LearningEntry(
  45. tool_name=tool_name, input_preview=input_preview[:200],
  46. success=success, error=error[:200], strategy_used=strategy,
  47. ))
  48. # Keep last 500 entries
  49. if len(self._entries) > 500:
  50. self._entries = self._entries[-500:]
  51. self._extract_rules()
  52. self._save()
  53. def get_strategy(self, tool_name: str, input_preview: str) -> str:
  54. """Get recommended strategy for a tool call."""
  55. for rule in sorted(self._strategies, key=lambda r: -r.confidence):
  56. if rule.confidence >= 0.6 and (
  57. tool_name.lower() in rule.pattern.lower() or
  58. any(w in input_preview.lower() for w in rule.pattern.lower().split())
  59. ):
  60. rule.last_applied = time.time()
  61. return rule.strategy
  62. return ""
  63. def list_strategies(self) -> List[StrategyRule]:
  64. return [r for r in self._strategies if r.confidence >= 0.5]
  65. def _extract_rules(self):
  66. """Auto-extract strategy rules from patterns."""
  67. # Count failures by tool
  68. fail_patterns: Dict[str, int] = {}
  69. success_patterns: Dict[str, int] = {}
  70. for e in self._entries:
  71. key = e.tool_name
  72. if e.success:
  73. success_patterns[key] = success_patterns.get(key, 0) + 1
  74. else:
  75. fail_patterns[key] = fail_patterns.get(key, 0) + 1
  76. # Strategy: if a tool fails >3 times and an alternative succeeds
  77. for tool, fails in fail_patterns.items():
  78. if fails >= 3:
  79. # Check if there's a successful alternative
  80. successes = success_patterns.get(tool, 0)
  81. total = fails + successes
  82. fail_rate = fails / max(1, total)
  83. if fail_rate > 0.5:
  84. # Find what strategy succeeded
  85. successful_strategies = [
  86. e.strategy_used for e in self._entries
  87. if e.tool_name == tool and e.success and e.strategy_used
  88. ]
  89. if successful_strategies:
  90. strategy = max(set(successful_strategies), key=successful_strategies.count)
  91. self._update_rule(
  92. f"{tool} frequently fails",
  93. f"Use alternative: {strategy}",
  94. fail_rate,
  95. fails,
  96. )
  97. def _update_rule(self, pattern: str, strategy: str, confidence: float, evidence: int):
  98. for rule in self._strategies:
  99. if rule.pattern == pattern:
  100. rule.strategy = strategy
  101. rule.confidence = confidence
  102. rule.evidence_count = evidence
  103. return
  104. self._strategies.append(StrategyRule(
  105. pattern=pattern, strategy=strategy,
  106. confidence=confidence, evidence_count=evidence,
  107. ))
  108. def _save(self):
  109. os.makedirs(os.path.dirname(self.path), exist_ok=True)
  110. data = {
  111. "strategies": [r.to_dict() for r in self._strategies],
  112. "recent_entries": [
  113. {"tool": e.tool_name, "success": e.success, "error": e.error[:100],
  114. "timestamp": e.timestamp}
  115. for e in self._entries[-50:]
  116. ],
  117. }
  118. with open(self.path, "w") as f:
  119. json.dump(data, f, indent=2, ensure_ascii=False)
  120. def _load(self):
  121. if not os.path.exists(self.path):
  122. return
  123. try:
  124. with open(self.path) as f:
  125. data = json.load(f)
  126. for r in data.get("strategies", []):
  127. self._strategies.append(StrategyRule(**r))
  128. except Exception:
  129. pass
  130. # Agent-side tool functions
  131. _default_service = None
  132. def _get_service() -> LearningService:
  133. global _default_service
  134. if _default_service is None:
  135. _default_service = LearningService()
  136. return _default_service
  137. def learning_feedback(input_val: Any) -> str:
  138. params = _parse(input_val)
  139. _get_service().record(
  140. tool_name=params.get("tool", ""),
  141. input_preview=params.get("input", ""),
  142. success=params.get("success", True),
  143. error=params.get("error", ""),
  144. strategy=params.get("strategy", ""),
  145. )
  146. return "[OK] Feedback recorded"
  147. def learning_strategies(input_val: Any) -> str:
  148. rules = _get_service().list_strategies()
  149. if not rules:
  150. return "[EMPTY] No strategies learned yet"
  151. lines = ["Learned strategies:"]
  152. for r in rules:
  153. lines.append(f" [{r.confidence:.0%}] {r.pattern} → {r.strategy} ({r.evidence_count} evidence)")
  154. return "\n".join(lines)
  155. def _parse(input_val: Any) -> dict:
  156. if isinstance(input_val, dict):
  157. return input_val
  158. if isinstance(input_val, str):
  159. try:
  160. return json.loads(input_val)
  161. except Exception:
  162. return {}
  163. return {}