"""Abgeglichene Ordner: Dokumente vom Rechner einer Person, die sich regelmäßig ändern. Der Browser gleicht einen lokalen Ordner mit Bifröst ab (nur Neues und Geändertes); gespeichert wird der ausgelesene Text, nicht das Original. Office-Dokumente, PDF und Bilder liest ein zentraler MCP-Server (Rolle „documents“) aus, Textdateien Bifröst selbst. Sichtbar je Ordner: nur für die Person, die abgleicht (nur ihre eigenen Runs in ihren eigenen Chats), oder für den ganzen Workspace (mit Bestätigung). """ from datetime import datetime from sqlalchemy import BigInteger, Computed, ForeignKey, Identity, Index, String, Text, UniqueConstraint from sqlalchemy.dialects.postgresql import TSVECTOR from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base, now PRIVATE, WORKSPACE = "private", "workspace" # Datei: wartet auf Auslese | ausgelesen | Fehler | Format ohne Auslese PENDING, OK, ERROR, UNSUPPORTED = "pending", "ok", "error", "unsupported" class DocFolder(Base): __tablename__ = "doc_folders" id: Mapped[int] = mapped_column(primary_key=True) workspace_id: Mapped[int] = mapped_column(ForeignKey("workspaces.id", ondelete="CASCADE"), index=True) owner_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) name: Mapped[str] = mapped_column(String(100)) scope: Mapped[str] = mapped_column(String(20), default=PRIVATE) # private | workspace exposure_confirmed: Mapped[bool] = mapped_column(default=False) synced: Mapped[datetime | None] # letzter vollständiger Abgleich created: Mapped[datetime] = mapped_column(default=now) class DocFile(Base): __tablename__ = "doc_files" __table_args__ = (UniqueConstraint("folder_id", "path"), Index("ix_doc_files_search", "search", postgresql_using="gin")) id: Mapped[int] = mapped_column(BigInteger, Identity(), primary_key=True) folder_id: Mapped[int] = mapped_column(ForeignKey("doc_folders.id", ondelete="CASCADE"), index=True) path: Mapped[str] = mapped_column(String(500)) # relativ zum Ordner size: Mapped[int] = mapped_column(BigInteger) modified: Mapped[int] = mapped_column(BigInteger) # lastModified im Browser (ms); erkennt Änderungen sha256: Mapped[str] = mapped_column(String(64)) status: Mapped[str] = mapped_column(String(20), default=PENDING) error: Mapped[str] = mapped_column(Text, default="") text: Mapped[str] = mapped_column(Text, default="") # Volltextsuche über Pfad und Inhalt (deutsche Stammformen) search = mapped_column(TSVECTOR, Computed("to_tsvector('german', coalesce(path, '') || ' ' || coalesce(text, ''))", persisted=True)) updated: Mapped[datetime] = mapped_column(default=now)