Files
LLeMbas/src/lembas/db/models/report.py
T
HomerandClaude Opus 5.5 9970bb43c6 Data groups: a provider's models read only their own group's data
Every connection is in a data group. Its models are handed, and can find,
only that group's memories, notes, skills, knowledge, reports and
personality -- by search and by id. A chat stays in the group it was
started in: switching its model, the endpoint fallback, the crowd, friends,
bases and the @ menu all stay inside it, and a chat whose model has moved
is refused rather than sent. A group may name its own embedder and image
reviewer. data.manage lets a person make personal groups, remap
connections for themselves and move their own records.

Also: a search no longer mixes two embedders of the same width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:07:55 +00:00

77 lines
3.8 KiB
Python

"""Reports: what was found, written down once and never replied to."""
from __future__ import annotations
from sqlalchemy import Boolean, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from lembas.db.base import Base, Timestamps, UUIDPrimaryKey
from lembas.db.models.data_group import InDataGroup
# Where a report came from. Not a foreign key to anything -- see `source_id`.
SOURCE_SCHEDULE = "schedule"
SOURCE_CHAT = "chat"
SOURCE_MANUAL = "manual"
SOURCES = (SOURCE_SCHEDULE, SOURCE_CHAT, SOURCE_MANUAL)
class Report(UUIDPrimaryKey, Timestamps, InDataGroup, Base):
"""A finished piece of work, filed.
Deliberately not a `Chat` with one `Message` in it. A report is read top to
bottom and never answered, so everything a conversation carries -- a
composer, a sidebar row, a title that regenerates itself, a bubble with an
avatar and a rewind button -- would be machinery to suppress rather than
machinery to use. It is the same line `services/library/` already draws
between a note and a chat: a durable artefact is not a turn.
It must also be writable with no chat behind it at all, being the fallback
destination for a scheduled run whose own chat has gone.
`body` is Markdown written by a model and goes through
`services/markdown.py` like everything else from an endpoint. Hard rule 6
applies here exactly as it does in a transcript.
"""
__tablename__ = "reports"
owner_id: Mapped[str] = mapped_column(
String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
title: Mapped[str] = mapped_column(String(300), nullable=False)
# One line for the list page, so a feed of forty reports can be read without
# opening any of them. Written by the model beside the body; falls back to
# the body's first line when it did not bother.
summary: Mapped[str] = mapped_column(String(500), default="")
body: Mapped[str] = mapped_column(Text, default="")
source: Mapped[str] = mapped_column(String(16), default=SOURCE_MANUAL, nullable=False)
# The chat or the schedule this came out of, kept so a report can say where
# it was made. Deliberately not a ForeignKey: `migrations.py` compiles the
# column type only, so a REFERENCES clause would exist on a fresh database
# and not on an upgraded one -- the same reason `Chat.compacted_through_id`
# and `Folder.ssh_profile_id` are plain ids. Both are validated on read, and
# the row outliving what it points at is normal rather than exceptional: a
# report is worth keeping after the chat that produced it has been deleted.
source_id: Mapped[str] = mapped_column(String(32), default="")
schedule_id: Mapped[str] = mapped_column(String(32), default="")
model_id: Mapped[str] = mapped_column(String(300), default="")
# NOT NULL with a scalar default so `migrations._add_column_sql` can backfill
# it if this column is ever added to a table that already has rows.
unread: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
# Whether its arrival has already been announced. The dot can be shown for
# as long as it is unread; the toast and the browser notification must fire
# once. Without this the poll would announce the same report every ten
# seconds until somebody opened it, which is the shape of notification
# nobody leaves switched on. `Chat.unread_notified` exists for exactly this
# and this is the same pair.
unread_notified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
# Why a run produced nothing worth reading. A scheduled report that failed
# is still a report -- one that silently did not appear is indistinguishable
# from a schedule that never fired.
error: Mapped[str] = mapped_column(Text, default="")
def __repr__(self) -> str:
return f"<Report {self.title!r}>"