106 lines
5.9 KiB
Python
106 lines
5.9 KiB
Python
"""Persistence models for the standalone position-collaboration workspace.
|
|
|
|
This deliberately does **not** reuse ``roundtable_drafts`` or
|
|
``roundtable_jobs``. A session captures the selected business chain and the
|
|
confirmed task intent; every chain seat then has one durable node record.
|
|
LangGraph threads remain the execution source, while these tables also retain
|
|
database-owned recovery copies of conversations and text deliverables so the
|
|
workspace survives replacement of container-local checkpoint/output volumes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlalchemy import ForeignKey, Integer, String, UniqueConstraint
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from deerflow.persistence.base import Base
|
|
from deerflow.persistence.types import BeijingDateTime, PortableLongText
|
|
|
|
|
|
class PositionRoundtableSessionRow(Base):
|
|
__tablename__ = "position_roundtable_sessions"
|
|
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
# Phase one uses normal account isolation. A future membership / role ACL
|
|
# can be added without changing the session or thread identifiers.
|
|
user_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
external_task_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
|
|
task_snapshot: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
chain_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
|
chain_snapshot: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
intent_thread_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
intent_snapshot: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
# Database-owned recovery copy for the complete Step-1 conversation. The
|
|
# normal LangGraph checkpoint remains the execution source, while this
|
|
# snapshot lets the position workspace recover after a checkpoint volume
|
|
# or container-local store has been replaced.
|
|
intent_conversation: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
# The two built-in closing agents own normal LangGraph threads too. Keep
|
|
# their ids on the position session so a refresh can continue the same
|
|
# report/planning dialogue instead of silently starting a fresh context.
|
|
summary_thread_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
summary_snapshot: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
summary_conversation: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
action_plan_thread_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
action_plan_snapshot: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
action_plan_conversation: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
# intent_pending -> active -> completed / archived. The router owns legal
|
|
# transitions; the DB keeps it as a concise, portable string.
|
|
status: Mapped[str] = mapped_column(String(32), nullable=False, default="intent_pending", index=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC)
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
BeijingDateTime(),
|
|
nullable=False,
|
|
default=lambda: datetime.now(UTC),
|
|
onupdate=lambda: datetime.now(UTC),
|
|
)
|
|
|
|
|
|
class PositionRoundtableNodeRow(Base):
|
|
__tablename__ = "position_roundtable_nodes"
|
|
__table_args__ = (
|
|
UniqueConstraint("session_id", "node_key", name="uq_position_roundtable_nodes_session_key"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
session_id: Mapped[str] = mapped_column(
|
|
String(64),
|
|
ForeignKey("position_roundtable_sessions.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
# Stable inside one frozen chain snapshot; e.g. ``stage-1-seat-2-agent``.
|
|
node_key: Mapped[str] = mapped_column(String(256), nullable=False)
|
|
stage_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
seat_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
agent_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
|
|
position_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
|
# The LangGraph/checkpoint store remains the execution source for messages.
|
|
thread_id: Mapped[str | None] = mapped_column(String(128), nullable=True, unique=True)
|
|
# locked / ready / running / done / stale / rejected / error
|
|
status: Mapped[str] = mapped_column(String(32), nullable=False, default="locked", index=True)
|
|
latest_answer: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
artifact_manifest: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
# A complete, JSON-serialisable copy of the node chat plus its composer
|
|
# draft. It is intentionally colocated with the node so deleting a
|
|
# position Session removes every recovery snapshot through the FK cascade.
|
|
conversation_snapshot: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
revision: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
upstream_revision: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
rejection_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
|
last_rejection: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
|
invalidated_by: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC)
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
BeijingDateTime(),
|
|
nullable=False,
|
|
default=lambda: datetime.now(UTC),
|
|
onupdate=lambda: datetime.now(UTC),
|
|
)
|