From 7380deb0210711e0ec95b31d90c2645163043a20 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 24 Feb 2026 18:29:45 -0800 Subject: [PATCH] feat: add UserProfile class with service URL generation and NDA helpers --- config/user.yaml.example | 34 ++++++++++++ scripts/user_profile.py | 109 +++++++++++++++++++++++++++++++++++++ tests/test_user_profile.py | 86 +++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 config/user.yaml.example create mode 100644 scripts/user_profile.py create mode 100644 tests/test_user_profile.py diff --git a/config/user.yaml.example b/config/user.yaml.example new file mode 100644 index 0000000..8b48c17 --- /dev/null +++ b/config/user.yaml.example @@ -0,0 +1,34 @@ +# config/user.yaml.example +# Copy to config/user.yaml and fill in your details. +# The first-run wizard will create this file automatically. + +name: "Your Name" +email: "you@example.com" +phone: "555-000-0000" +linkedin: "linkedin.com/in/yourprofile" +career_summary: > + Experienced professional with X years in [your field]. + Specialise in [key skills]. Known for [strength]. + +nda_companies: [] # e.g. ["FormerEmployer"] — masked in research briefs + +docs_dir: "~/Documents/JobSearch" +ollama_models_dir: "~/models/ollama" +vllm_models_dir: "~/models/vllm" + +inference_profile: "remote" # remote | cpu | single-gpu | dual-gpu + +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 diff --git a/scripts/user_profile.py b/scripts/user_profile.py new file mode 100644 index 0000000..de2f45b --- /dev/null +++ b/scripts/user_profile.py @@ -0,0 +1,109 @@ +""" +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", + "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._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", + } diff --git a/tests/test_user_profile.py b/tests/test_user_profile.py new file mode 100644 index 0000000..6950dd5 --- /dev/null +++ b/tests/test_user_profile.py @@ -0,0 +1,86 @@ +# tests/test_user_profile.py +import pytest +from pathlib import Path +import tempfile, yaml +from scripts.user_profile import UserProfile + +@pytest.fixture +def profile_yaml(tmp_path): + data = { + "name": "Jane Smith", + "email": "jane@example.com", + "phone": "555-1234", + "linkedin": "linkedin.com/in/janesmith", + "career_summary": "Experienced CSM with 8 years in SaaS.", + "nda_companies": ["AcmeCorp"], + "docs_dir": "~/Documents/JobSearch", + "ollama_models_dir": "~/models/ollama", + "vllm_models_dir": "~/models/vllm", + "inference_profile": "single-gpu", + "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, + } + } + p = tmp_path / "user.yaml" + p.write_text(yaml.dump(data)) + return p + +def test_loads_fields(profile_yaml): + p = UserProfile(profile_yaml) + assert p.name == "Jane Smith" + assert p.email == "jane@example.com" + assert p.nda_companies == ["acmecorp"] # stored lowercase + assert p.inference_profile == "single-gpu" + +def test_service_url_http(profile_yaml): + p = UserProfile(profile_yaml) + assert p.ollama_url == "http://localhost:11434" + assert p.vllm_url == "http://localhost:8000" + assert p.searxng_url == "http://localhost:8888" + +def test_service_url_https(tmp_path): + data = { + "name": "X", "services": { + "ollama_host": "myserver.com", "ollama_port": 443, + "ollama_ssl": True, "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, + } + } + p2 = tmp_path / "user2.yaml" + p2.write_text(yaml.dump(data)) + prof = UserProfile(p2) + assert prof.ollama_url == "https://myserver.com:443" + +def test_nda_mask(profile_yaml): + p = UserProfile(profile_yaml) + assert p.is_nda("AcmeCorp") + assert p.is_nda("acmecorp") # case-insensitive + assert not p.is_nda("Google") + +def test_missing_file_raises(): + with pytest.raises(FileNotFoundError): + UserProfile(Path("/nonexistent/user.yaml")) + +def test_exists_check(profile_yaml, tmp_path): + assert UserProfile.exists(profile_yaml) + assert not UserProfile.exists(tmp_path / "missing.yaml") + +def test_docs_dir_expanded(profile_yaml): + p = UserProfile(profile_yaml) + assert not str(p.docs_dir).startswith("~") + assert p.docs_dir.is_absolute()