mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-27 01:11:40 +00:00
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>
This commit is contained in:
parent
e5d41f05d4
commit
3cd384dc43
23 changed files with 6173 additions and 0 deletions
296
self_evolution/db.py
Normal file
296
self_evolution/db.py
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
"""
|
||||
Self Evolution Plugin — Independent SQLite Database
|
||||
=====================================================
|
||||
Independent from state.db to avoid upstream schema conflicts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from self_evolution.paths import DATA_DIR as DB_DIR, DB_PATH
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
VALID_TABLES = frozenset({
|
||||
"tool_invocations", "session_scores", "outcome_signals",
|
||||
"reflection_reports", "evolution_proposals", "improvement_units",
|
||||
"strategy_versions", "_meta",
|
||||
})
|
||||
|
||||
|
||||
def _validate_table(table: str) -> None:
|
||||
"""Reject table names not in the known schema."""
|
||||
if table not in VALID_TABLES:
|
||||
raise ValueError(f"Invalid table name: {table!r}")
|
||||
|
||||
|
||||
SCHEMA = """
|
||||
-- Tool invocation telemetry
|
||||
CREATE TABLE IF NOT EXISTS tool_invocations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
tool_name TEXT NOT NULL,
|
||||
duration_ms INTEGER,
|
||||
success BOOLEAN NOT NULL,
|
||||
error_type TEXT,
|
||||
turn_number INTEGER,
|
||||
created_at REAL NOT NULL DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
|
||||
-- Session quality scores
|
||||
CREATE TABLE IF NOT EXISTS session_scores (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
composite_score REAL,
|
||||
completion_rate REAL,
|
||||
efficiency_score REAL,
|
||||
cost_efficiency REAL,
|
||||
satisfaction_proxy REAL,
|
||||
task_category TEXT,
|
||||
model TEXT,
|
||||
created_at REAL NOT NULL DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
|
||||
-- Outcome signals
|
||||
CREATE TABLE IF NOT EXISTS outcome_signals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
signal_type TEXT NOT NULL,
|
||||
signal_value REAL,
|
||||
metadata TEXT,
|
||||
created_at REAL NOT NULL DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
|
||||
-- Reflection reports
|
||||
CREATE TABLE IF NOT EXISTS reflection_reports (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
period_start REAL,
|
||||
period_end REAL,
|
||||
sessions_analyzed INTEGER,
|
||||
avg_score REAL,
|
||||
error_summary TEXT DEFAULT '',
|
||||
waste_summary TEXT DEFAULT '',
|
||||
code_change_summary TEXT DEFAULT '',
|
||||
worst_patterns TEXT DEFAULT '[]',
|
||||
best_patterns TEXT DEFAULT '[]',
|
||||
tool_insights TEXT DEFAULT '{}',
|
||||
recommendations TEXT DEFAULT '[]',
|
||||
model_used TEXT DEFAULT '',
|
||||
created_at REAL NOT NULL DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
|
||||
-- Evolution proposals
|
||||
CREATE TABLE IF NOT EXISTS evolution_proposals (
|
||||
id TEXT PRIMARY KEY,
|
||||
report_id INTEGER REFERENCES reflection_reports(id),
|
||||
proposal_type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
expected_impact TEXT DEFAULT '',
|
||||
risk_assessment TEXT DEFAULT 'low',
|
||||
rollback_plan TEXT DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'pending_approval',
|
||||
user_feedback TEXT DEFAULT '',
|
||||
created_at REAL NOT NULL DEFAULT (strftime('%s','now')),
|
||||
resolved_at REAL
|
||||
);
|
||||
|
||||
-- Improvement unit tracking (A/B testing)
|
||||
CREATE TABLE IF NOT EXISTS improvement_units (
|
||||
id TEXT PRIMARY KEY,
|
||||
proposal_id TEXT REFERENCES evolution_proposals(id),
|
||||
change_type TEXT NOT NULL,
|
||||
version INTEGER DEFAULT 0,
|
||||
baseline_score REAL DEFAULT 0.0,
|
||||
current_score REAL DEFAULT 0.0,
|
||||
sessions_sampled INTEGER DEFAULT 0,
|
||||
min_sessions INTEGER DEFAULT 10,
|
||||
min_improvement REAL DEFAULT 0.05,
|
||||
max_regression REAL DEFAULT 0.10,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at REAL NOT NULL DEFAULT (strftime('%s','now')),
|
||||
resolved_at REAL
|
||||
);
|
||||
|
||||
-- Strategy version history
|
||||
CREATE TABLE IF NOT EXISTS strategy_versions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
version INTEGER NOT NULL,
|
||||
strategies_json TEXT NOT NULL,
|
||||
avg_score REAL,
|
||||
active_from REAL NOT NULL,
|
||||
active_until REAL
|
||||
);
|
||||
|
||||
-- Schema version tracking
|
||||
CREATE TABLE IF NOT EXISTS _meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_tool_invocations_session ON tool_invocations(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tool_invocations_created ON tool_invocations(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_scores_created ON session_scores(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_outcome_signals_session ON outcome_signals(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_evolution_proposals_status ON evolution_proposals(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_improvement_units_status ON improvement_units(status);
|
||||
"""
|
||||
|
||||
|
||||
def _ensure_dir():
|
||||
DB_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
_local = threading.local()
|
||||
|
||||
|
||||
def get_connection() -> sqlite3.Connection:
|
||||
"""Return a thread-local cached connection (reused across calls)."""
|
||||
conn = getattr(_local, "conn", None)
|
||||
if conn is not None:
|
||||
try:
|
||||
conn.execute("SELECT 1")
|
||||
return conn
|
||||
except sqlite3.Error:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
_ensure_dir()
|
||||
conn = sqlite3.connect(str(DB_PATH))
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("PRAGMA foreign_keys=ON")
|
||||
_local.conn = conn
|
||||
return conn
|
||||
|
||||
|
||||
def close_connection():
|
||||
"""Close the thread-local connection (for test cleanup / teardown)."""
|
||||
conn = getattr(_local, "conn", None)
|
||||
if conn is not None:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
_local.conn = None
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Initialize database with schema."""
|
||||
conn = get_connection()
|
||||
conn.executescript(SCHEMA)
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)",
|
||||
("schema_version", str(SCHEMA_VERSION)),
|
||||
)
|
||||
conn.commit()
|
||||
logger.info("self_evolution database initialized at %s", DB_PATH)
|
||||
|
||||
# Schema migration: add code_change_summary column if missing
|
||||
try:
|
||||
conn.execute("ALTER TABLE reflection_reports ADD COLUMN code_change_summary TEXT DEFAULT ''")
|
||||
logger.info("Added code_change_summary column to reflection_reports")
|
||||
except sqlite3.OperationalError:
|
||||
pass # Column already exists
|
||||
|
||||
# Close after init so subsequent calls get a fresh connection with the new schema
|
||||
close_connection()
|
||||
|
||||
|
||||
# ── Generic CRUD ─────────────────────────────────────────────────────────
|
||||
|
||||
def insert(table: str, data: dict) -> int:
|
||||
"""Insert a row into a table. Returns the rowid."""
|
||||
_validate_table(table)
|
||||
conn = get_connection()
|
||||
cols = ", ".join(data.keys())
|
||||
placeholders = ", ".join("?" for _ in data)
|
||||
sql = f"INSERT INTO {table} ({cols}) VALUES ({placeholders})"
|
||||
cur = conn.execute(sql, list(data.values()))
|
||||
conn.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def insert_many(table: str, rows: List[dict]):
|
||||
"""Insert multiple rows."""
|
||||
_validate_table(table)
|
||||
if not rows:
|
||||
return
|
||||
conn = get_connection()
|
||||
cols = list(rows[0].keys())
|
||||
placeholders = ", ".join("?" for _ in cols)
|
||||
sql = f"INSERT INTO {table} ({', '.join(cols)}) VALUES ({placeholders})"
|
||||
conn.executemany(sql, [[row.get(c) for c in cols] for row in rows])
|
||||
conn.commit()
|
||||
|
||||
|
||||
def update(table: str, data: dict, where: str, where_params: tuple = ()):
|
||||
"""Update rows matching where clause."""
|
||||
_validate_table(table)
|
||||
conn = get_connection()
|
||||
set_clause = ", ".join(f"{k} = ?" for k in data.keys())
|
||||
sql = f"UPDATE {table} SET {set_clause} WHERE {where}"
|
||||
conn.execute(sql, list(data.values()) + list(where_params))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def fetch_one(table: str, where: str = "", params: tuple = ()) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch a single row as dict."""
|
||||
_validate_table(table)
|
||||
conn = get_connection()
|
||||
sql = f"SELECT * FROM {table}"
|
||||
if where:
|
||||
sql += f" WHERE {where}"
|
||||
sql += " LIMIT 1"
|
||||
row = conn.execute(sql, params).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def fetch_all(table: str, where: str = "", params: tuple = (),
|
||||
order_by: str = "", limit: int = 0) -> List[Dict[str, Any]]:
|
||||
"""Fetch all matching rows as list of dicts."""
|
||||
_validate_table(table)
|
||||
conn = get_connection()
|
||||
sql = f"SELECT * FROM {table}"
|
||||
if where:
|
||||
sql += f" WHERE {where}"
|
||||
if order_by:
|
||||
sql += f" ORDER BY {order_by}"
|
||||
if limit:
|
||||
sql += f" LIMIT {int(limit)}"
|
||||
rows = conn.execute(sql, params).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def query(sql: str, params: tuple = ()) -> List[Dict[str, Any]]:
|
||||
"""Run a raw query."""
|
||||
conn = get_connection()
|
||||
rows = conn.execute(sql, params).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def execute(sql: str, params: tuple = ()):
|
||||
"""Run a raw execute."""
|
||||
conn = get_connection()
|
||||
conn.execute(sql, params)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def cleanup(days: int = 30):
|
||||
"""Remove data older than N days."""
|
||||
cutoff = time.time() - (days * 86400)
|
||||
conn = get_connection()
|
||||
for table in ["tool_invocations", "outcome_signals"]:
|
||||
conn.execute(f"DELETE FROM {table} WHERE created_at < ?", (cutoff,))
|
||||
conn.commit()
|
||||
logger.info("Cleaned up data older than %d days", days)
|
||||
Loading…
Add table
Add a link
Reference in a new issue