feat(migration): полный ингест истории Гермеса в стандартную миграцию (--apply, шаг 6.5)

- tools/hermes_history_ingest.py: параметры --hermes-db/--rag-db/--prefix/--cutoff/--sessions-cards; rag.DB перекрывается атрибутом (вендор фиксирует при импорте); cutoff='' = вся история
- scripts/migrate_hermes.py: --history-full по умолчанию ON в --apply (rag_index → ingest), --no-history-full / --history-agent <имя> для префикса
- docs/migration-hermes.md: раздел 6.5
- e2e: полный apply на смоук-базе — +350 чанков / 75 файлов за 24с, history_full.ok=true
This commit is contained in:
simbiot 2026-10-04 13:12:54 +00:00
parent 3dc6ca6ab9
commit b780c9ba9e
3 changed files with 83 additions and 15 deletions

View file

@ -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, визард).

View file

@ -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/<agent> (по умолчанию — базовый префикс)")
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

View file

@ -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/<agent>)")
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__":