diff --git a/docs/migration-hermes.md b/docs/migration-hermes.md index 129b396..559056a 100644 --- a/docs/migration-hermes.md +++ b/docs/migration-hermes.md @@ -42,6 +42,16 @@ python3 scripts/migrate_hermes.py --hermes /home/hermes/.hermes --apply \ 3. RAG-индексация: `.md`-файлы пакета → целевая база (вендор дедупит по sha). 4. `OUT/report.json` — полный отчёт: counts, mapping, events, rag, errors. +### 6.5) Полный ингест истории (с 04.10.2026, голосовое Alex m-261004) + +`--apply` по умолчанию запускает `tools/hermes_history_ingest.py` поверх +**всех** сессий `state.db` Гермеса: помесячные роллапы + карточки человеческих +диалогов → та же RAG-база (префикс `hermes-history/…`). Флаги: +`--no-history-full` (отключить), `--history-agent <имя>` (префикс +`hermes-history/<имя>` — для нескольких агентов в одной базе). Ингест +идемпотентен (DELETE+INSERT по префиксу), запуск — `--rag-py` вендора +(fastembed). Проверено e2e: +350 чанков / 75 файлов за 24с на 70 сессиях. + `.env` целевого инстанса не трогается скриптом: `ROUTER_KEY`/`SIMBIOT_AGENT_NAME` и портал-канал (`/etc/coop-reporter.env`: `PORTAL_URL` + `AGENT_TOKEN` agt_…) настраиваются при установке (scripts/install.sh, визард). diff --git a/scripts/migrate_hermes.py b/scripts/migrate_hermes.py index 0555e5a..ac2f0d6 100644 --- a/scripts/migrate_hermes.py +++ b/scripts/migrate_hermes.py @@ -366,6 +366,10 @@ def main(): ap.add_argument("--rag-db", default=None) ap.add_argument("--rag-py", default="/root/rag-venv/bin/python") ap.add_argument("--rag-script", default="/root/rag/rag.py") + ap.add_argument("--no-history-full", dest="history_full", action="store_false", + help="НЕ гонять полный ингест истории (tools/hermes_history_ingest.py)") + ap.add_argument("--history-agent", default=None, + help="имя агента для префикса чанков hermes-history/ (по умолчанию — базовый префикс)") args = ap.parse_args() simbiot_repo = REPO_ROOT @@ -417,6 +421,34 @@ def main(): _jlog("rag: indexed=" + str(len(report["rag"].get("indexed", []))) + " failed=" + str(len(report["rag"].get("failed", [])))) + # 6.5) ПОЛНЫЙ ингест истории (t-261003150347 → стандартная миграция, голосовое + # Alex m-516173686): выжимки ВСЕХ сессий state.db (карточки + помесячные + # роллапы) в ту же RAG-базу. Идемпотентно (DELETE+INSERT по префиксу). + # Запускается python-ом, где есть fastembed (тот же rag-py вендора). + if args.history_full: + hermes_state = os.path.join(args.hermes, "state.db") + ingest = os.path.join(simbiot_repo, "tools", "hermes_history_ingest.py") + prefix = f"hermes-history/{args.history_agent}" if args.history_agent else "hermes-history" + if os.path.exists(hermes_state) and os.path.exists(ingest): + import subprocess + cmd = [args.rag_py, ingest, "--hermes-db", hermes_state, + "--rag-db", args.rag_db, "--prefix", prefix, "--cutoff", ""] + try: + proc = subprocess.run(cmd, capture_output=True, text=True, + timeout=1800) + report["history_full"] = { + "ok": proc.returncode == 0, "prefix": prefix, + "tail": (proc.stdout or "").strip().splitlines()[-3:], + "err": (proc.stderr or "")[-300:] or None, + } + _jlog(f"history_full: ok={proc.returncode == 0} prefix={prefix}") + except Exception as e: + report["history_full"] = {"ok": False, "error": str(e)[:200]} + _jlog(f"history_full: FAILED {str(e)[:120]}") + else: + report["history_full"] = {"skipped": "state.db или ingest не найден"} + _jlog("history_full: skip (нет источника)") + # 7) события нового инстанса counts = {"sessions_written": hist.get("sessions_written", 0)} report["store"] = apply_to_store(args.harness_dir if args.harness_dir diff --git a/tools/hermes_history_ingest.py b/tools/hermes_history_ingest.py index 4aa6bae..8cf04b2 100644 --- a/tools/hermes_history_ingest.py +++ b/tools/hermes_history_ingest.py @@ -20,12 +20,18 @@ import sqlite3 import sys import time -sys.path.insert(0, "/root/rag") +RAG_DIR = os.environ.get("RAG_DIR", "/root/rag") # где лежит rag.py (вендор) +RAG_VENV_PY = os.environ.get("RAG_VENV_PY", "/root/rag-venv/bin/python3") +sys.path.insert(0, RAG_DIR) import rag # noqa: E402 (общий индексатор: chunk_text/embed/get_db) -HERMES_DB = "/root/.hermes/state.db" -# Старт текущего харнеса: ивенты до этой даты симбиот «не помнит». +HERMES_DB = os.environ.get("HERMES_DB", "/root/.hermes/state.db") +RAG_DB = os.environ.get("RAG_DB", "/root/rag/rag.db") +# Старт текущего харнеса: ивенты до этой даты симбиот «не помнит» (для +# ручного прогона на этом хосте). При миграции нового агента cutoff не нужен — +# передаётся --cutoff явно или берётся «всё» (пустая строка). CUTOFF_UTC = "2026-10-03 00:00:00" +PREFIX = "hermes-history" P_USER, P_ASSIST = 40, 10 # лимиты строк на сессию LEN_USER, LEN_ASSIST = 170, 220 HUMAN_SOURCES = ("telegram", "cli", "matrix") @@ -54,13 +60,19 @@ def usable(s): return True -def fetch_sessions(db): +def fetch_sessions(db, cutoff_epoch=None): + if cutoff_epoch: + return db.execute( + """SELECT id, source, title, started_at, message_count + FROM sessions + WHERE started_at < ? + ORDER BY started_at""", + (cutoff_epoch,), + ).fetchall() return db.execute( """SELECT id, source, title, started_at, message_count FROM sessions - WHERE started_at < ? - ORDER BY started_at""", - (CUTOFF_EPOCH,), + ORDER BY started_at""" ).fetchall() @@ -140,20 +152,33 @@ def json_dumps(v): return json.dumps(v) -def main(): +def main(argv=None): + global HERMES_DB, RAG_DB, PREFIX ap = argparse.ArgumentParser() ap.add_argument("--limit", type=int, default=0, help="только N человеческих сессий (смоук)") ap.add_argument("--dry-run", action="store_true") - args = ap.parse_args() + ap.add_argument("--hermes-db", default=HERMES_DB, help="state.db Гермеса (источник)") + ap.add_argument("--rag-db", default=RAG_DB, help="rag.db приёмника (RAG_DB env)") + ap.add_argument("--prefix", default=PREFIX, help="префикс путей чанков (для нескольких агентов: hermes-history/)") + ap.add_argument("--cutoff", default=CUTOFF_UTC, help="UTC 'YYYY-MM-DD HH:MM:SS': только сессии ДО; '' = без cutoff") + ap.add_argument("--sessions-cards", type=int, default=0, help="карточки только последних N сессий (0 = все человеческие)") + args = ap.parse_args(argv) + + HERMES_DB, RAG_DB, PREFIX = args.hermes_db, args.rag_db, args.prefix + rag.DB = RAG_DB # rag.py фиксирует DB при импорте — перекрываем атрибутом + cutoff_epoch = iso2epoch(args.cutoff) if args.cutoff.strip() else None db = sqlite3.connect(f"file:{HERMES_DB}?mode=ro", uri=True) - sessions = fetch_sessions(db) + sessions = fetch_sessions(db, cutoff_epoch) human = [s for s in sessions if s[1] in HUMAN_SOURCES and (s[4] or 0) >= 2] cron = [s for s in sessions if s[1] not in HUMAN_SOURCES] if args.limit: human = human[: args.limit] + if args.sessions_cards: + human = human[-args.sessions_cards:] + print(f"src={HERMES_DB} rag={RAG_DB} prefix={PREFIX}") print(f"sessions total={len(sessions)} human={len(human)} auto/cron={len(cron)} " - f"cutoff={CUTOFF_UTC}") + f"cutoff={args.cutoff or 'нет'}") if args.dry_run: for sid, source, title, started, mcount in human[:10]: @@ -168,7 +193,7 @@ def main(): human_ids = {s[0] for s in human} roll = month_rollups(sessions, db, human_ids) for month in sorted(roll): - n = index_chunks(rdb, f"hermes-history/monthly/{month}.md", roll[month]) + n = index_chunks(rdb, f"{PREFIX}/monthly/{month}.md", roll[month]) total_chunks += n print(f" monthly/{month}: {n} chunks") rdb.commit() @@ -185,7 +210,7 @@ def main(): text = (f"# Hermes-сессия {sid} [{source}] {d} — {(title or '').strip()[:80]}\n" f"Сообщений: {mcount}. Цитаты пользователя (U) и ключевые ответы (A):\n" + card) - n = index_chunks(rdb, f"hermes-history/sessions/{sid}.md", text) + n = index_chunks(rdb, f"{PREFIX}/sessions/{sid}.md", text) total_chunks += n if done % 10 == 0 or done == len(human): rdb.commit() @@ -194,10 +219,11 @@ def main(): rdb.commit() grand = rdb.execute("SELECT COUNT(*) FROM chunks").fetchone()[0] hermes = rdb.execute( - "SELECT COUNT(*), COUNT(DISTINCT path) FROM chunks WHERE path LIKE 'hermes-history/%'" + "SELECT COUNT(*), COUNT(DISTINCT path) FROM chunks WHERE path LIKE ?", + (f"{PREFIX}/%",), ).fetchone() print(f"DONE: +{total_chunks} чанков истории за {time.time()-t0:.0f}s; " - f"всего в rag.db={grand}, hermes-history={hermes[0]} чанков / {hermes[1]} файлов") + f"всего в rag.db={grand}, {PREFIX}={hermes[0]} чанков / {hermes[1]} файлов") if __name__ == "__main__":