feat: add ai_chat_sessions database model and migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-27 03:42:10 -05:00
parent 09c5d60067
commit ccd178b02e
3 changed files with 136 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
"""add ai_chat_sessions table
Revision ID: e2d81e82ea5e
Revises: 1490781700bc
Create Date: 2026-02-27 03:41:33.832260
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID, JSONB
# revision identifiers, used by Alembic.
revision: str = 'e2d81e82ea5e'
down_revision: Union[str, None] = '1490781700bc'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"ai_chat_sessions",
sa.Column("id", UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("account_id", UUID(as_uuid=True), sa.ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("status", sa.String(20), nullable=False, server_default="active"),
sa.Column("current_phase", sa.String(20), nullable=False, server_default="scoping"),
sa.Column("flow_type", sa.String(20), nullable=False),
sa.Column("conversation_history", JSONB, nullable=False, server_default="[]"),
sa.Column("working_tree", JSONB, nullable=True),
sa.Column("tree_metadata", JSONB, nullable=False, server_default="{}"),
sa.Column("provider_used", sa.String(20), nullable=True),
sa.Column("message_count", sa.Integer, nullable=False, server_default="0"),
sa.Column("total_input_tokens", sa.Integer, nullable=False, server_default="0"),
sa.Column("total_output_tokens", sa.Integer, nullable=False, server_default="0"),
sa.Column("generated_tree_id", UUID(as_uuid=True), sa.ForeignKey("trees.id", ondelete="SET NULL"), nullable=True),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
def downgrade() -> None:
op.drop_table("ai_chat_sessions")

View File

@@ -28,6 +28,7 @@ from .maintenance_schedule import MaintenanceSchedule
from .feedback import Feedback
from .ai_conversation import AIConversation
from .ai_usage import AIUsage
from .ai_chat_session import AIChatSession
__all__ = [
"User",
@@ -67,4 +68,5 @@ __all__ = [
"Feedback",
"AIConversation",
"AIUsage",
"AIChatSession",
]

View File

@@ -0,0 +1,88 @@
"""AI Chat Builder session tracking.
Stores conversational flow builder state across the multi-phase interview.
Sessions expire after 24 hours.
"""
import uuid
from datetime import datetime, timezone
from typing import Optional, Any
from sqlalchemy import String, DateTime, ForeignKey, Integer
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID, JSONB
from app.core.database import Base
class AIChatSession(Base):
__tablename__ = "ai_chat_sessions"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
account_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("accounts.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
status: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="active",
comment="active | completed | abandoned",
)
current_phase: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="scoping",
comment="scoping | discovery | enrichment | review | generation",
)
flow_type: Mapped[str] = mapped_column(
String(20),
nullable=False,
comment="troubleshooting | procedural",
)
conversation_history: Mapped[list[dict[str, Any]]] = mapped_column(
JSONB, nullable=False, default=list
)
working_tree: Mapped[Optional[dict[str, Any]]] = mapped_column(
JSONB, nullable=True
)
tree_metadata: Mapped[dict[str, Any]] = mapped_column(
JSONB, nullable=False, default=dict
)
provider_used: Mapped[Optional[str]] = mapped_column(
String(20), nullable=True
)
message_count: Mapped[int] = mapped_column(
Integer, nullable=False, default=0
)
total_input_tokens: Mapped[int] = mapped_column(
Integer, nullable=False, default=0
)
total_output_tokens: Mapped[int] = mapped_column(
Integer, nullable=False, default=0
)
generated_tree_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("trees.id", ondelete="SET NULL"),
nullable=True,
)
expires_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)