generated from kgod/ai-review-template
自内部仓库剥离深度优化与 RAG 知识库后的交付版本: - Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强) - 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护 - 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏 - PostgreSQL 运行时 + Alembic 迁移链 Co-Authored-By: Claude <noreply@anthropic.com>
97 lines
4.8 KiB
Python
97 lines
4.8 KiB
Python
"""Candidate rewrite for Builder entries (light STAR optimization)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from copy import deepcopy
|
||
import re
|
||
from typing import Any
|
||
|
||
from .state import _dedupe_strings
|
||
from ..experience_optimizer import _fact_text_is_preserved, split_description_parts
|
||
|
||
|
||
def _candidate_rewrite(
|
||
agent: Any, profile: dict[str, Any], entry: dict[str, Any], section: str, *, instruction: str | None = None,
|
||
ensure_facts: bool = False,
|
||
) -> dict[str, Any]:
|
||
try:
|
||
proposal = agent.expander.expand(
|
||
deepcopy(entry),
|
||
context={
|
||
"job_type": profile.get("job_type"),
|
||
"target_position": profile.get("target_position"),
|
||
"entry_type": section,
|
||
"instruction": instruction,
|
||
},
|
||
)
|
||
except Exception:
|
||
proposal = {}
|
||
original = str(entry.get("description") or "").strip()
|
||
optimized = str(proposal.get("optimized_description") or "").strip() or original
|
||
if ensure_facts:
|
||
# Explicit user-requested revision: still-missing material facts are folded
|
||
# back in (the user asked for them; this is not a silent auto-append).
|
||
missing = _uncovered_material_facts(optimized, original)
|
||
if missing:
|
||
if "• " in optimized:
|
||
optimized = optimized + "".join(f"\n• {fact}" for fact in missing)
|
||
else:
|
||
optimized = f"{optimized.rstrip('。')};{';'.join(missing)}。"
|
||
return {
|
||
"optimized_description": optimized,
|
||
"changes": proposal.get("changes") or [],
|
||
"source": proposal.get("source") or "ai_expanded",
|
||
"uncovered_facts": _uncovered_material_facts(optimized, original),
|
||
**({"generation_source": proposal["generation_source"]} if proposal.get("generation_source") else {}),
|
||
}
|
||
|
||
|
||
def _uncovered_material_facts(candidate: str, original: str) -> list[str]:
|
||
"""Material user facts the candidate dropped. Reported, never auto-appended."""
|
||
uncovered = [fact for fact in _material_fact_fragments(original) if not _fact_is_preserved(fact, candidate)]
|
||
fragments = split_description_parts(original)
|
||
if len(fragments) >= 2:
|
||
# Structured descriptions (feature lists, tech stack, outcomes) are checked
|
||
# fragment by fragment, so a dropped feature module is reported even when the
|
||
# tech stack survived. Single-sentence descriptions keep the regex-only path.
|
||
ledger = [
|
||
{"id": f"fragment_{index}", "source": "user_form", "field": "description_part", "text": fragment}
|
||
for index, fragment in enumerate(fragments, start=1)
|
||
]
|
||
uncovered.extend(
|
||
fragment
|
||
for index, fragment in enumerate(fragments, start=1)
|
||
if not _fact_text_is_preserved(f"fragment_{index}", ledger, candidate)
|
||
)
|
||
return _dedupe_strings(uncovered)
|
||
|
||
|
||
def _material_fact_fragments(text: str) -> list[str]:
|
||
facts: list[str] = []
|
||
patterns = (
|
||
r"gpa\s*[::]?\s*\d+(?:\.\d+)?\s*/\s*\d+(?:\.\d+)?",
|
||
r"(?:排名\s*)?(?:前\s*百分之\s*\d+(?:\.\d+)?|前\s*\d+(?:\.\d+)?\s*%|top\s*\d+(?:\.\d+)?\s*%)",
|
||
r"(?:专业|年级)?(?:排名)?前(?:十|二十|三十|五十)",
|
||
r"(?:获得|荣获|获评|获奖|取得)[^。;;\n]{0,30}(?:奖学金|奖项|荣誉|一等奖|二等奖|三等奖|优秀[^。;;\n]{0,12})",
|
||
r"(?:完成|参与|负责|主导|开发|设计|实现|搭建|推进|开展)[^。;;\n]{0,40}(?:课程项目|课程设计|项目|竞赛|实验室|实践|实训|研究|论文)",
|
||
r"(?:服务|覆盖|面向|参与|支持|管理|处理|完成|交付|提升|降低|增长)[^。;;\n]{0,20}?\d+(?:\.\d+)?\s*(?:%|人|名(?:学生|用户|客户|参与者)?|次|天|周|月|小时|万元|万|千|个|项|篇|场)",
|
||
)
|
||
for pattern in patterns:
|
||
facts.extend(match.group(0).strip(" \t,,") for match in re.finditer(pattern, text, flags=re.IGNORECASE))
|
||
tool_pattern = r"\b(?:python|sql|java|javascript|typescript|vue|react|excel|power\s*bi|tableau|pandas|tensorflow|pytorch|docker|git|linux)\b"
|
||
facts.extend(match.group(0).strip() for match in re.finditer(tool_pattern, text, flags=re.IGNORECASE))
|
||
return _dedupe_strings([fact for fact in facts if fact])
|
||
|
||
|
||
def _fact_is_preserved(fact: str, candidate: str) -> bool:
|
||
normalized_fact = _normalize_material_fact(fact)
|
||
normalized_candidate = _normalize_material_fact(candidate)
|
||
return bool(normalized_fact) and normalized_fact in normalized_candidate
|
||
|
||
|
||
def _normalize_material_fact(value: str) -> str:
|
||
normalized = value.casefold().replace("百分之", "%")
|
||
normalized = re.sub(r"(?:排名|专业排名|年级排名)?前\s*(\d+(?:\.\d+)?)\s*%?", r"top\1", normalized)
|
||
normalized = re.sub(r"top\s*(\d+(?:\.\d+)?)\s*%?", r"top\1", normalized)
|
||
return re.sub(r"[\s,,。;;::]", "", normalized)
|