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.
This commit is contained in:
parent
4e600c3019
commit
ce8d5a4ac0
2 changed files with 85 additions and 1 deletions
|
|
@ -124,4 +124,37 @@ def suggest_resume_keywords(
|
||||||
|
|
||||||
Returns: {"skills": [...], "domains": [...], "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", []),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,3 +95,54 @@ def test_suggest_search_terms_raises_on_llm_exhausted():
|
||||||
with patch("scripts.suggest_helpers.LLMRouter", return_value=mock_router):
|
with patch("scripts.suggest_helpers.LLMRouter", return_value=mock_router):
|
||||||
with pytest.raises(RuntimeError, match="All LLM backends exhausted"):
|
with pytest.raises(RuntimeError, match="All LLM backends exhausted"):
|
||||||
suggest_search_terms(["CSM"], RESUME_PATH, BLOCKLIST, USER_PROFILE)
|
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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue