feat: add UserProfile class with service URL generation and NDA helpers
This commit is contained in:
parent
f11a38eb0b
commit
6493cf5c5b
3 changed files with 229 additions and 0 deletions
34
config/user.yaml.example
Normal file
34
config/user.yaml.example
Normal 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
109
scripts/user_profile.py
Normal 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",
|
||||
}
|
||||
86
tests/test_user_profile.py
Normal file
86
tests/test_user_profile.py
Normal 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()
|
||||
Loading…
Reference in a new issue