"""SQLAlchemy models for conversations and leads.""" from __future__ import annotations from datetime import datetime from typing import Optional from sqlalchemy import ( Boolean, DateTime, ForeignKey, Integer, String, Text, ) from sqlalchemy.orm import ( DeclarativeBase, Mapped, mapped_column, relationship, ) class Base(DeclarativeBase): """Declarative base.""" class Conversation(Base): """A chat session on web or WhatsApp.""" __tablename__ = "conversations" id: Mapped[int] = mapped_column(Integer, primary_key=True) channel: Mapped[str] = mapped_column( String(32), default="web" ) language: Mapped[str] = mapped_column( String(8), default="en" ) status: Mapped[str] = mapped_column( String(32), default="active" ) external_id: Mapped[Optional[str]] = mapped_column( String(128), nullable=True ) failure_streak: Mapped[int] = mapped_column( Integer, default=0 ) resolved_without_human: Mapped[bool] = mapped_column( Boolean, default=False ) handed_off: Mapped[bool] = mapped_column( Boolean, default=False ) appointment_booked: Mapped[bool] = mapped_column( Boolean, default=False ) created_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow ) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, ) messages: Mapped[list["Message"]] = relationship( back_populates="conversation", cascade="all, delete-orphan", order_by="Message.created_at", ) lead: Mapped[Optional["Lead"]] = relationship( back_populates="conversation", uselist=False, cascade="all, delete-orphan", ) appointment: Mapped[Optional["Appointment"]] = ( relationship( back_populates="conversation", uselist=False, cascade="all, delete-orphan", ) ) class Message(Base): """Single message in a conversation.""" __tablename__ = "messages" id: Mapped[int] = mapped_column(Integer, primary_key=True) conversation_id: Mapped[int] = mapped_column( ForeignKey("conversations.id"), index=True ) role: Mapped[str] = mapped_column(String(16)) content: Mapped[str] = mapped_column(Text) language: Mapped[Optional[str]] = mapped_column( String(8), nullable=True ) grounded: Mapped[Optional[bool]] = mapped_column( Boolean, nullable=True ) created_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow ) conversation: Mapped["Conversation"] = relationship( back_populates="messages" ) class Lead(Base): """Prospect qualification data collected in chat.""" __tablename__ = "leads" id: Mapped[int] = mapped_column(Integer, primary_key=True) conversation_id: Mapped[int] = mapped_column( ForeignKey("conversations.id"), unique=True ) name: Mapped[Optional[str]] = mapped_column( String(128), nullable=True ) need: Mapped[Optional[str]] = mapped_column( Text, nullable=True ) budget: Mapped[Optional[str]] = mapped_column( String(64), nullable=True ) contact_channel: Mapped[Optional[str]] = mapped_column( String(64), nullable=True ) email: Mapped[Optional[str]] = mapped_column( String(256), nullable=True ) phone: Mapped[Optional[str]] = mapped_column( String(64), nullable=True ) complete: Mapped[bool] = mapped_column( Boolean, default=False ) created_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow ) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, ) conversation: Mapped["Conversation"] = relationship( back_populates="lead" ) class Appointment(Base): """Booked meeting slot.""" __tablename__ = "appointments" id: Mapped[int] = mapped_column(Integer, primary_key=True) conversation_id: Mapped[int] = mapped_column( ForeignKey("conversations.id"), unique=True ) slot_start: Mapped[str] = mapped_column(String(64)) slot_end: Mapped[str] = mapped_column(String(64)) attendee_name: Mapped[Optional[str]] = mapped_column( String(128), nullable=True ) attendee_email: Mapped[Optional[str]] = mapped_column( String(256), nullable=True ) external_id: Mapped[Optional[str]] = mapped_column( String(128), nullable=True ) status: Mapped[str] = mapped_column( String(32), default="confirmed" ) created_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow ) conversation: Mapped["Conversation"] = relationship( back_populates="appointment" ) class UnansweredQuestion(Base): """Question the bot could not answer from KB.""" __tablename__ = "unanswered_questions" id: Mapped[int] = mapped_column(Integer, primary_key=True) conversation_id: Mapped[int] = mapped_column( ForeignKey("conversations.id"), index=True ) question: Mapped[str] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow ) class KnowledgeDoc(Base): """Ingested knowledge document metadata.""" __tablename__ = "knowledge_docs" id: Mapped[int] = mapped_column(Integer, primary_key=True) filename: Mapped[str] = mapped_column(String(256)) source_type: Mapped[str] = mapped_column(String(32)) chunk_count: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow )