flexigo-support-bot / app /models.py
victor34593993's picture
wave3: agentic write-actions
61b5750 verified
Raw
History Blame
7.54 kB
"""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?")
# Persona shown in the widget (name + avatar photo/illustration URL)
assistant_name: Mapped[str] = mapped_column(String(120), default="")
avatar_url: Mapped[str] = mapped_column(String(1000), default="")
# Shopify (optional — only for order tracking / live catalog). Secret encrypted.
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)
# Agentic write-actions — opt-in per store (default OFF; merchant enables).
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)) # "file" | "url"
name: Mapped[str] = mapped_column(String(500))
location: Mapped[str] = mapped_column(String(2000)) # file path or URL
status: Mapped[str] = mapped_column(String(20), default="pending") # pending|indexed|error
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)) # user|assistant|tool|system
content: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
session: Mapped[ChatSession] = relationship(back_populates="messages")