- 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/
28 lines
1.2 KiB
Python
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()
|
|
)
|