from datetime import datetime, timezone from sqlalchemy import String, Text, ForeignKey, Integer, Float, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.database import Base from app.db.types import UtcDateTime class Session(Base): __tablename__ = "sessions" id: Mapped[str] = mapped_column(String, primary_key=True) title: Mapped[str] = mapped_column(String, default="New Chat") model: Mapped[str] = mapped_column(String, default="") created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) updated_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) messages: Mapped[list["Message"]] = relationship(back_populates="session", cascade="all, delete-orphan", order_by="Message.created_at") memories: Mapped[list["Memory"]] = relationship(back_populates="session", cascade="all, delete-orphan") note_links: Mapped[list["SessionNotePage"]] = relationship(back_populates="session", cascade="all, delete-orphan") class Message(Base): __tablename__ = "messages" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) session_id: Mapped[str] = mapped_column(ForeignKey("sessions.id", ondelete="CASCADE")) role: Mapped[str] = mapped_column(String) # "user" | "assistant" | "system" content: Mapped[str] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) # 要約されて長期メモリに移ったメッセージ。LLMに渡すコンテキストからは外れるが、 # 人間が後から履歴を確認できるようDBには残す(app.core.memory_kb.memory.maybe_summarize を参照)。 archived_at: Mapped[datetime | None] = mapped_column(UtcDateTime, nullable=True) # どの要約に取り込まれたか。要約を削除してもメッセージ自体は残す(SET NULL)。 memory_id: Mapped[int | None] = mapped_column( ForeignKey("memories.id", ondelete="SET NULL"), nullable=True, ) session: Mapped["Session"] = relationship(back_populates="messages") tags: Mapped[list["MessageTag"]] = relationship(back_populates="message", cascade="all, delete-orphan") class MessageTag(Base): __tablename__ = "message_tags" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) message_id: Mapped[int] = mapped_column(ForeignKey("messages.id", ondelete="CASCADE")) tag: Mapped[str] = mapped_column(String) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) message: Mapped["Message"] = relationship(back_populates="tags") class Memory(Base): __tablename__ = "memories" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) session_id: Mapped[str] = mapped_column(ForeignKey("sessions.id", ondelete="CASCADE")) summary: Mapped[str] = mapped_column(Text) embedding_json: Mapped[str | None] = mapped_column(Text, nullable=True) # 将来のベクトル検索用 created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) session: Mapped["Session"] = relationship(back_populates="memories") tags: Mapped[list["MemoryTag"]] = relationship(back_populates="memory", cascade="all, delete-orphan") class MemoryTag(Base): __tablename__ = "memory_tags" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) memory_id: Mapped[int] = mapped_column(ForeignKey("memories.id", ondelete="CASCADE")) tag: Mapped[str] = mapped_column(String) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) memory: Mapped["Memory"] = relationship(back_populates="tags") class KnowledgeEntry(Base): """Web検索で得た知識。セッションを横断して蓄積し、以後の会話のシステムプロンプトに注入する。""" __tablename__ = "knowledge_entries" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) query: Mapped[str] = mapped_column(String) summary: Mapped[str] = mapped_column(Text) # システムプロンプトには本文ではなくこの一行要約だけを並べる(app.core.memory_kb.knowledge を参照)。 # このカラム追加より前に作られた行では None になるため、表示側で summary から導出する。 headline: Mapped[str | None] = mapped_column(String, nullable=True) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) sources: Mapped[list["KnowledgeSource"]] = relationship(back_populates="entry", cascade="all, delete-orphan") class KnowledgeSource(Base): __tablename__ = "knowledge_sources" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) entry_id: Mapped[int] = mapped_column(ForeignKey("knowledge_entries.id", ondelete="CASCADE")) title: Mapped[str] = mapped_column(String) url: Mapped[str] = mapped_column(String) entry: Mapped["KnowledgeEntry"] = relationship(back_populates="sources") class LocalKnowledgeDoc(Base): """backend/knowledge_base/ 配下のMarkdownファイルとその埋め込みベクトル。 話題に応じて関連ファイルをシステムプロンプトに注入するために使う。 ファイル内容のハッシュか埋め込みモデルが変わった時だけ埋め込みを再計算する。 """ __tablename__ = "local_knowledge_docs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) path: Mapped[str] = mapped_column(String, unique=True) title: Mapped[str] = mapped_column(String) content_hash: Mapped[str] = mapped_column(String) embedding_json: Mapped[str] = mapped_column(Text) # この埋め込みを計算したモデル名。モデルを切り替えた行を見分けて埋め直すために持つ # (次元数の違うベクトル同士を比較しても意味のないスコアしか出ないため)。 # このカラムより前に作られた行は NULL で、モデル不明として埋め直し対象になる。 embedding_model: Mapped[str | None] = mapped_column(String, nullable=True) updated_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) class NotePage(Base): """ユーザーとAIが共同で編集する共有ノートの1ページ。UIではタブとして表示する。 ページのタイトル一覧は毎回システムプロンプトに注入され、本文はモデルが read_note ツールで読み出す(app.core.notes を参照)。ただし SessionNotePage で セッションに紐付けられたページは、本文もそのままシステムプロンプトに載る。 """ __tablename__ = "note_pages" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) title: Mapped[str] = mapped_column(String) content: Mapped[str] = mapped_column(Text, default="") position: Mapped[int] = mapped_column(Integer, default=0) # タブの並び順(セクションでまとめる際、同じセクションのページは連続させる) section: Mapped[str | None] = mapped_column(String, nullable=True) # タブをグループ化するセクション名。未設定なら未分類 created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) updated_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) session_links: Mapped[list["SessionNotePage"]] = relationship(back_populates="page", cascade="all, delete-orphan") class SessionNotePage(Base): """「このセッションではこのページの本文をプロンプトに載せる」という指定。 通常、共有ノートはタイトル一覧だけがシステムプロンプトに入り、本文はモデルが read_note ツールで取りに行く。ここに登録されたページはそれを待たず、本文が毎回 そのままシステムプロンプトに入る(app.core.notes.get_session_notes_context を参照)。 セッション単位なので、会話ごとに違う組み合わせを選べる。 """ __tablename__ = "session_note_pages" __table_args__ = (UniqueConstraint("session_id", "page_id", name="uq_session_note_page"),) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) session_id: Mapped[str] = mapped_column(ForeignKey("sessions.id", ondelete="CASCADE")) page_id: Mapped[int] = mapped_column(ForeignKey("note_pages.id", ondelete="CASCADE")) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) session: Mapped["Session"] = relationship(back_populates="note_links") page: Mapped["NotePage"] = relationship(back_populates="session_links") class AppSetting(Base): """GUIの設定画面から編集する接続情報(APIキー等)。app.core.settings_store が読み書きする。""" __tablename__ = "app_settings" key: Mapped[str] = mapped_column(String, primary_key=True) value: Mapped[str] = mapped_column(Text) updated_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) class ModelPreset(Base): """モデル選択プルダウン(ModelSelector.vue)に表示するプリセット。GUIの設定画面から編集する。""" __tablename__ = "model_presets" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) label: Mapped[str] = mapped_column(String) value: Mapped[str] = mapped_column(String, unique=True) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) class ApiLog(Base): __tablename__ = "api_logs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) session_id: Mapped[str | None] = mapped_column(ForeignKey("sessions.id", ondelete="SET NULL"), nullable=True) model: Mapped[str] = mapped_column(String) request_json: Mapped[str] = mapped_column(Text) # JSON: messages array response_text: Mapped[str | None] = mapped_column(Text, nullable=True) error: Mapped[str | None] = mapped_column(Text, nullable=True) latency_ms: Mapped[float | None] = mapped_column(Float, nullable=True) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) class Reminder(Base): """Discord Bot がメンションで受け付けた一回きりのリマインド。 登録は app.core.discord.reminders の create_reminder ツール、配信は同モジュールの deliver_due_reminders() を APScheduler が定期的に呼ぶことで行う。 日時は他のテーブルと同じくUTCで持ち、ユーザーとのやり取りの際にローカル時刻へ直す。 """ __tablename__ = "reminders" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) channel_id: Mapped[str] = mapped_column(String) # 登録された(=配信先の)チャンネル user_id: Mapped[str] = mapped_column(String) # 依頼者。配信時にメンションする user_name: Mapped[str] = mapped_column(String) # 一覧表示とログ用の表示名 content: Mapped[str] = mapped_column(Text) due_at: Mapped[datetime] = mapped_column(UtcDateTime) created_at: Mapped[datetime] = mapped_column(UtcDateTime, default=lambda: datetime.now(timezone.utc)) # 配信済み。配信を諦めた場合もここを立てて再試行を止める(deliver_due_reminders を参照) notified_at: Mapped[datetime | None] = mapped_column(UtcDateTime, nullable=True) cancelled_at: Mapped[datetime | None] = mapped_column(UtcDateTime, nullable=True)