import enum import uuid from datetime import datetime, timezone from sqlalchemy import String, Text, Boolean, Integer, ForeignKey, JSON, Enum as SAEnum from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): pass class ConversationStatus(str, enum.Enum): active = "active" completed = "completed" abandoned = "abandoned" class MessageRole(str, enum.Enum): user = "user" assistant = "assistant" system = "system" class ConversationSource(str, enum.Enum): standalone = "standalone" embedded = "embedded" widget = "widget" app = "app" class IntentLevel(str, enum.Enum): interested = "interested" not_interested = "not_interested" pending = "pending" class Conversation(Base): __tablename__ = "conversations" id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()) ) session_id: Mapped[str] = mapped_column(String(64), unique=True, index=True) page_token: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) status: Mapped[ConversationStatus] = mapped_column( SAEnum(ConversationStatus), default=ConversationStatus.active ) source: Mapped[ConversationSource] = mapped_column( SAEnum(ConversationSource), default=ConversationSource.standalone ) client_info: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column( default=lambda: datetime.now(timezone.utc) ) updated_at: Mapped[datetime] = mapped_column( default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), ) messages: Mapped[list["Message"]] = relationship( back_populates="conversation", order_by="Message.created_at" ) profile: Mapped["CustomerProfile | None"] = relationship( back_populates="conversation", uselist=False ) evaluation: Mapped["Evaluation | None"] = relationship( back_populates="conversation", uselist=False ) class Message(Base): __tablename__ = "messages" id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()) ) conversation_id: Mapped[str] = mapped_column( ForeignKey("conversations.id", ondelete="CASCADE") ) role: Mapped[MessageRole] = mapped_column(SAEnum(MessageRole)) content: Mapped[str] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column( default=lambda: datetime.now(timezone.utc) ) conversation: Mapped["Conversation"] = relationship(back_populates="messages") class CustomerProfile(Base): __tablename__ = "customer_profiles" id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()) ) conversation_id: Mapped[str] = mapped_column( ForeignKey("conversations.id", ondelete="CASCADE"), unique=True ) raw_info: Mapped[dict | None] = mapped_column(JSON, nullable=True, default=dict) summary: Mapped[str | None] = mapped_column(Text, nullable=True) updated_at: Mapped[datetime] = mapped_column( default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), ) conversation: Mapped["Conversation"] = relationship(back_populates="profile") class Evaluation(Base): __tablename__ = "evaluations" id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()) ) conversation_id: Mapped[str] = mapped_column( ForeignKey("conversations.id", ondelete="CASCADE"), unique=True ) intent_level: Mapped[IntentLevel] = mapped_column(SAEnum(IntentLevel)) reasoning: Mapped[str | None] = mapped_column(Text, nullable=True) contact_info: Mapped[dict | None] = mapped_column(JSON, nullable=True) email_sent: Mapped[bool] = mapped_column(Boolean, default=False) correction_count: Mapped[int] = mapped_column(Integer, default=0) evaluated_at: Mapped[datetime] = mapped_column( default=lambda: datetime.now(timezone.utc) ) conversation: Mapped["Conversation"] = relationship(back_populates="evaluation") class LeadPushLog(Base): """询盘推送 UniService 日志(设计 2026-09-28-lead-push-to-uniservice)。 status: pending → success/failed;skipped = contact 三选一(phone/whatsapp/ email)不满足,对方必拒,预检后不发 HTTP 只留审计痕迹。 outcome: 200 时对方逐条结论 accepted/duplicated(duplicated 不是错误)。 """ __tablename__ = "lead_push_logs" id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()) ) evaluation_id: Mapped[str] = mapped_column( ForeignKey("evaluations.id", ondelete="CASCADE"), index=True ) conversation_id: Mapped[str] = mapped_column( ForeignKey("conversations.id", ondelete="CASCADE"), index=True ) push_type: Mapped[str] = mapped_column(String(16)) # lead/returning/correction status: Mapped[str] = mapped_column(String(16), default="pending", index=True) attempts: Mapped[int] = mapped_column(Integer, default=0) request_payload: Mapped[dict | None] = mapped_column(JSON, nullable=True) response_status: Mapped[int | None] = mapped_column(Integer, nullable=True) outcome: Mapped[str | None] = mapped_column(String(16), nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( default=lambda: datetime.now(timezone.utc) ) updated_at: Mapped[datetime] = mapped_column( default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), )