hermes-agent/self_evolution/strategy_injector.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

124 lines
3.5 KiB
Python

"""
Self Evolution Plugin — Strategy Injector
===========================================
Injects learned strategy hints into sessions via pre_llm_call hook.
Design reference: Claude Code plugins/learning-output-style/
- SessionStart hook injects behavioral context automatically
- Equivalent to CLAUDE.md but more flexible and distributable
- No core modification needed
"""
from __future__ import annotations
import logging
import time
from typing import Any, Dict, Optional
from self_evolution.models import StrategyRule
from self_evolution.rule_engine import StrategyRuleEngine
logger = logging.getLogger(__name__)
_engine = StrategyRuleEngine()
# ── TTL-based cache to avoid reading strategies.json on every LLM call ────
_cached_strategies: list | None = None
_cache_ts: float = 0.0
_CACHE_TTL: float = 60.0 # seconds
def _load_active_strategies() -> list:
"""Load active strategies from strategy store (cached for _CACHE_TTL)."""
global _cached_strategies, _cache_ts
now = time.time()
if _cached_strategies is not None and (now - _cache_ts) < _CACHE_TTL:
return _cached_strategies
from self_evolution.strategy_store import StrategyStore
store = StrategyStore()
data = store.load()
rules = data.get("rules", [])
strategies = []
for rule_data in rules:
if not rule_data.get("enabled", True):
continue
strategy = StrategyRule.from_dict(rule_data)
strategies.append(strategy)
_cached_strategies = strategies
_cache_ts = now
return strategies
def invalidate_cache():
"""Invalidate the strategy cache (call after strategy updates)."""
global _cached_strategies
_cached_strategies = None
_MAX_INJECT_STRATEGIES = 3 # 最多注入策略数
_MAX_HINT_CHARS = 100 # 注入提示总字符预算
_MAX_SINGLE_HINT = 30 # 单条 hint_text 最大字符数
def inject_hints(kwargs: dict) -> Optional[str]:
"""Pre-llm-call hook: inject learned strategy hints.
Rules:
- Strategies without conditions are skipped (must be condition-based).
- hint_text longer than _MAX_SINGLE_HINT chars are skipped.
- At most _MAX_INJECT_STRATEGIES hints, total ≤ _MAX_HINT_CHARS.
"""
strategies = _load_active_strategies()
if not strategies:
return None
# Build context from current session
context = _build_context(kwargs)
# Match strategies
matched = _engine.match_strategies(strategies, context)
if not matched:
return None
# Filter: require conditions and enforce hint length
eligible = []
for s in matched:
if not s.conditions:
continue # Skip unconditioned strategies
if len(s.hint_text.strip()) > _MAX_SINGLE_HINT:
continue # Skip overly long hints
eligible.append(s)
if not eligible:
return None
# Deduplicate by hint_text content
seen_hints: set[str] = set()
unique: list = []
for s in eligible:
key = s.hint_text.strip().lower()
if key not in seen_hints:
seen_hints.add(key)
unique.append(s)
# Cap count
selected = unique[:_MAX_INJECT_STRATEGIES]
# Format hints within char budget
return _engine.format_hints(selected, max_chars=_MAX_HINT_CHARS)
def _build_context(kwargs: dict) -> dict:
"""Build matching context from hook kwargs."""
return {
"platform": kwargs.get("platform", ""),
"model": kwargs.get("model", ""),
"task_type": kwargs.get("task_type", ""),
"tool_name": kwargs.get("tool_name", ""),
}