- get_scout_stats aggregator (app/engine/stats): assembles the REAL Summary metrics —
funnel + cohort engagement rank + active-window (activity timestamps by hour) from our DB,
accumulating salary band (avg of each deck's peak, ₹L), match/competition stats from the
feed, Momentum/QX + Q-Score trend (qscore-service), day streak (user-service). Honest:
unsourced cards return None so the UI omits/locks them, never faked. posted_date extractor.
- Activity tracking: viewed/saved flags + search_count → funnel (Matches→Viewed→Shortlisted→
Applied) + engagement percentile. matchesFound = all-time count.
- Per-board search cursors {board: page} (replaces the single cursor): only boards that truly
paginate (LinkedIn) get one; cursor = LAST page fetched (1st search of a new query → 1).
Resets on query change OR >24h (boards refresh ~daily). Dropped Naukri incremental/stateKey
(opaque, exhausting, cross-account dedup state) — dedup is the PER-USER seen-net only.
- tests: stats helpers (posting-age, histogram, active-window, engagement score).
91 lines
4.3 KiB
Python
91 lines
4.3 KiB
Python
"""matchmaking-v2 settings — lean, on-demand. No corpus DB, no nightly scrape."""
|
|
from __future__ import annotations
|
|
|
|
from functools import lru_cache
|
|
import os
|
|
|
|
from pydantic_settings import BaseSettings
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
|
|
|
|
ENV: str = "development"
|
|
LOG_LEVEL: str = "INFO"
|
|
SERVICE_NAME: str = "matchmaking-service" # MUST stay "matchmaking-service" — orchestrator routes by card name
|
|
API_PREFIX: str = "/api/v1"
|
|
API_HOST: str = "0.0.0.0"
|
|
API_PORT: int = 8000
|
|
CORS_ORIGINS: str = "*"
|
|
|
|
# Public URL the orchestrator discovers this agent at (one of AGENT_URLS).
|
|
AGENT_URL: str = "http://localhost:8006"
|
|
|
|
# A2A (agent-to-agent) bearer auth — the orchestrator presents one of these.
|
|
A2A_ALLOWED_KEYS: str = "dev-a2a-key"
|
|
|
|
# Orchestrator comms (Redis Streams). Optional in dev — the worker no-ops if absent.
|
|
ORCHESTRATOR_REDIS_URL: str | None = None
|
|
REDIS_URL: str = "redis://localhost:6379/0"
|
|
|
|
# Inputs / engine
|
|
APIFY_TOKEN: str | None = None
|
|
OPENAI_API_KEY: str | None = None
|
|
|
|
# LLM gateway (opencode.ai/zen — OpenAI-compatible; reuse interview-service's DSPY key).
|
|
# One `openai` client (base_url override) serves the Opus curator + embeddings.
|
|
DSPY_API_BASE: str | None = None
|
|
DSPY_API_KEY: str | None = None
|
|
CURATE_MODEL: str = "claude-opus-4-8" # the senior recruiter (Opus, latest) — Opus's call is final
|
|
SUGGEST_MODEL: str = "claude-haiku-4-5" # the bubbling engine — fast + cheap (latency-sensitive UI)
|
|
EMBED_MODEL: str = "text-embedding-3-small" # the vibe-engineer (direct OpenAI — gateway has no embeddings route)
|
|
ENGINE_LLM_ENABLED: bool = True # off / key absent → white-box sift + templated cards (safety net)
|
|
SIFT_TOP_K: int = 18 # candidate pool the curator reads
|
|
|
|
# Result cache (dev cost-saver). Keyed by (actor, exact input) → saved job set on disk.
|
|
# off = always live, never save (PRODUCTION default)
|
|
# readwrite = replay if saved else live + save (DEV: one live sweep, then $0 replays)
|
|
# read = replay only, error if missing (offline / deterministic)
|
|
# refresh = always live + overwrite the saved set
|
|
APIFY_CACHE_MODE: str = "off"
|
|
APIFY_CACHE_DIR: str = ".apify_cache"
|
|
# Active boards — "Balanced" India stack: all three filter to the exact city at the board
|
|
# (hyper-relevant) and surface offsite apply links. Naukri=blackfalcondata feed,
|
|
# Foundit=Monster India, LinkedIn=geoId/city. Others (ats/indeed/naukri_v1) stay registered, off.
|
|
BOARDS_ENABLED: str = "naukri,foundit,linkedin"
|
|
|
|
# Per-board fetch budget (the #1 Apify cost lever). Demo-sized to conserve credits.
|
|
NAUKRI_MAX_JOBS: int = 15 # blackfalcondata maxResults (no floor); fetchDetails=True for offsite apply links
|
|
FOUNDIT_MAX_JOBS: int = 50
|
|
LINKEDIN_MAX_JOBS: int = 25
|
|
# ATS lane (off by default) — company-seeded global/remote "dream companies".
|
|
ATS_MAX_PER_COMPANY: int = 4
|
|
ATS_COMPANIES: str = "stripe,databricks,gitlab,figma,ramp,notion,razorpay,zerodha"
|
|
INDEED_MAX_JOBS: int = 30
|
|
|
|
# QScore (competence proxy) — consumed, not computed. Dashboard Momentum/QX reads it via REST.
|
|
QSCORE_BASE_URL: str = "http://localhost:8004"
|
|
QSCORE_AUTH_TOKEN: str | None = None
|
|
QSCORE_ORG_ID: str = "growqr"
|
|
# user-service — dashboard Day-streak reads metadata.current_streak via GET /api/state/{clerk_id}
|
|
USER_SERVICE_BASE_URL: str = "http://localhost:8003"
|
|
# dashboard-service — Q-Score trend series via GET /api/qscore-history/{clerk_id}
|
|
DASHBOARD_SERVICE_BASE_URL: str = "http://localhost:8005"
|
|
A2A_OUTBOUND_KEY: str = "dev-a2a-key" # Bearer token presented to sibling services
|
|
|
|
# Storage for feedback/labels (NOT the old corpus). Optional until the learning loop lands.
|
|
DATABASE_URL: str | None = None
|
|
|
|
@property
|
|
def cors_origins_list(self) -> list[str]:
|
|
if self.CORS_ORIGINS.strip() == "*":
|
|
return ["*"]
|
|
return [o.strip() for o in self.CORS_ORIGINS.split(",") if o.strip()]
|
|
|
|
|
|
@lru_cache
|
|
def get_settings() -> Settings:
|
|
if os.environ.get("PYTEST_CURRENT_TEST"):
|
|
return Settings(_env_file=None)
|
|
return Settings()
|