Files
matchmaking-v2/app/engine/schema.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

60 lines
2.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""The `match` block — the ONE contract between the engine and the card.
Defined once here (Pydantic) and mirrored as a `JobMatch` TS interface on the frontend
`ScoutJob`. Every engine phase writes this exact shape; `scoutJobToMatchRole` reads it.
Keeping the breakdown/growth shapes identical to the card's existing `MatchDim`/`growth`
means the UI renders real numbers with **no shape change**.
"""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
Level = Literal["Strong", "Solid", "Light"]
Fit = Literal["fit", "stretch"]
class MatchDim(BaseModel):
"""One row of the "how you match" breakdown — mirrors the frontend `MatchDim`.
`note` is a one-line, Gemini-toned observation for this dimension (the report-card voice)."""
name: str
score: int # 0100
level: Level
note: str | None = None
class Growth(BaseModel):
"""The weakest *reachable* dim + the lift closing it buys — mirrors `MatchRole.growth`.
`from` is a Python keyword, so it's aliased (JSON stays `{text, from, to}`)."""
model_config = ConfigDict(populate_by_name=True)
text: str
from_: int = Field(alias="from")
to: int
class MatchResult(BaseModel):
"""Attached to each surviving ScoutJob as `job["match"]` — the engine's report card.
Voice mirrors interview-service's Gemini video-analysis: a characterful archetype, an honest
balanced one-liner, per-dimension notes, and a warm coach note. Templated now → Opus later."""
score: int # 0100 — the real utility (NOT fetch order)
fit: Fit
archetype: str | None = None # characterful label, e.g. "The Stretch Worth Taking"
one_line: str | None = None # balanced headline verdict (strength AND gap)
reason: str # short "why picked"
breakdown: list[MatchDim] = [] # per-dimension dims, each with a `note`
growth: Growth | None = None
coach_note: str | None = None # warm, actionable closing line
proofReady: bool = False # user is Strong on the job's key skill
factors: dict[str, float | None] = {} # debug/observability (per-factor φ; None = absent)
def as_dict(self) -> dict:
"""Serialize for the ScoutJob payload — by alias so growth emits `from`/`to`."""
return self.model_dump(by_alias=True)
def attach_match(job: dict, result: MatchResult) -> dict:
"""Set `job['match']` from a MatchResult (in place) and return the job."""
job["match"] = result.as_dict()
return job