Files
matchmaking-v2/app/config.py
raulgupta 89ca9ad647 Initial commit: matchmaking-v2 on-demand multi-board job-search agent
On-demand Scout service (replaces the nightly aggregator):
- FastAPI agent-mesh service; card name "matchmaking-service" (HTTP /a2a/tasks
  + Redis-stream worker matching the sibling-service pattern)
- run_search: parallel multi-board sweep (Naukri/blackfalcondata + Foundit +
  LinkedIn), city-filtered at the board, cheapest-per-result first
- per-board adapters + normalizers -> ScoutJob with rich read-only details and
  offsite apply links; recall mode + MVQ guard
- result cache for dev replay ($0); per-board cost knobs; retry-on-5xx
- research/: engine design, board cost economics, India-actor shortlist, POC
2026-06-18 19:22:03 +05:30

75 lines
3.0 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
# 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
QSCORE_BASE_URL: str = "http://localhost:8004"
QSCORE_AUTH_TOKEN: str | None = None
# 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()