generated from kgod/ai-review-template
106 lines
3.8 KiB
Python
106 lines
3.8 KiB
Python
"""Injectable, grounded skill suggestions for the enrichment skills card."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Protocol
|
|
|
|
from pydantic import Field
|
|
|
|
from .enrichment_modules import skill_suggestions
|
|
from .llm_services import OpenAICompatibleStructuredClient, StrictSchema
|
|
from .settings import Settings
|
|
|
|
|
|
class SkillSuggester(Protocol):
|
|
"""Suggest skills from the user's target role and already-entered resume facts."""
|
|
|
|
def suggest(self, profile: dict[str, Any]) -> list[str]: ...
|
|
|
|
|
|
class RuleBasedSkillSuggester:
|
|
def suggest(self, profile: dict[str, Any]) -> list[str]:
|
|
return skill_suggestions(profile.get("target_position"), profile)
|
|
|
|
|
|
class SkillSuggestionOutput(StrictSchema):
|
|
skills: list[str] = Field(max_length=8)
|
|
|
|
|
|
class OpenAISkillSuggester:
|
|
def __init__(
|
|
self, completion: OpenAICompatibleStructuredClient, fallback: SkillSuggester
|
|
) -> None:
|
|
self.completion = completion
|
|
self.fallback = fallback
|
|
|
|
def suggest(self, profile: dict[str, Any]) -> list[str]:
|
|
fallback = self.fallback.suggest(profile)
|
|
try:
|
|
output = self.completion.complete(
|
|
schema=SkillSuggestionOutput,
|
|
schema_name="skill_suggestions",
|
|
system_prompt=(
|
|
"你是中文求职简历助手。根据目标岗位和用户已经填写的经历事实推荐技能标签。"
|
|
"只输出适合技能卡的简短技能名称,不要写句子、等级、熟练度或虚构项目成果。"
|
|
"可以补充目标岗位常见但用户尚未填写的技能,作为待学习/待确认建议;"
|
|
"不要把公司、学校、课程或奖项名称当作技能。"
|
|
),
|
|
payload={
|
|
"target_position": profile.get("target_position"),
|
|
"facts": _skill_facts(profile),
|
|
"existing_skills": (profile.get("tags") or {}).get("skills") or [],
|
|
},
|
|
)
|
|
except Exception:
|
|
return fallback
|
|
return _merge_suggestions(output.skills, fallback, profile)
|
|
|
|
|
|
def build_skill_suggester(
|
|
settings: Settings, client: Any | None = None
|
|
) -> SkillSuggester:
|
|
rules = RuleBasedSkillSuggester()
|
|
if not settings.use_openai:
|
|
return rules
|
|
return OpenAISkillSuggester(OpenAICompatibleStructuredClient(settings, client), rules)
|
|
|
|
|
|
def _skill_facts(profile: dict[str, Any]) -> list[dict[str, Any]]:
|
|
facts: list[dict[str, Any]] = []
|
|
entries: list[Any] = [profile.get("anchor")]
|
|
entries.extend(profile.get("experiences") or [])
|
|
for records in (profile.get("records") or {}).values():
|
|
entries.extend(records or [])
|
|
for entry in entries:
|
|
if not isinstance(entry, dict):
|
|
continue
|
|
description = str(entry.get("description") or "").strip()
|
|
highlights = [str(item).strip() for item in entry.get("highlights") or [] if str(item).strip()]
|
|
if description or highlights:
|
|
facts.append(
|
|
{
|
|
"record_type": entry.get("record_type"),
|
|
"description": description or None,
|
|
"highlights": highlights,
|
|
}
|
|
)
|
|
return facts
|
|
|
|
|
|
def _merge_suggestions(
|
|
proposed: list[str], fallback: list[str], profile: dict[str, Any]
|
|
) -> list[str]:
|
|
existing = {
|
|
str(skill).strip().casefold()
|
|
for skill in ((profile.get("tags") or {}).get("skills") or [])
|
|
if str(skill).strip()
|
|
}
|
|
result: list[str] = []
|
|
seen: set[str] = set()
|
|
for skill in [*proposed, *fallback]:
|
|
normalized = str(skill).strip()
|
|
key = normalized.casefold()
|
|
if normalized and len(normalized) <= 32 and key not in existing and key not in seen:
|
|
result.append(normalized)
|
|
seen.add(key)
|
|
return result[:8] |