feat: add stateful conversation storage
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
"""SQLModel tables.
|
||||
|
||||
Two tables, both flat, no relationships modelled yet (``actor`` and
|
||||
Four tables, all flat, no FK relationships modelled (``actor`` and
|
||||
``agent_name`` are stored as strings — joining audit→token by name is
|
||||
fine at this volume; we'll introduce FKs when the admin UI actually
|
||||
demands them).
|
||||
|
||||
A ``Session`` table originally lived here for live-session
|
||||
observability. It was dropped after we decided the gateway stays
|
||||
stateless about identity (claude-code-api's in-memory fingerprint pool
|
||||
is the source of truth) and that conversation persistence belongs in a
|
||||
future Obsidian-sync frontend, not a sessions table.
|
||||
The ``Conversation`` + ``ConversationMessage`` pair persists chat
|
||||
history per frontend so we can survive cache misses without losing
|
||||
tool-call memory. The gateway is now stateful about conversation
|
||||
content (we keep the raw Anthropic-shape message list including
|
||||
``tool_use`` / ``tool_result`` blocks); the live ``claude-code-api``
|
||||
session pool stays the source of truth for *fingerprints*, and the DB
|
||||
mirrors what we'd want to re-seed if a session evicts. See
|
||||
``core/conversation_store.py`` for the diff-and-fork logic and
|
||||
``frontends/markdown/frontend.py`` for the integration point.
|
||||
|
||||
Datetimes are stored UTC; we set ``default_factory`` rather than relying
|
||||
on DB defaults so SQLite + Postgres behave identically. Every row that
|
||||
@@ -20,6 +24,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import UniqueConstraint
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
@@ -66,4 +71,58 @@ class AuditLog(SQLModel, table=True):
|
||||
detail_json: str = Field(default="{}")
|
||||
|
||||
|
||||
__all__ = ["AuditLog", "Token"]
|
||||
class Conversation(SQLModel, table=True):
|
||||
"""One chat thread, scoped to a frontend.
|
||||
|
||||
``external_id`` is the identifier the frontend uses to find this
|
||||
thread again on the next request — for the markdown frontend it's a
|
||||
uuid we mint and persist into the file's frontmatter, for the
|
||||
anthropic frontend it'd be the same metadata.conversation_id the
|
||||
client passes. Unique per ``(frontend, external_id)`` because two
|
||||
frontends sharing a uuid is fine; the same frontend reusing one is
|
||||
a bug.
|
||||
"""
|
||||
|
||||
__tablename__ = "conversations"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("frontend", "external_id", name="uq_conv_frontend_extid"),
|
||||
)
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
frontend: str = Field(index=True)
|
||||
external_id: str = Field(index=True)
|
||||
agent_name: str = Field(index=True)
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
updated_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class ConversationMessage(SQLModel, table=True):
|
||||
"""One raw Anthropic-shape message in a conversation's transcript.
|
||||
|
||||
A single user/assistant exchange visible in Obsidian can occupy
|
||||
multiple rows when claude ran a tool cycle: ``assistant``
|
||||
(tool_use), ``user`` (tool_result), ``assistant`` (final text) all
|
||||
live as separate rows with the same conversation_id and monotonic
|
||||
``seq``. ``content_json`` is the canonical Anthropic content payload
|
||||
(string or list-of-blocks) — exactly what we'll feed back to the
|
||||
backend so its session-pool fingerprint matches.
|
||||
|
||||
``seq`` is per-conversation 0-based monotonic; the unique
|
||||
constraint catches the trivial bug of two writers racing on the
|
||||
same conversation.
|
||||
"""
|
||||
|
||||
__tablename__ = "conversation_messages"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("conversation_id", "seq", name="uq_msg_conv_seq"),
|
||||
)
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
conversation_id: int = Field(index=True)
|
||||
seq: int
|
||||
role: str
|
||||
content_json: str
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
__all__ = ["AuditLog", "Conversation", "ConversationMessage", "Token"]
|
||||
|
||||
Reference in New Issue
Block a user