204 lines
8.1 KiB
Python
204 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
||
"""Hermes → Simbiot: историческая память (t-261003150347-896567).
|
||
|
||
Тянет сессии из /root/.hermes/state.db (до CUTOFF — старт харнеса 2026-10-03),
|
||
строит выжимки (диалоговые карточки + помесячные роллапы) и индексирует их
|
||
в RAG-базу симбиота /root/rag/rag.db (та же схема chunks, что и у остального
|
||
корпуса: fastembed BAAI/bge-small-en-v1.5, чанк 900/150).
|
||
|
||
Идемпотентно: перед вставкой чанков пути `hermes-history/...` удаляются.
|
||
Сырые логи не индексируются — только цитаты user + ключевые ответы assistant.
|
||
|
||
Usage:
|
||
hermes_history_ingest.py # полная загрузка
|
||
hermes_history_ingest.py --limit 3 # смоук на 3 сессиях
|
||
hermes_history_ingest.py --dry-run # статистика без записи
|
||
"""
|
||
import argparse
|
||
import os
|
||
import sqlite3
|
||
import sys
|
||
import time
|
||
|
||
sys.path.insert(0, "/root/rag")
|
||
import rag # noqa: E402 (общий индексатор: chunk_text/embed/get_db)
|
||
|
||
HERMES_DB = "/root/.hermes/state.db"
|
||
# Старт текущего харнеса: ивенты до этой даты симбиот «не помнит».
|
||
CUTOFF_UTC = "2026-10-03 00:00:00"
|
||
P_USER, P_ASSIST = 40, 10 # лимиты строк на сессию
|
||
LEN_USER, LEN_ASSIST = 170, 220
|
||
HUMAN_SOURCES = ("telegram", "cli", "matrix")
|
||
|
||
|
||
def iso2epoch(s):
|
||
import calendar
|
||
t = time.strptime(s, "%Y-%m-%d %H:%M:%S")
|
||
return calendar.timegm(t)
|
||
|
||
|
||
CUTOFF_EPOCH = iso2epoch(CUTOFF_UTC)
|
||
|
||
|
||
def clean(s, limit):
|
||
s = " ".join((s or "").split())
|
||
if len(s) > limit:
|
||
s = s[: limit - 1].rstrip() + "…"
|
||
return s
|
||
|
||
|
||
def usable(s):
|
||
s = (s or "").strip()
|
||
if not s or s.startswith("<"): # системные вставки/контейнеры
|
||
return False
|
||
return True
|
||
|
||
|
||
def fetch_sessions(db):
|
||
return db.execute(
|
||
"""SELECT id, source, title, started_at, message_count
|
||
FROM sessions
|
||
WHERE started_at < ?
|
||
ORDER BY started_at""",
|
||
(CUTOFF_EPOCH,),
|
||
).fetchall()
|
||
|
||
|
||
def session_card(db, sid):
|
||
"""Карточка сессии: дата/титул + цитаты user и ключевые ответы assistant."""
|
||
msgs = db.execute(
|
||
"""SELECT role, content, timestamp FROM messages
|
||
WHERE session_id=? AND role IN ('user','assistant')
|
||
ORDER BY id""",
|
||
(sid,),
|
||
).fetchall()
|
||
users = [m for m in msgs if m[0] == "user" and usable(m[1])]
|
||
assists = [m for m in msgs if m[0] == "assistant" and usable(m[1]) and len(m[1]) > 60]
|
||
if not users:
|
||
return None
|
||
u_sel = users[: P_USER // 2] + users[-(P_USER // 2):] if len(users) > P_USER else users
|
||
a_sel = sorted(assists, key=lambda m: -len(m[1]))[:P_ASSIST] if assists else []
|
||
a_pos = {id(m): i for i, m in enumerate(a_sel)}
|
||
lines = []
|
||
for m in msgs:
|
||
if m[0] == "user" and any(m is x for x in u_sel):
|
||
lines.append("- U: " + clean(m[1], LEN_USER))
|
||
elif m[0] == "assistant" and id(m) in a_pos:
|
||
lines.append("- A: " + clean(m[1], LEN_ASSIST))
|
||
return "\n".join(lines)
|
||
|
||
|
||
def month_rollups(sessions_meta, db, human_ids):
|
||
"""Помесячный роллап: список сессий с датой, источником, титулом, первой репликой."""
|
||
by_month = {}
|
||
for sid, source, title, started, mcount in sessions_meta:
|
||
month = time.strftime("%Y-%m", time.gmtime(started))
|
||
first = db.execute(
|
||
"""SELECT content FROM messages
|
||
WHERE session_id=? AND role='user'
|
||
ORDER BY id LIMIT 1""",
|
||
(sid,),
|
||
).fetchone()
|
||
first_txt = clean(first[0], 110) if first and usable(first[0]) else ""
|
||
t = (title or "").strip()
|
||
label = t if t and len(t) > 3 and not t.lower().startswith(("йоу", "привет")) else first_txt
|
||
by_month.setdefault(month, []).append(
|
||
(started, source, label or "(без титула)", mcount, sid in human_ids)
|
||
)
|
||
out = {}
|
||
for month, rows in by_month.items():
|
||
rows.sort()
|
||
parts = [f"# Hermes: история {month} (выжимка сессий до {CUTOFF_UTC})",
|
||
f"Сессий за месяц: {len(rows)}."]
|
||
for started, source, label, mcount, is_human in rows:
|
||
d = time.strftime("%d.%m", time.gmtime(started))
|
||
src = {"telegram": "tg", "cli": "cli", "cron": "cron", "matrix": "mx",
|
||
"subagent": "sub"}.get(source, source)
|
||
star = "" if is_human else " [авто]"
|
||
parts.append(f"- {d} ({src}) «{clean(label, 90)}» msgs={mcount}{star}")
|
||
out[month] = "\n".join(parts)
|
||
return out
|
||
|
||
|
||
def index_chunks(rdb, path, text):
|
||
chunks = rag.chunk_text(text)
|
||
rdb.execute("DELETE FROM chunks WHERE path=?", (path,))
|
||
vecs = rag.embed(chunks)
|
||
for i, (c, v) in enumerate(zip(chunks, vecs)):
|
||
import hashlib
|
||
sha = hashlib.sha256(c.encode()).hexdigest()[:16]
|
||
rdb.execute(
|
||
"INSERT OR REPLACE INTO chunks (path, chunk_no, sha, text, vec, access_level) "
|
||
"VALUES (?,?,?,?,?,1)",
|
||
(path, i, sha, c, json_dumps(v)),
|
||
)
|
||
return len(chunks)
|
||
|
||
|
||
def json_dumps(v):
|
||
import json
|
||
return json.dumps(v)
|
||
|
||
|
||
def main():
|
||
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()
|
||
|
||
db = sqlite3.connect(f"file:{HERMES_DB}?mode=ro", uri=True)
|
||
sessions = fetch_sessions(db)
|
||
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]
|
||
print(f"sessions total={len(sessions)} human={len(human)} auto/cron={len(cron)} "
|
||
f"cutoff={CUTOFF_UTC}")
|
||
|
||
if args.dry_run:
|
||
for sid, source, title, started, mcount in human[:10]:
|
||
print(f" {time.strftime('%Y-%m-%d %H:%M', time.gmtime(started))} "
|
||
f"[{source}] {(title or '')[:60]} msgs={mcount}")
|
||
return
|
||
|
||
rdb = rag.get_db()
|
||
total_chunks, t0 = 0, time.time()
|
||
|
||
# 1) помесячные роллапы (по всем сессиям, включая cron)
|
||
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])
|
||
total_chunks += n
|
||
print(f" monthly/{month}: {n} chunks")
|
||
rdb.commit()
|
||
|
||
# 2) карточки диалоговых сессий
|
||
done = 0
|
||
for sid, source, title, started, mcount in human:
|
||
card = session_card(db, sid)
|
||
done += 1
|
||
if not card:
|
||
print(f" {sid}: пропущена (нет user-сообщений)")
|
||
continue
|
||
d = time.strftime("%Y-%m-%d", time.gmtime(started))
|
||
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)
|
||
total_chunks += n
|
||
if done % 10 == 0 or done == len(human):
|
||
rdb.commit()
|
||
print(f" sessions {done}/{len(human)} (+{total_chunks} chunks, "
|
||
f"{time.time()-t0:.0f}s)")
|
||
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/%'"
|
||
).fetchone()
|
||
print(f"DONE: +{total_chunks} чанков истории за {time.time()-t0:.0f}s; "
|
||
f"всего в rag.db={grand}, hermes-history={hermes[0]} чанков / {hermes[1]} файлов")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|