feat(hands,memory): the recall carries the next week of both calendars daily
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
Gateway путей не знает; здесь - что искать по тексту сообщения и куда писать
|
||||
сказанное. ``Recall.block`` отдаёт указатели, не выводы: карточка упомянутого
|
||||
человека, его записки о нём (`мета/бобер/наблюдения/<имя> - наблюдения.md`),
|
||||
дни дневника, где он встречался, и раз в день - подошедшие сроки из досок.
|
||||
дни дневника, где он встречался, и раз в день - подошедшие сроки из досок и
|
||||
ближайшая неделя календарей.
|
||||
``ReplyLog.write`` кладёт каждую реплику Бобра из мастера и веток в
|
||||
`мета/бобер/реплики/YYYY-MM.md` - единственное место, где они grep-абельны:
|
||||
транскрипт живёт в Postgres, а глубокие чаты и так лежат файлами в `💬 чаты/`.
|
||||
@@ -11,6 +12,7 @@ Gateway путей не знает; здесь - что искать по тек
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
@@ -19,14 +21,22 @@ from typing import TYPE_CHECKING
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from beaver_gateway.conversations.envelope import RecallContext
|
||||
from beaver_gateway.conversations.texts import UserSaid
|
||||
|
||||
from beaver_agent.hands.calendars import Calendars, Event, Upcoming
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
LABEL = "справка (указатели по сообщению, не выводы; открой, если относится к вопросу):"
|
||||
VOWELS = "аеёиоуыэюя"
|
||||
KINDS = {"master": "мастер", "branch": "ветка"}
|
||||
CALENDAR = "🗓 календарь"
|
||||
CALENDAR_LIMIT = 15
|
||||
CALENDAR_ERROR_MAX = 90
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -200,11 +210,14 @@ class Recall:
|
||||
"""Сколько последних дней дневника просматривать на упоминания."""
|
||||
mute: frozenset[str] = frozenset()
|
||||
"""Агенты, которым справку не показывают: чужие люди, чужие уши."""
|
||||
calendar: Calendars | None = None
|
||||
"""Ical-фиды календарей; None - календари не настроены, секции нет вовсе."""
|
||||
_people: People | None = field(default=None, init=False, repr=False)
|
||||
_diary_cache: dict[str, tuple[float, list[str]]] = field(
|
||||
default_factory=dict, init=False, repr=False
|
||||
)
|
||||
_deadlines_day: date | None = field(default=None, init=False, repr=False)
|
||||
_calendar_day: date | None = field(default=None, init=False, repr=False)
|
||||
|
||||
@property
|
||||
def people(self) -> People:
|
||||
@@ -219,6 +232,7 @@ class Recall:
|
||||
lines = [self.person_line(p) for p in mentions(ctx.text, self.people.all())]
|
||||
if ctx.kind == "master":
|
||||
lines += self.deadlines(today)
|
||||
lines += self.calendar_lines(ctx.now, today)
|
||||
if not lines:
|
||||
return None
|
||||
return "\n".join([LABEL, *lines])
|
||||
@@ -302,6 +316,51 @@ class Recall:
|
||||
found.sort()
|
||||
return [f"📆 {due:%m-%d} {board}: {task}" for due, board, task in found[:limit]]
|
||||
|
||||
def calendar_lines(self, now: datetime, today: date) -> list[str]:
|
||||
"""Ближайшая неделя календарей - раз в день, следом за сроками досок.
|
||||
|
||||
Секция печатается всегда, когда до неё дошло: пустое окно - строкой
|
||||
«событий нет», упавший фид - строкой «ОШИБКА» и записью в лог. Молча
|
||||
пропасть она не может, иначе отказ канала неотличим от спокойного
|
||||
календаря - так однажды и пролежала незамеченной сессия в USOS.
|
||||
"""
|
||||
if self.calendar is None or self._calendar_day == today:
|
||||
return []
|
||||
self._calendar_day = today
|
||||
try:
|
||||
found = self.calendar.upcoming(now)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_log.exception("календарь: справка не собралась")
|
||||
reason = " ".join(f"{type(exc).__name__}: {exc}".split())
|
||||
return [f"{CALENDAR}: ОШИБКА {reason[:CALENDAR_ERROR_MAX]}"]
|
||||
lines = [f"{CALENDAR}: ОШИБКА {reason}" for reason in found.errors]
|
||||
if found.read:
|
||||
lines += self.calendar_window(found, self.calendar.days)
|
||||
return lines or [f"{CALENDAR}: ОШИБКА фиды не ответили"]
|
||||
|
||||
def calendar_window(self, found: Upcoming, days: int) -> list[str]:
|
||||
"""Окно ответивших фидов: заголовок с временем данных и события строками."""
|
||||
stamp = f"{found.at:%Y-%m-%d %H:%M}"
|
||||
if not found.events:
|
||||
return [f"{CALENDAR} ({days} дней): событий нет, данные на {stamp}"]
|
||||
zone = ZoneInfo(self.tz)
|
||||
shown = found.events[:CALENDAR_LIMIT]
|
||||
lines = [
|
||||
f"{CALENDAR} ({days} дней), событий {len(found.events)}, данные на {stamp}:"
|
||||
]
|
||||
lines += [f" - {event_line(event, zone)}" for event in shown]
|
||||
if len(found.events) > len(shown):
|
||||
lines.append(f" … и ещё {len(found.events) - len(shown)} в окне")
|
||||
return lines
|
||||
|
||||
|
||||
def event_line(event: Event, zone: ZoneInfo) -> str:
|
||||
"""Дата, начало, название и место - одной строкой в зоне агента."""
|
||||
start = event.start.astimezone(zone)
|
||||
when = "весь день" if event.all_day else f"{start:%H:%M}"
|
||||
place = f" · {event.place}" if event.place else ""
|
||||
return f"{start:%m-%d} {when} {event.title}{place}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReplyLog:
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from beaver_gateway.vault.watch import VaultWatch, WatchRules
|
||||
|
||||
from beaver_agent.hands import CALENDARS
|
||||
from beaver_agent.memory.curator import Watched
|
||||
from beaver_agent.memory.recall import Recall, ReplyLog
|
||||
from beaver_agent.vault import (
|
||||
@@ -54,6 +55,7 @@ recall = Recall(
|
||||
boards_dir=BOARDS,
|
||||
tz=TZ,
|
||||
mute=frozenset({VOICE}),
|
||||
calendar=CALENDARS,
|
||||
)
|
||||
"""Голосовому агенту справки нет: за колонкой может стоять кто угодно."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user