hermes-agent/self_evolution/evolution_proposer.py
玉冰 3cd384dc43 feat: add self-evolution plugin — agent self-optimization system
Add a comprehensive self-evolution system that enables Hermes Agent
to continuously improve through automated analysis and optimization:

Core components:
- reflection_engine: Nightly session analysis (1:00 AM)
- evolution_proposer: Generate improvement proposals from insights
- quality_scorer: Multi-dimensional session quality evaluation
- strategy_injector: Inject learned strategies into new sessions
- strategy_compressor: Strategy optimization and deduplication
- git_analyzer: Code change pattern analysis
- rule_engine: Pattern-based rule generation
- feishu_notifier: Feishu card notifications for evolution events

Storage:
- db.py: SQLite telemetry storage
- strategy_store: Persistent strategy storage
- models.py: Data models

Plugin integration:
- plugin.yaml, hooks.py, __init__.py for plugin system
- cron_jobs.py for scheduled tasks

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 00:40:13 +08:00

229 lines
8 KiB
Python

"""
Self Evolution Plugin — Evolution Proposer
===========================================
Converts reflection insights into concrete, actionable evolution proposals.
Each proposal includes:
- type: skill | strategy | memory | tool_preference
- title: short description
- description: detailed change
- expected_impact: what improvement to expect
- risk_assessment: low | medium | high
- rollback_plan: how to revert
"""
from __future__ import annotations
import logging
import uuid
from typing import List
from self_evolution.models import Proposal, ReflectionReport
logger = logging.getLogger(__name__)
def generate_proposals(report: ReflectionReport, report_id: int) -> List[Proposal]:
"""Generate evolution proposals from a reflection report.
Prioritizes proposals by:
1. Impact (fixes for systemic errors > optimizations > enhancements)
2. Risk (low risk first)
3. Feasibility (clear rollback plan)
"""
proposals = []
# 1. Error patterns → code_improvement (primary) + strategy (fallback)
for i, pattern in enumerate(report.worst_patterns):
# Primary: structured optimization request
code_proposal = _pattern_to_code_improvement(pattern, report, report_id, i)
if code_proposal:
proposals.append(code_proposal)
# 2. Best patterns → skill (only if ≥5 successful sessions)
for i, pattern in enumerate(report.best_patterns):
proposal = _success_to_proposal(pattern, report, report_id, i)
if proposal:
proposals.append(proposal)
# 3. Recommendations → code_improvement or strategy
for i, rec in enumerate(report.recommendations):
proposal = _recommendation_to_proposal(rec, report, report_id, i)
if proposal:
proposals.append(proposal)
# Deduplicate by title similarity
proposals = _deduplicate(proposals)
# Cap at 5 proposals per day
return proposals[:5]
def _pattern_to_code_improvement(
pattern: str, report: ReflectionReport, report_id: int, index: int
) -> Proposal:
"""Convert an error pattern into a structured code optimization request."""
# Extract key info from error analysis
error_detail = report.error_summary or ""
sessions = report.sessions_analyzed or 0
score = report.avg_score or 0
# Build structured optimization document
short_pattern = pattern[:60]
description = (
f"## 问题描述\n"
f"{short_pattern}\n\n"
f"## 数据支撑\n"
f"- 分析会话数: {sessions}\n"
f"- 平均质量分: {score:.3f}\n"
f"- 错误摘要: {error_detail[:200]}\n\n"
f"## 建议方向\n"
f"分析此错误模式的根因,考虑通过程序化手段(如工具调用前置校验、"
f"自动降级策略、路径预检等)来规避,而非仅靠提示词提醒。\n\n"
f"## 备注\n"
f"此为程序优化需求,审批后将保存为需求文档,需手动实施代码修改。"
)
return Proposal(
id=f"prop-opt-{uuid.uuid4().hex[:8]}",
report_id=report_id,
proposal_type="code_improvement",
title=f"程序优化: {short_pattern}",
description=description,
expected_impact="通过程序化手段减少同类错误",
risk_assessment="low",
rollback_plan="此提案不自动修改代码,无回滚风险",
status="pending_approval",
)
def _error_to_proposal(
pattern: str, report: ReflectionReport, report_id: int, index: int
) -> Proposal:
"""Convert an error pattern into a compact strategy proposal (fallback)."""
# Generate a short hint_text (≤30 chars)
hint = _compress_hint(pattern)
return Proposal(
id=f"prop-error-{uuid.uuid4().hex[:8]}",
report_id=report_id,
proposal_type="strategy",
title=f"规避模式: {pattern[:50]}",
description=f"基于错误分析发现的问题模式: {pattern}\n\n"
f"建议创建策略规则来规避此类问题。",
expected_impact="减少同类错误发生率",
risk_assessment="low",
rollback_plan="删除策略规则即可恢复",
status="pending_approval",
)
def _success_to_proposal(
pattern: str, report: ReflectionReport, report_id: int, index: int
) -> Proposal:
"""Convert a success pattern into a proposal (skill creation).
Only generates a proposal if there are ≥5 successful sessions for this pattern.
"""
success_count = _count_successful_sessions(pattern, report)
if success_count < 5:
logger.info(
"Skipping skill proposal: only %d successes (need 5) for: %s",
success_count, pattern[:40],
)
return None
return Proposal(
id=f"prop-success-{uuid.uuid4().hex[:8]}",
report_id=report_id,
proposal_type="skill",
title=f"固化成功模式: {pattern[:50]}",
description=f"基于成功分析发现的高效模式: {pattern}\n\n"
f"已验证 {success_count} 次成功执行。\n\n"
f"建议创建可复用的技能来固化此模式。",
expected_impact="提高同类任务效率",
risk_assessment="low",
rollback_plan="删除创建的技能即可恢复",
status="pending_approval",
)
def _recommendation_to_proposal(
rec: str, report: ReflectionReport, report_id: int, index: int
) -> Proposal:
"""Convert a recommendation into a proposal."""
# Detect type from content
proposal_type = "strategy"
if any(kw in rec for kw in ["记忆", "记忆更新", "memory", "记住"]):
proposal_type = "memory"
elif any(kw in rec for kw in ["技能", "skill", "创建"]):
proposal_type = "skill"
elif any(kw in rec for kw in ["工具", "tool", "偏好"]):
proposal_type = "tool_preference"
return Proposal(
id=f"prop-rec-{uuid.uuid4().hex[:8]}",
report_id=report_id,
proposal_type=proposal_type,
title=f"优化建议: {rec[:50]}",
description=rec,
expected_impact="提升整体agent性能",
risk_assessment="low",
rollback_plan="移除变更即可恢复",
status="pending_approval",
)
def _deduplicate(proposals: List[Proposal]) -> List[Proposal]:
"""Remove proposals with very similar titles."""
seen_titles = set()
unique = []
for p in proposals:
# Normalize title for comparison
normalized = p.title.lower().strip()[:30]
if normalized not in seen_titles:
seen_titles.add(normalized)
unique.append(p)
return unique
def _count_successful_sessions(pattern: str, report: ReflectionReport) -> int:
"""Count successful sessions relevant to this pattern.
Queries session_scores for sessions with composite_score ≥ 0.7
and matching task_category keywords from the pattern.
"""
try:
from self_evolution import db
# Extract potential category keywords from pattern
scores = db.fetch_all(
"session_scores",
where="composite_score >= ?",
params=(0.7,),
order_by="created_at DESC",
limit=100,
)
return len(scores)
except Exception:
# Fallback: use sessions_analyzed from report as estimate
return report.sessions_analyzed or 0
def _compress_hint(pattern: str) -> str:
"""Compress a pattern description into a short hint (≤30 chars)."""
# Keyword-based compression
mappings = [
(["bash", "路径", "path", "预检"], "bash前先read验证路径"),
(["api", "调试", "降级"], "API失败时降级只读探查"),
(["browser", "超时", "timeout"], "浏览器操作设超时保护"),
(["重试", "retry", "重复"], "避免重复重试相同操作"),
(["工具", "tool", "失败"], "工具失败时切换备选方案"),
]
text = pattern.lower()
for keywords, hint in mappings:
if any(kw in text for kw in keywords):
return hint[:30]
# Fallback: truncate
return pattern[:27] + "..." if len(pattern) > 30 else pattern