diff --git a/backend/app/models/ai_session.py b/backend/app/models/ai_session.py index 721924ad..85d5ef4e 100644 --- a/backend/app/models/ai_session.py +++ b/backend/app/models/ai_session.py @@ -20,6 +20,10 @@ if TYPE_CHECKING: from app.models.account import Account from app.models.tree import Tree from app.models.psa_connection import PsaConnection + from app.models.session_branch import SessionBranch + from app.models.fork_point import ForkPoint + from app.models.session_handoff import SessionHandoff + from app.models.session_resolution_output import SessionResolutionOutput class AISession(Base): @@ -206,6 +210,28 @@ class AISession(Base): comment="Full LLM message history for context continuity", ) + # ── Branching ── + is_branching: Mapped[bool] = mapped_column( + default=False, + comment="Whether conversational branching is active for this session", + ) + active_branch_id: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), nullable=True, + comment="Currently viewed branch. No FK — soft pointer to avoid circular FK with session_branches", + ) + handoff_count: Mapped[int] = mapped_column( + Integer, nullable=False, default=0, + comment="Number of times this session has been handed off", + ) + total_active_seconds: Mapped[int] = mapped_column( + Integer, nullable=False, default=0, + comment="Cumulative active time in seconds", + ) + total_parked_seconds: Mapped[int] = mapped_column( + Integer, nullable=False, default=0, + comment="Cumulative parked time in seconds", + ) + # ── Relationships ── user: Mapped["User"] = relationship("User", foreign_keys=[user_id]) account: Mapped["Account"] = relationship("Account") @@ -218,3 +244,18 @@ class AISession(Base): cascade="all, delete-orphan", order_by="AISessionStep.step_order", ) + branches: Mapped[list["SessionBranch"]] = relationship( + "SessionBranch", + foreign_keys="SessionBranch.session_id", + cascade="all, delete-orphan", + order_by="SessionBranch.branch_order", + ) + handoffs: Mapped[list["SessionHandoff"]] = relationship( + "SessionHandoff", + cascade="all, delete-orphan", + order_by="SessionHandoff.created_at", + ) + resolution_outputs: Mapped[list["SessionResolutionOutput"]] = relationship( + "SessionResolutionOutput", + cascade="all, delete-orphan", + ) diff --git a/backend/app/models/ai_session_step.py b/backend/app/models/ai_session_step.py index 413f142c..ac08da72 100644 --- a/backend/app/models/ai_session_step.py +++ b/backend/app/models/ai_session_step.py @@ -16,6 +16,8 @@ from app.core.database import Base if TYPE_CHECKING: from app.models.ai_session import AISession from app.models.script_template import ScriptGeneration + from app.models.session_branch import SessionBranch + from app.models.fork_point import ForkPoint class AISessionStep(Base): @@ -34,7 +36,7 @@ class AISessionStep(Base): __table_args__ = ( CheckConstraint( "step_type IN ('question', 'action', 'script_generation', 'verification', " - "'info_request', 'note', 'intake_analysis')", + "'info_request', 'note', 'intake_analysis', 'fork')", name="ck_ai_session_steps_step_type", ), ) @@ -119,6 +121,24 @@ class AISessionStep(Base): Integer, nullable=False, default=0, ) + # ── Branching ── + branch_id: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), + ForeignKey("session_branches.id", ondelete="SET NULL"), + nullable=True, + index=True, + comment="NULL = pre-branching/root messages", + ) + is_fork_point: Mapped[bool] = mapped_column( + default=False, + comment="Whether this step triggered a fork", + ) + fork_point_id: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), + ForeignKey("fork_points.id", ondelete="SET NULL"), + nullable=True, + ) + # ── Timestamps ── created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) diff --git a/backend/app/models/file_upload.py b/backend/app/models/file_upload.py index 6846b1a3..208c35b5 100644 --- a/backend/app/models/file_upload.py +++ b/backend/app/models/file_upload.py @@ -3,7 +3,7 @@ import uuid from datetime import datetime, timezone from typing import Optional -from sqlalchemy import String, Integer, DateTime, ForeignKey +from sqlalchemy import String, Integer, DateTime, ForeignKey, Text from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.dialects.postgresql import UUID @@ -30,3 +30,27 @@ class FileUpload(Base): created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) + + # ── AI description + branching context ── + ai_description: Mapped[Optional[str]] = mapped_column( + Text, nullable=True, + comment="AI-generated one-sentence description of the file", + ) + extracted_content: Mapped[Optional[str]] = mapped_column( + Text, nullable=True, + comment="Extracted text from logs/configs", + ) + content_summary: Mapped[Optional[str]] = mapped_column( + Text, nullable=True, + comment="AI summary for long text files (>2000 tokens)", + ) + uploaded_on_branch_id: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), + ForeignKey("session_branches.id", ondelete="SET NULL"), + nullable=True, + ) + uploaded_at_step_id: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), + ForeignKey("ai_session_steps.id", ondelete="SET NULL"), + nullable=True, + )