Files
resume-agent/backend/app/builder_conversation/predicates.py
T

115 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Rule predicates for Builder messages (legacy keyword routing, kept as fallback)."""
from __future__ import annotations
import re
from typing import Any
from .constants import (
GAP_PROMPTS,
IDENTITY_CHANGE_TERMS,
MAX_GAP_DIMENSIONS,
MAX_GAPS_PER_TURN,
SECTION_GAP_DIMENSIONS,
SECTION_KEYWORDS,
)
def _requests_new_entry(content: str) -> bool:
normalized = content.casefold()
return any(token in normalized for token in ("新增", "新建", "再添加", "再补充", "另一段", "另一个", "第二段", "写一段"))
def _is_revision_instruction(content: str) -> bool:
normalized = re.sub(r"[\s,,。;;!?]", "", content.casefold())
correction_terms = ("不要跳过", "没有跳过", "没说要跳过", "没有说要跳过", "不是这个意思", "保留原文", "保留这段", "不要删除", "不要删", "无需跳过")
return any(term in normalized for term in correction_terms)
def _requested_section(content: str) -> str | None:
normalized = content.casefold()
if not any(token in normalized for token in ("补充", "新增", "添加", "新建", "写一段")):
return None
return next((kind for kind, tokens in SECTION_KEYWORDS.items() if any(token in normalized for token in tokens)), None)
def _looks_like_recent_continuation(state: dict[str, Any], content: str) -> bool:
if not isinstance(state.get("last_confirmed_entry"), dict):
return False
normalized = re.sub(r"[,。!!?\s]", "", content.casefold())
if len(normalized) < 4:
return False
if normalized in {"可以", "好的", "继续", "没问题", "谢谢", "知道了"}:
return False
continuation_terms = ("对了", "还", "另外", "前面", "之前", "补充", "获得", "拿过", "拿到")
return any(term in normalized for term in continuation_terms) and not any(
token in normalized for token in ("新增", "新建", "写一段", "另一段", "别的经历")
)
def _matching_entries(resume_content: dict[str, Any], content: str) -> list[tuple[dict[str, Any], dict[str, Any]]]:
normalized = content.casefold()
if not any(token in normalized for token in ("修改", "编辑", "调整", "补充")):
return []
all_entries = [(section, entry) for section in resume_content.get("sections") or [] if isinstance(section, dict) for entry in section.get("items") or [] if isinstance(entry, dict)]
named = [
pair for pair in all_entries
if any(str(pair[1].get(key) or "").strip().casefold() in normalized for key in ("company", "project_name", "school", "organization", "position", "role") if str(pair[1].get(key) or "").strip())
]
if named:
return named
kinds = [kind for kind, tokens in SECTION_KEYWORDS.items() if any(token in normalized for token in tokens)]
return [pair for pair in all_entries if str(pair[0].get("kind") or "") in kinds]
def _entry_by_id(resume_content: dict[str, Any], entry_id: str) -> tuple[dict[str, Any], dict[str, Any]] | None:
for section in resume_content.get("sections") or []:
if not isinstance(section, dict):
continue
for entry in section.get("items") or []:
if isinstance(entry, dict) and entry.get("id") == entry_id:
return section, entry
return None
def _requests_identity_change(content: str) -> bool:
normalized = content.casefold()
return any(token in normalized for token in ("改", "修改", "变更", "换")) and any(term in normalized for term in IDENTITY_CHANGE_TERMS)
def _is_no_information_reply(content: str) -> bool:
normalized = re.sub(r"[\s,,。;;!?]", "", content.casefold())
return normalized in {
"没有", "没", "无", "暂无", "没了", "没有了", "不清楚", "不确定",
"跳过", "先跳过", "跳过吧", "暂时跳过", "略过", "不用了", "先不用", "不需要", "暂时不用", "以后再说", "再说吧",
}
def _dimension_present(dimension: str, entry: dict[str, Any]) -> bool:
# Identity fields such as dates are not evidence of an experience outcome or scale.
text = str(entry.get("description") or "").casefold()
patterns = {
"academic_result": r"gpa|均分|成绩|绩点|排名|top\s*\d+|前\s*\d+|奖学金|荣誉|获奖|奖项",
"practice_evidence": r"课程项目|课程设计|项目|竞赛|实验室|实践|实训|研究|论文",
"contribution_method": r"负责|主导|参与|设计|开发|实现|搭建|分析|调研|协调|测试|维护|优化|使用|通过|python|sql|java|vue|react|excel",
"delivery_or_outcome": r"交付|上线|发布|落地|完成|产出|验收|结果|成果|提升|降低|减少|增长|获得|达成",
"scale_or_metric": r"\d|百分比|%|人|次|天|周|月|小时|万元|万|千|覆盖|规模|效率|质量",
"responsibility_execution": r"负责|主导|参与|组织|策划|执行|协调|运营|宣传|招募|管理",
"scale_or_result": r"\d|人|次|场|覆盖|规模|参与|报名|增长|完成|结果|成果|获奖",
}
return bool(re.search(patterns[dimension], text, flags=re.IGNORECASE))
def _next_gap_dimensions(section: str, entry: dict[str, Any], state: dict[str, Any]) -> list[str]:
asked = set(state["asked"])
skipped = set(state["skipped"])
if len(asked) >= MAX_GAP_DIMENSIONS:
return []
candidates = [dimension for dimension in SECTION_GAP_DIMENSIONS.get(section, ()) if dimension not in skipped and not _dimension_present(dimension, entry)]
remaining_capacity = MAX_GAP_DIMENSIONS - len(asked)
return candidates[: min(MAX_GAPS_PER_TURN, remaining_capacity)]
def _gap_prompt(dimensions: list[str]) -> str:
return "\n".join(GAP_PROMPTS[dimension] for dimension in dimensions)