154 lines
6.3 KiB
Python
154 lines
6.3 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
DateTime,
|
|
ForeignKey,
|
|
Index,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
func,
|
|
)
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
class Section(Base):
|
|
"""A sub-section inside a vertical, e.g. ('real_estate', 'dubai').
|
|
|
|
The pair (vertical, slug) is unique and identifies a section in URLs
|
|
and API calls. A channel belongs to exactly one section, the section
|
|
knows its vertical, and the LLM prompt store can hold a per-section
|
|
override that falls back to the vertical-level prompt.
|
|
"""
|
|
|
|
__tablename__ = "sections"
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"vertical",
|
|
"department_id",
|
|
"slug",
|
|
name="uq_section_vertical_department_slug",
|
|
),
|
|
Index("ix_sections_vertical_department", "vertical", "department_id"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
vertical: Mapped[str] = mapped_column(String(32))
|
|
department_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
slug: Mapped[str] = mapped_column(String(64))
|
|
title: Mapped[str] = mapped_column(String(255))
|
|
emoji: Mapped[str | None] = mapped_column(String(8), nullable=True)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now()
|
|
)
|
|
|
|
channels: Mapped[list["Channel"]] = relationship(back_populates="section")
|
|
|
|
|
|
class Channel(Base):
|
|
__tablename__ = "channels"
|
|
__table_args__ = (
|
|
UniqueConstraint("section_id", "identifier", name="uq_channels_section_identifier"),
|
|
Index("ix_channels_source_channel_id", "source_channel_id"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
# Telegram numeric channel id (peer id), nullable until first resolve
|
|
tg_id: Mapped[int | None] = mapped_column(BigInteger, unique=True, nullable=True)
|
|
# Username or t.me/joinchat link supplied by user
|
|
identifier: Mapped[str] = mapped_column(String(255))
|
|
title: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
# 'real_estate' or 'hr' — picks which LLM prompt and lead schema is used
|
|
vertical: Mapped[str] = mapped_column(
|
|
String(32), default="real_estate", server_default="real_estate", index=True
|
|
)
|
|
section_id: Mapped[int] = mapped_column(
|
|
ForeignKey("sections.id", ondelete="RESTRICT"), index=True
|
|
)
|
|
source_channel_id: Mapped[int | None] = mapped_column(
|
|
ForeignKey("channels.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
is_active: Mapped[bool] = mapped_column(default=True, server_default="true")
|
|
last_message_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
last_polled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
last_poll_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
last_poll_error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
last_poll_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
last_poll_error_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now()
|
|
)
|
|
|
|
section: Mapped[Section] = relationship(back_populates="channels")
|
|
messages: Mapped[list["Message"]] = relationship(
|
|
back_populates="channel",
|
|
cascade="all, delete-orphan",
|
|
passive_deletes=True,
|
|
)
|
|
|
|
|
|
class Message(Base):
|
|
__tablename__ = "messages"
|
|
__table_args__ = (
|
|
UniqueConstraint("channel_id", "tg_message_id", name="uq_channel_message"),
|
|
Index("ix_messages_channel_date", "channel_id", "date"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
channel_id: Mapped[int] = mapped_column(ForeignKey("channels.id", ondelete="CASCADE"))
|
|
tg_message_id: Mapped[int] = mapped_column(BigInteger)
|
|
date: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
sender_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
sender_username: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
sender_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
grouped_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
has_media: Mapped[bool] = mapped_column(default=False, server_default="false")
|
|
views: Mapped[int | None] = mapped_column(nullable=True)
|
|
forwards: Mapped[int | None] = mapped_column(nullable=True)
|
|
raw: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
media_files: Mapped[list | None] = mapped_column(JSONB, nullable=True)
|
|
extracted: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
fetched_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now()
|
|
)
|
|
|
|
channel: Mapped[Channel] = relationship(back_populates="messages")
|
|
|
|
|
|
class MessageClassification(Base):
|
|
__tablename__ = "message_classifications"
|
|
__table_args__ = (
|
|
UniqueConstraint("message_id", "section_id", name="uq_message_classification_section"),
|
|
Index("ix_message_classifications_message", "message_id"),
|
|
Index("ix_message_classifications_section", "section_id"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
message_id: Mapped[int] = mapped_column(ForeignKey("messages.id", ondelete="CASCADE"))
|
|
section_id: Mapped[int] = mapped_column(ForeignKey("sections.id", ondelete="CASCADE"))
|
|
vertical: Mapped[str] = mapped_column(String(32))
|
|
verdict: Mapped[dict] = mapped_column(JSONB)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now()
|
|
)
|
|
|
|
|
|
class AppSetting(Base):
|
|
"""Runtime-editable settings, edited from the UI without a restart."""
|
|
|
|
__tablename__ = "app_settings"
|
|
|
|
key: Mapped[str] = mapped_column(String(255), primary_key=True)
|
|
value: Mapped[dict | str | int | bool | None] = mapped_column(JSONB, nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now()
|
|
)
|