| """ORM models. The embedding column uses pgvector on Postgres and JSON |
| elsewhere (so unit tests run on sqlite).""" |
|
|
| from __future__ import annotations |
|
|
| import uuid |
| from datetime import UTC, datetime |
|
|
| from sqlalchemy import ( |
| JSON, |
| DateTime, |
| ForeignKey, |
| Integer, |
| String, |
| Text, |
| types, |
| ) |
| from sqlalchemy.orm import Mapped, mapped_column, relationship |
|
|
| from app.db import Base |
|
|
| EMBEDDING_DIM = 384 |
|
|
|
|
| def _utcnow() -> datetime: |
| return datetime.now(UTC) |
|
|
|
|
| class EmbeddingType(types.TypeDecorator): |
| """Vector(384) on Postgres, JSON list[float] on other dialects.""" |
|
|
| impl = JSON |
| cache_ok = True |
|
|
| def load_dialect_impl(self, dialect): |
| if dialect.name == "postgresql": |
| from pgvector.sqlalchemy import Vector |
|
|
| return dialect.type_descriptor(Vector(EMBEDDING_DIM)) |
| return dialect.type_descriptor(JSON()) |
|
|
| def process_bind_param(self, value, dialect): |
| if value is None: |
| return None |
| return list(value) |
|
|
| def process_result_value(self, value, dialect): |
| if value is None: |
| return None |
| return list(value) |
|
|
|
|
| class Config(Base): |
| __tablename__ = "config" |
|
|
| id: Mapped[int] = mapped_column(Integer, primary_key=True) |
| key: Mapped[str] = mapped_column(String(100), unique=True, index=True) |
| value: Mapped[dict] = mapped_column(JSON, default=dict) |
|
|
|
|
| class Tenant(Base): |
| """A merchant served by the shared multi-tenant backend.""" |
|
|
| __tablename__ = "tenants" |
|
|
| id: Mapped[int] = mapped_column(Integer, primary_key=True) |
| slug: Mapped[str] = mapped_column(String(64), unique=True, index=True) |
| name: Mapped[str] = mapped_column(String(255), default="") |
| brand_name: Mapped[str] = mapped_column(String(255), default="Asistente") |
| brand_color: Mapped[str] = mapped_column(String(16), default="#e8491d") |
| welcome: Mapped[str] = mapped_column(Text, default="¡Hola! ¿En qué puedo ayudarte?") |
| |
| assistant_name: Mapped[str] = mapped_column(String(120), default="") |
| avatar_url: Mapped[str] = mapped_column(String(1000), default="") |
| |
| shopify_shop: Mapped[str] = mapped_column(String(255), default="") |
| shopify_client_id: Mapped[str] = mapped_column(String(255), default="") |
| shopify_client_secret_enc: Mapped[str] = mapped_column(Text, default="") |
| shopify_api_version: Mapped[str] = mapped_column(String(20), default="2026-01") |
| read_all_orders: Mapped[bool] = mapped_column(default=False) |
| |
| allow_cancel: Mapped[bool] = mapped_column(default=False) |
| allow_address_change: Mapped[bool] = mapped_column(default=False) |
| allow_returns: Mapped[bool] = mapped_column(default=False) |
| created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) |
|
|
|
|
| class KnowledgeSource(Base): |
| __tablename__ = "knowledge_sources" |
|
|
| id: Mapped[int] = mapped_column(Integer, primary_key=True) |
| tenant_id: Mapped[int | None] = mapped_column( |
| ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True |
| ) |
| kind: Mapped[str] = mapped_column(String(10)) |
| name: Mapped[str] = mapped_column(String(500)) |
| location: Mapped[str] = mapped_column(String(2000)) |
| status: Mapped[str] = mapped_column(String(20), default="pending") |
| error: Mapped[str | None] = mapped_column(Text, nullable=True) |
| created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) |
|
|
| chunks: Mapped[list[KnowledgeChunk]] = relationship( |
| back_populates="source", cascade="all, delete-orphan" |
| ) |
|
|
|
|
| class KnowledgeChunk(Base): |
| __tablename__ = "knowledge_chunks" |
|
|
| id: Mapped[int] = mapped_column(Integer, primary_key=True) |
| source_id: Mapped[int] = mapped_column( |
| ForeignKey("knowledge_sources.id", ondelete="CASCADE"), index=True |
| ) |
| tenant_id: Mapped[int | None] = mapped_column( |
| ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True |
| ) |
| ordinal: Mapped[int] = mapped_column(Integer, default=0) |
| text: Mapped[str] = mapped_column(Text) |
| embedding: Mapped[list[float]] = mapped_column(EmbeddingType) |
| meta: Mapped[dict] = mapped_column(JSON, default=dict) |
|
|
| source: Mapped[KnowledgeSource] = relationship(back_populates="chunks") |
|
|
|
|
| class ChatSession(Base): |
| __tablename__ = "chat_sessions" |
|
|
| id: Mapped[str] = mapped_column(String(40), primary_key=True, default=lambda: uuid.uuid4().hex) |
| tenant_id: Mapped[int | None] = mapped_column( |
| ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True |
| ) |
| shop: Mapped[str] = mapped_column(String(255), default="") |
| lang: Mapped[str | None] = mapped_column(String(10), nullable=True) |
| verified: Mapped[bool] = mapped_column(default=False) |
| verified_order_id: Mapped[str | None] = mapped_column(String(80), nullable=True) |
| verify_attempts: Mapped[int] = mapped_column(Integer, default=0) |
| locked_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) |
| created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) |
| last_seen: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), default=_utcnow, onupdate=_utcnow |
| ) |
|
|
| messages: Mapped[list[ChatMessage]] = relationship( |
| back_populates="session", |
| cascade="all, delete-orphan", |
| order_by="ChatMessage.id", |
| lazy="selectin", |
| ) |
|
|
|
|
| class Event(Base): |
| """Per-tenant analytics event (chat -> sales funnel).""" |
|
|
| __tablename__ = "events" |
|
|
| id: Mapped[int] = mapped_column(Integer, primary_key=True) |
| tenant_id: Mapped[int | None] = mapped_column( |
| ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True |
| ) |
| type: Mapped[str] = mapped_column(String(40), index=True) |
| session_id: Mapped[str | None] = mapped_column(String(40), nullable=True) |
| meta: Mapped[dict] = mapped_column(JSON, default=dict) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), default=_utcnow, index=True |
| ) |
|
|
|
|
| class StockWatch(Base): |
| """Per-tenant back-in-stock / price-drop capture.""" |
|
|
| __tablename__ = "stock_watches" |
|
|
| id: Mapped[int] = mapped_column(Integer, primary_key=True) |
| tenant_id: Mapped[int | None] = mapped_column( |
| ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True |
| ) |
| email: Mapped[str] = mapped_column(String(320)) |
| product_title: Mapped[str] = mapped_column(String(500), default="") |
| variant_id: Mapped[str] = mapped_column(String(40), default="") |
| notified: Mapped[bool] = mapped_column(default=False) |
| created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) |
|
|
|
|
| class ChatMessage(Base): |
| __tablename__ = "chat_messages" |
|
|
| id: Mapped[int] = mapped_column(Integer, primary_key=True) |
| session_id: Mapped[str] = mapped_column( |
| ForeignKey("chat_sessions.id", ondelete="CASCADE"), index=True |
| ) |
| role: Mapped[str] = mapped_column(String(20)) |
| content: Mapped[str] = mapped_column(Text) |
| created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) |
|
|
| session: Mapped[ChatSession] = relationship(back_populates="messages") |
|
|