From ce8d5a4ac02ee3c59ac6f73b3f3ff7886515c061 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Thu, 5 Mar 2026 15:00:53 -0800 Subject: [PATCH] feat: add suggest_resume_keywords for skills/domains/keywords gap analysis Replaces NotImplementedError stub with full LLM-backed implementation. Builds a prompt from the last 3 resume positions plus already-selected skills/domains/keywords, calls LLMRouter, and returns de-duped suggestions in all three categories. --- scripts/suggest_helpers.py | 35 +++++++++++++++++++++++- tests/test_suggest_helpers.py | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/scripts/suggest_helpers.py b/scripts/suggest_helpers.py index a9a2651..6ac3475 100644 --- a/scripts/suggest_helpers.py +++ b/scripts/suggest_helpers.py @@ -124,4 +124,37 @@ def suggest_resume_keywords( Returns: {"skills": [...], "domains": [...], "keywords": [...]} """ - raise NotImplementedError + resume_context = _load_resume_context(resume_path) + + already_skills = ", ".join(current_kw.get("skills", [])) or "none" + already_domains = ", ".join(current_kw.get("domains", [])) or "none" + already_keywords = ", ".join(current_kw.get("keywords", [])) or "none" + + prompt = f"""You are helping a job seeker build a keyword profile used to score job description matches. + +--- RESUME BACKGROUND --- +{resume_context or "Not provided"} + +--- ALREADY SELECTED (do not repeat these) --- +Skills: {already_skills} +Domains: {already_domains} +Keywords: {already_keywords} + +Suggest additional tags in each of the three categories below. Only suggest tags NOT already in the lists above. + +SKILLS — specific technical or soft skills (e.g. "Salesforce", "Executive Communication", "SQL", "Stakeholder Management") +DOMAINS — industry verticals, company types, or functional areas (e.g. "B2B SaaS", "EdTech", "Non-profit", "Series A-C") +KEYWORDS — specific terms, methodologies, metrics, or JD phrases (e.g. "NPS", "churn prevention", "QBR", "cross-functional") + +Return ONLY valid JSON in exactly this format (no extra text): +{{"skills": ["Skill A", "Skill B"], + "domains": ["Domain A"], + "keywords": ["Keyword A", "Keyword B"]}}""" + + raw = LLMRouter().complete(prompt).strip() + parsed = _parse_json(raw) + return { + "skills": parsed.get("skills", []), + "domains": parsed.get("domains", []), + "keywords": parsed.get("keywords", []), + } diff --git a/tests/test_suggest_helpers.py b/tests/test_suggest_helpers.py index 4a9fd2b..2f071b5 100644 --- a/tests/test_suggest_helpers.py +++ b/tests/test_suggest_helpers.py @@ -95,3 +95,54 @@ def test_suggest_search_terms_raises_on_llm_exhausted(): with patch("scripts.suggest_helpers.LLMRouter", return_value=mock_router): with pytest.raises(RuntimeError, match="All LLM backends exhausted"): suggest_search_terms(["CSM"], RESUME_PATH, BLOCKLIST, USER_PROFILE) + + +# ── suggest_resume_keywords ─────────────────────────────────────────────────── + +CURRENT_KW = { + "skills": ["Customer Success", "SQL"], + "domains": ["B2B SaaS"], + "keywords": ["NPS"], +} + + +def test_suggest_resume_keywords_returns_all_three_categories(): + from scripts.suggest_helpers import suggest_resume_keywords + payload = { + "skills": ["Project Management"], + "domains": ["EdTech"], + "keywords": ["churn prevention"], + } + with _mock_llm(payload): + result = suggest_resume_keywords(RESUME_PATH, CURRENT_KW) + assert "skills" in result + assert "domains" in result + assert "keywords" in result + + +def test_suggest_resume_keywords_excludes_already_selected(): + from scripts.suggest_helpers import suggest_resume_keywords + with _mock_llm({"skills": [], "domains": [], "keywords": []}) as mock_cls: + suggest_resume_keywords(RESUME_PATH, CURRENT_KW) + prompt_sent = mock_cls.return_value.complete.call_args[0][0] + # Already-selected tags should appear in the prompt so LLM knows to skip them + assert "Customer Success" in prompt_sent + assert "NPS" in prompt_sent + + +def test_suggest_resume_keywords_returns_empty_on_bad_json(): + from scripts.suggest_helpers import suggest_resume_keywords + mock_router = MagicMock() + mock_router.complete.return_value = "I cannot assist." + with patch("scripts.suggest_helpers.LLMRouter", return_value=mock_router): + result = suggest_resume_keywords(RESUME_PATH, CURRENT_KW) + assert result == {"skills": [], "domains": [], "keywords": []} + + +def test_suggest_resume_keywords_raises_on_llm_exhausted(): + from scripts.suggest_helpers import suggest_resume_keywords + mock_router = MagicMock() + mock_router.complete.side_effect = RuntimeError("All LLM backends exhausted") + with patch("scripts.suggest_helpers.LLMRouter", return_value=mock_router): + with pytest.raises(RuntimeError, match="All LLM backends exhausted"): + suggest_resume_keywords(RESUME_PATH, CURRENT_KW)