feat: add UserProfile class with service URL generation and NDA helpers

This commit is contained in:
pyr0ball 2026-02-24 18:29:45 -08:00
parent 1dc1ca89d7
commit 7380deb021
3 changed files with 229 additions and 0 deletions

34
config/user.yaml.example Normal file
View file

@ -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

109
scripts/user_profile.py Normal file
View file

@ -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",
}

View file

@ -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()