refactor: split flat core into capability packages, layer the conversations service, English defaults for every model-facing text

This commit is contained in:
hh
2026-09-02 00:13:20 +02:00
parent b96714338f
commit cae2ed4161
77 changed files with 2987 additions and 2944 deletions
@@ -0,0 +1,141 @@
"""The envelope: a background block under the user's text - clock, changes, recall."""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from zoneinfo import ZoneInfo
from beaver_gateway.conversations.texts import EnvelopeTexts
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from beaver_gateway.vault.watch import Change, VaultWatch
__all__ = ["Envelope", "RecallContext", "render"]
_log = logging.getLogger(__name__)
@dataclass(frozen=True, slots=True)
class RecallContext:
text: str
kind: str
now: datetime
@dataclass(slots=True)
class Envelope:
watch: VaultWatch | None = None
tz: str = "UTC"
max_lines: int = 120
per_file: int = 30
names_only_within: float = 600.0
last_at: datetime | None = None
recall: Callable[[RecallContext], str | None] | None = None
"""Setup-side lookup on the user's text; its lines go under the change block."""
texts: EnvelopeTexts = field(default_factory=EnvelopeTexts)
def build(
self, *, now: datetime | None = None, text: str = "", kind: str = "master"
) -> str:
now = now or datetime.now(UTC)
changes = self.watch.take() if self.watch is not None else []
names_only = (
self.last_at is not None
and (now - self.last_at).total_seconds() < self.names_only_within
)
out = render(
now=now,
tz=self.tz,
changes=changes,
since=self.last_at,
names_only=names_only,
max_lines=self.max_lines,
per_file=self.per_file,
texts=self.texts,
)
self.last_at = now
block = self.recall_block(text=text, kind=kind, now=now)
return f"{out}\n{block}" if block else out
def recall_only(
self, *, text: str, kind: str, now: datetime | None = None
) -> str | None:
block = self.recall_block(text=text, kind=kind, now=now or datetime.now(UTC))
return f"{self.texts.header}\n{block}" if block else None
def recall_block(self, *, text: str, kind: str, now: datetime) -> str | None:
if self.recall is None or not text.strip():
return None
try:
block = self.recall(RecallContext(text=text, kind=kind, now=now))
except Exception: # noqa: BLE001
_log.exception("recall hook failed")
return None
return block.strip() or None if block else None
def render(
*,
now: datetime,
tz: str,
changes: Sequence[Change],
since: datetime | None,
names_only: bool,
max_lines: int = 120,
per_file: int = 30,
texts: EnvelopeTexts | None = None,
) -> str:
texts = texts or EnvelopeTexts()
zone = ZoneInfo(tz)
stamp = now.astimezone(zone)
lines = [
texts.header,
texts.time.format(stamp=f"{stamp:%Y-%m-%d %H:%M}", zone=_zone_label(tz)),
]
ordered = sorted(changes, key=lambda c: (not c.full, c.path))
since_label = (
texts.since.format(time=f"{since.astimezone(zone):%H:%M}")
if since is not None
else texts.since_start
)
if ordered:
names = ", ".join(f"{c.path} (+{c.added_count})" for c in ordered)
lines.append(texts.changed.format(since=since_label, names=names))
if not names_only:
_append_diffs(
lines, ordered, max_lines=max_lines, per_file=per_file, texts=texts
)
return "\n".join(lines[:max_lines])
def _append_diffs(
lines: list[str],
changes: Sequence[Change],
*,
max_lines: int,
per_file: int,
texts: EnvelopeTexts,
) -> None:
budget = max_lines - len(lines) - 1
for change in changes:
if not change.full or not change.added:
continue
if budget < 3:
lines.append(texts.truncated)
return
shown = change.added[: min(per_file, budget - 2)]
lines.append(texts.file_header.format(path=change.path))
lines.extend(f"+ {line}" for line in shown)
budget -= 1 + len(shown)
if len(change.added) > len(shown):
lines.append(texts.more_lines.format(count=len(change.added) - len(shown)))
budget -= 1
def _zone_label(tz: str) -> str:
return tz.rsplit("/", 1)[-1].replace("_", " ")