182 lines
7.4 KiB
Python
182 lines
7.4 KiB
Python
"""Смена мастера и закрытие чата: утренний сид, хендаут, «новый день», дистиллятор."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
from beaver_gateway.frontends.turn_record import slugify
|
|
|
|
from beaver_agent.memory.recall import notes_head
|
|
from beaver_agent.vault import (
|
|
DAYS,
|
|
DIARY,
|
|
DIGESTS,
|
|
GRANULES,
|
|
PORTRAIT,
|
|
STATE,
|
|
VAULT,
|
|
ZONE,
|
|
read,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from beaver_gateway.conversations.distill import DistillContext
|
|
from beaver_gateway.conversations.rotation import HandoutContext
|
|
from beaver_gateway.conversations.texts import NewDayContext, SeedContext
|
|
|
|
|
|
def latest_handout() -> Path | None:
|
|
"""Последний хендаут, а не «вчерашний по календарю»."""
|
|
files = sorted(DAYS.glob("????-??-??.md")) if DAYS.exists() else []
|
|
return files[-1] if files else None
|
|
|
|
|
|
PROC_INIT = Path("/proc/1")
|
|
"""mtime PID 1 - момент старта контейнера, проверено в бою."""
|
|
PACKAGE = Path(__file__).resolve().parent.parent
|
|
"""Корень пакета: свежесть кода меряем от себя, а не от пути в контейнере."""
|
|
|
|
DEPLOY_MARK = "Выкат: процесс поднят {start} ({ago}), свежесть кода {code} - {verdict}."
|
|
CODE_OLDER = "код старше старта, значит крутится то, что выкатывали"
|
|
CODE_NEWER = "код НОВЕЕ старта: правка на диске есть, рестарта после неё не было"
|
|
SAME_MINUTE = "выкат и правка в одну минуту"
|
|
HOUR = 3600
|
|
|
|
|
|
def _ago(seconds: float) -> str:
|
|
"""Целыми часами, а меньше часа - минутами."""
|
|
if seconds < HOUR:
|
|
return f"{int(seconds // 60)} мин назад"
|
|
return f"{int(seconds // HOUR)} ч назад"
|
|
|
|
|
|
def _clock(stamp: float) -> str:
|
|
return f"{datetime.fromtimestamp(stamp, ZONE):%Y-%m-%d %H:%M}"
|
|
|
|
|
|
def deploy_mark(proc: Path = PROC_INIT, package: Path = PACKAGE) -> str | None:
|
|
"""Когда подняли процесс и насколько свеж код, который в нём крутится.
|
|
|
|
Две цифры рядом: mtime PID 1 - старт контейнера, последний изменённый
|
|
`*.py` пакета - свежесть исходников. Порознь каждая врёт: код бывает
|
|
новее старта, и тогда правка на диске есть, а рестарта после неё не было.
|
|
Любая осечка (нет `/proc`, нет прав, пустой пакет) - None: сид не имеет
|
|
права упасть из-за этой отметки.
|
|
"""
|
|
try:
|
|
started = proc.stat().st_mtime
|
|
sources = [
|
|
path.stat().st_mtime
|
|
for path in package.rglob("*.py")
|
|
if "__pycache__" not in path.parts
|
|
]
|
|
if not sources:
|
|
return None
|
|
code = max(sources)
|
|
gap = code - started
|
|
if abs(gap) < 60:
|
|
verdict = SAME_MINUTE
|
|
elif gap > 0:
|
|
verdict = CODE_NEWER
|
|
else:
|
|
verdict = CODE_OLDER
|
|
return DEPLOY_MARK.format(
|
|
start=_clock(started),
|
|
ago=_ago(time.time() - started),
|
|
code=_clock(code),
|
|
verdict=verdict,
|
|
)
|
|
except (OSError, ValueError):
|
|
return None
|
|
|
|
|
|
def seed_body(ctx: SeedContext) -> str | None:
|
|
"""Утренний сид: хендаут, шапка состояния, портрет Бобра, отметка о выкате."""
|
|
if ctx.seed != "morning":
|
|
return None
|
|
handout = latest_handout()
|
|
if handout is None:
|
|
return "Хендаут не приехал."
|
|
parts = [
|
|
f"Хендаут ({handout.stem}):\n\n{handout.read_text(encoding='utf-8').strip()}"
|
|
]
|
|
signal = "\n".join((read(STATE) or "").splitlines()[:5])
|
|
if signal:
|
|
parts.append(
|
|
f"Состояние (первые строки, полностью в {STATE.relative_to(VAULT)}):\n"
|
|
f"{signal}"
|
|
)
|
|
portrait = notes_head(PORTRAIT, ("сейчас", "паттерны"), cap=40)
|
|
if portrait:
|
|
parts.append(
|
|
"Портрет Бобра (шапка "
|
|
f"`{PORTRAIT.relative_to(VAULT)}`, лента - там же):\n{portrait}"
|
|
)
|
|
mark = deploy_mark()
|
|
if mark:
|
|
parts.append(mark)
|
|
return "\n\n".join(parts)
|
|
|
|
|
|
DEFAULT_HANDOUT = (
|
|
"Этот мастер закрывается ({reason}). Напиши хендаут за {day} в "
|
|
"`мета/бобер/дни/{day}.md`: справку на утро, не задание."
|
|
)
|
|
|
|
|
|
def handout_prompt(ctx: HandoutContext) -> str:
|
|
day = ctx.day.isoformat()
|
|
body = (
|
|
(read(GRANULES / "хендаут.md") or DEFAULT_HANDOUT)
|
|
.replace("{day}", day)
|
|
.replace("{reason}", ctx.reason)
|
|
)
|
|
if (DIARY / f"{day}.md").exists():
|
|
return f"{body}\n\nДневник за {day} приехал: `📅 дни/{day}.md`, возьми из него."
|
|
return f"{body}\n\nДневник за {day} не приехал - так и напиши в разделе «дневник»."
|
|
|
|
|
|
NEW_DAY = (
|
|
"Новый день ({day}). Ты - новый мастер того же треда: хендаут и сигнал состояния "
|
|
"в сиде выше. Скажи Бобру что-то через say, только если есть что сказать."
|
|
)
|
|
SAME_DAY = (
|
|
"Мастер пересоздан ({why}), день тот же - {day}. Хендаут в сиде выше - справка от "
|
|
"предыдущего мастера за сегодня, не утро: продолжай с того места, ничего не "
|
|
"приветствуй. Скажи Бобру что-то через say, только если есть что сказать."
|
|
)
|
|
ROTATION_WHY = {
|
|
"age": "прошло больше 36 часов",
|
|
"context": "контекст стал слишком большим",
|
|
}
|
|
|
|
|
|
def new_day(ctx: NewDayContext) -> str:
|
|
"""«Новый день» - только ночная ротация; остальные причины - тот же день."""
|
|
day = ctx.day.isoformat()
|
|
if ctx.reason == "night":
|
|
return NEW_DAY.format(day=day)
|
|
return SAME_DAY.format(day=day, why=ROTATION_WHY.get(ctx.reason, ctx.reason))
|
|
|
|
|
|
DISTILL = (
|
|
"Глубокий чат [[{chat}]] закрыт ({reason}), сегодня {day}. Выжимка - `Write` в "
|
|
'`{path}`, фронтматтер `source: "[[{chat}]]"`, `date: {day}`; слив - текст '
|
|
"ответа, до 5 строк, третье лицо."
|
|
)
|
|
DISTILL_NO_MEMORY = (
|
|
"Глубокий чат [[{chat}]] закрыт ({reason}), сегодня {day}. Память для него "
|
|
"выключена: файл не пиши, только слив текстом ответа - до 5 строк, третье лицо."
|
|
)
|
|
|
|
|
|
def distill_prompt(ctx: DistillContext) -> str:
|
|
day = ctx.day.isoformat()
|
|
if not ctx.memory:
|
|
return DISTILL_NO_MEMORY.format(chat=ctx.chat_name, reason=ctx.reason, day=day)
|
|
path = DIGESTS / f"{day} - {slugify(ctx.chat_name, maxlen=60)}.md"
|
|
return DISTILL.format(chat=ctx.chat_name, reason=ctx.reason, day=day, path=path)
|