Files
matchmaking-v2/app/db/models.py
raulgupta b1e9cdd182 Engine v2: embeddings sift + Opus curator + Postgres feed persistence
- A1: fire the hard filter (Apify is precise); embedding vibe-ranker (embed.py)
  blends with the floor-free white-box (rank.py) to sift ~90 -> top 18
- A2: Opus curator (curate.py) reads the 18 -> honest <=count + Gemini-voiced
  report cards; graceful fallback to the white-box + templated cards
- LLM via opencode.ai/zen gateway (llm.py): Opus chat + direct-OpenAI embeddings
- Run LLM work off the event loop (asyncio.to_thread) so the Redis response publishes
- C1: dedicated Postgres (app/db/) persists the per-user feed; get_scout_feed
  replays it so matches survive navigation/refresh
- match contract + report-card fields (schema.py); skills.py; tests/
2026-06-19 15:51:16 +05:30

28 lines
1.2 KiB
Python

"""ORM models. One table for now: the per-user cached feed (the last completed search)."""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import JSON, DateTime, Integer, String, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class UserFeed(Base):
"""The user's last completed search — replayed on load so matches survive navigation.
Upserted on each `search_complete`; read by `get_feed`. One row per user (PK = user_id)."""
__tablename__ = "user_feed"
user_id: Mapped[str] = mapped_column(String, primary_key=True)
prefs: Mapped[dict] = mapped_column(JSON, default=dict) # the ScoutPrefs that produced it
opportunities: Mapped[list] = mapped_column(JSON, default=list) # the curated deck (with match blocks)
sources: Mapped[dict] = mapped_column(JSON, default=dict) # per-board counts
engine: Mapped[str] = mapped_column(String, default="") # "opus" | "fallback:…"
scanned: Mapped[int] = mapped_column(Integer, default=0)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)