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
50833a5b67
commit
552f5822bc
2 changed files with 85 additions and 1 deletions
|
|
@ -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", []),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue