generated from kgod/ai-review-template
feat: initialize resume agent with OfferPai sync
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, Protocol
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from .llm_services import OpenAICompatibleStructuredClient
|
||||
from .settings import Settings
|
||||
|
||||
|
||||
class ProfileSummaryOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
content: str = Field(min_length=20, max_length=600)
|
||||
|
||||
|
||||
class ProfileSummaryGenerator(Protocol):
|
||||
def generate(self, content: dict[str, Any]) -> str: ...
|
||||
|
||||
|
||||
class RuleBasedProfileSummaryGenerator:
|
||||
"""Deterministic Chinese summary for tests and configured rule fallback."""
|
||||
|
||||
def generate(self, content: dict[str, Any]) -> str:
|
||||
basics = content.get("basics") if isinstance(content.get("basics"), dict) else {}
|
||||
target = content.get("target") if isinstance(content.get("target"), dict) else {}
|
||||
position = str(target.get("position") or target.get("target_position") or "目标岗位").strip()
|
||||
groups = content.get("skill_groups") if isinstance(content.get("skill_groups"), list) else []
|
||||
skills = [
|
||||
str(skill).strip()
|
||||
for group in groups
|
||||
if isinstance(group, dict)
|
||||
for skill in group.get("skills") or []
|
||||
if str(skill).strip()
|
||||
][:5]
|
||||
first_entry: dict[str, Any] = {}
|
||||
for section in content.get("sections") or []:
|
||||
if isinstance(section, dict) and section.get("items"):
|
||||
candidate = section["items"][0]
|
||||
if isinstance(candidate, dict):
|
||||
first_entry = candidate
|
||||
break
|
||||
major = str(first_entry.get("major") or basics.get("major") or "").strip()
|
||||
focus = str(
|
||||
first_entry.get("company")
|
||||
or first_entry.get("project_name")
|
||||
or first_entry.get("school")
|
||||
or "相关实践"
|
||||
).strip()
|
||||
skill_text = "、".join(dict.fromkeys(skills)) or "相关技术与实践能力"
|
||||
major_text = f",具备{major}相关学习背景" if major else ""
|
||||
return f"面向{position}{major_text},具备{skill_text}等能力,拥有{focus}相关经历,能够结合已完成的项目与实践持续提升岗位匹配度。"
|
||||
|
||||
|
||||
class OpenAIProfileSummaryGenerator:
|
||||
def __init__(self, completion: OpenAICompatibleStructuredClient) -> None:
|
||||
self.completion = completion
|
||||
|
||||
def generate(self, content: dict[str, Any]) -> str:
|
||||
output: ProfileSummaryOutput = self.completion.complete(
|
||||
schema=ProfileSummaryOutput,
|
||||
schema_name="profile_summary",
|
||||
system_prompt=(
|
||||
"你是中文简历个人总结撰写助手。仅返回 JSON。根据用户已确认的简历内容,"
|
||||
"写一段 80 到 180 字、适合置于中文简历开头的个人总结。"
|
||||
"只概括目标岗位、教育/经历、项目和技能中的已有事实;不得包含手机、邮箱等隐私信息,"
|
||||
"不得编造公司、学校、项目、学历、奖项、证书或量化数字。"
|
||||
"内容应自然连贯,不使用标题、列表、Markdown 或解释。"
|
||||
),
|
||||
payload={"resume": _summary_source(content)},
|
||||
)
|
||||
return _validate_summary(output.content)
|
||||
|
||||
|
||||
class FallbackProfileSummaryGenerator:
|
||||
def __init__(self, primary: ProfileSummaryGenerator, fallback: ProfileSummaryGenerator) -> None:
|
||||
self.primary = primary
|
||||
self.fallback = fallback
|
||||
|
||||
def generate(self, content: dict[str, Any]) -> str:
|
||||
try:
|
||||
return self.primary.generate(content)
|
||||
except Exception:
|
||||
return self.fallback.generate(content)
|
||||
|
||||
|
||||
def build_profile_summary_generator(
|
||||
settings: Settings, client: Any | None = None
|
||||
) -> ProfileSummaryGenerator:
|
||||
rules = RuleBasedProfileSummaryGenerator()
|
||||
if not settings.use_openai:
|
||||
return rules
|
||||
primary = OpenAIProfileSummaryGenerator(OpenAICompatibleStructuredClient(settings, client))
|
||||
return FallbackProfileSummaryGenerator(primary, rules) if settings.fallback_to_rules else primary
|
||||
|
||||
|
||||
def generated_summary(content: str) -> dict[str, Any]:
|
||||
return {
|
||||
"content": _validate_summary(content),
|
||||
"source": "ai_generated",
|
||||
"generated_at": datetime.now(UTC).isoformat(),
|
||||
"stale": False,
|
||||
}
|
||||
|
||||
|
||||
def _validate_summary(value: str) -> str:
|
||||
clean = " ".join(str(value or "").split())
|
||||
if not 20 <= len(clean) <= 600:
|
||||
raise ValueError("profile_summary_invalid")
|
||||
return clean
|
||||
|
||||
|
||||
def _summary_source(content: dict[str, Any]) -> dict[str, Any]:
|
||||
result = deepcopy(content)
|
||||
basics = result.get("basics")
|
||||
if isinstance(basics, dict):
|
||||
for field in ("phone", "email", "masked_phone"):
|
||||
basics.pop(field, None)
|
||||
result.pop("profile_summary", None)
|
||||
return result
|
||||
Reference in New Issue
Block a user