peregrine/scripts/user_profile.py
pyr0ball 35735c3074 feat: LGBTQIA+ focus + Phase 2/3 audit fixes
LGBTQIA+ inclusion section in research briefs:
- user_profile.py: add candidate_lgbtq_focus bool accessor
- user.yaml.example: add candidate_lgbtq_focus flag (default false)
- company_research.py: gate new LGBTQIA+ section behind flag; section
  count now dynamic (7 base + 1 per opt-in section, max 9)
- 2_Settings.py: add "Research Brief Preferences" expander with
  checkboxes for both accessibility and LGBTQIA+ focus flags;
  mission_preferences now round-trips through save (no silent drop)

Phase 2 fixes:
- manage-vllm.sh: MODEL_DIR and VLLM_BIN now read from env vars
  (VLLM_MODELS_DIR, VLLM_BIN) with portable defaults
- search_profiles.yaml: replace personal CS/TAM/Bay Area profiles
  with a documented generic starter profile

Phase 3 fix:
- llm.yaml: rename meghan-cover-writer:latest → llama3.2:3b with
  inline comment for users to substitute their fine-tuned model;
  fix model-exclusion comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 20:02:03 -08:00

115 lines
4.7 KiB
Python

"""
UserProfile — wraps config/user.yaml and provides typed accessors.
All hard-coded personal references in the app should import this instead
of reading strings directly. URL construction for services is centralised
here so port/host/SSL changes propagate everywhere automatically.
"""
from __future__ import annotations
from pathlib import Path
import yaml
_DEFAULTS = {
"name": "",
"email": "",
"phone": "",
"linkedin": "",
"career_summary": "",
"nda_companies": [],
"docs_dir": "~/Documents/JobSearch",
"ollama_models_dir": "~/models/ollama",
"vllm_models_dir": "~/models/vllm",
"inference_profile": "remote",
"mission_preferences": {},
"candidate_accessibility_focus": False,
"candidate_lgbtq_focus": False,
"services": {
"streamlit_port": 8501,
"ollama_host": "localhost",
"ollama_port": 11434,
"ollama_ssl": False,
"ollama_ssl_verify": True,
"vllm_host": "localhost",
"vllm_port": 8000,
"vllm_ssl": False,
"vllm_ssl_verify": True,
"searxng_host": "localhost",
"searxng_port": 8888,
"searxng_ssl": False,
"searxng_ssl_verify": True,
},
}
class UserProfile:
def __init__(self, path: Path):
if not path.exists():
raise FileNotFoundError(f"user.yaml not found at {path}")
raw = yaml.safe_load(path.read_text()) or {}
data = {**_DEFAULTS, **raw}
svc_defaults = dict(_DEFAULTS["services"])
svc_defaults.update(raw.get("services", {}))
data["services"] = svc_defaults
self.name: str = data["name"]
self.email: str = data["email"]
self.phone: str = data["phone"]
self.linkedin: str = data["linkedin"]
self.career_summary: str = data["career_summary"]
self.nda_companies: list[str] = [c.lower() for c in data["nda_companies"]]
self.docs_dir: Path = Path(data["docs_dir"]).expanduser().resolve()
self.ollama_models_dir: Path = Path(data["ollama_models_dir"]).expanduser().resolve()
self.vllm_models_dir: Path = Path(data["vllm_models_dir"]).expanduser().resolve()
self.inference_profile: str = data["inference_profile"]
self.mission_preferences: dict[str, str] = data.get("mission_preferences", {})
self.candidate_accessibility_focus: bool = bool(data.get("candidate_accessibility_focus", False))
self.candidate_lgbtq_focus: bool = bool(data.get("candidate_lgbtq_focus", False))
self._svc = data["services"]
# ── Service URLs ──────────────────────────────────────────────────────────
def _url(self, host: str, port: int, ssl: bool) -> str:
scheme = "https" if ssl else "http"
return f"{scheme}://{host}:{port}"
@property
def ollama_url(self) -> str:
s = self._svc
return self._url(s["ollama_host"], s["ollama_port"], s["ollama_ssl"])
@property
def vllm_url(self) -> str:
s = self._svc
return self._url(s["vllm_host"], s["vllm_port"], s["vllm_ssl"])
@property
def searxng_url(self) -> str:
s = self._svc
return self._url(s["searxng_host"], s["searxng_port"], s["searxng_ssl"])
def ssl_verify(self, service: str) -> bool:
"""Return ssl_verify flag for a named service (ollama/vllm/searxng)."""
return bool(self._svc.get(f"{service}_ssl_verify", True))
# ── NDA helpers ───────────────────────────────────────────────────────────
def is_nda(self, company: str) -> bool:
return company.lower() in self.nda_companies
def nda_label(self, company: str, score: int = 0, threshold: int = 3) -> str:
"""Return masked label if company is NDA and score below threshold."""
if self.is_nda(company) and score < threshold:
return "previous employer (NDA)"
return company
# ── Existence check (used by app.py before load) ─────────────────────────
@staticmethod
def exists(path: Path) -> bool:
return path.exists()
# ── llm.yaml URL generation ───────────────────────────────────────────────
def generate_llm_urls(self) -> dict[str, str]:
"""Return base_url values for each backend, derived from services config."""
return {
"ollama": f"{self.ollama_url}/v1",
"ollama_research": f"{self.ollama_url}/v1",
"vllm": f"{self.vllm_url}/v1",
}