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:
parent
3dc6ca6ab9
commit
b780c9ba9e
3 changed files with 83 additions and 15 deletions
|
|
@ -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, визард).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,),
|
||||
(cutoff_epoch,),
|
||||
).fetchall()
|
||||
return db.execute(
|
||||
"""SELECT id, source, title, started_at, message_count
|
||||
FROM sessions
|
||||
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__":
|
||||
|
|
|
|||
Loading…
Reference in a new issue