generated from kgod/ai-review-template
feat: builder 简历生成 + 轻度优化 + 简历导入交付副本
自内部仓库剥离深度优化与 RAG 知识库后的交付版本: - Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强) - 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护 - 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏 - PostgreSQL 运行时 + Alembic 迁移链 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
"""LLM rescue for messages the keyword routing drops to the generic fallback (问题1b)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from app.builder_conversation.rescue import llm_intent_rescue
|
||||
from app.chat_intents import ChatTurnClassification
|
||||
from app.settings import Settings
|
||||
from builder_flow_helpers import confirm_card, create_builder_session, finish_education, send_message, start_education
|
||||
from test_api import active_component
|
||||
|
||||
RESUME = {
|
||||
"sections": [
|
||||
{
|
||||
"kind": "project_experience",
|
||||
"items": [{"id": "e1", "project_name": "AI Career Copilot", "description": "全栈求职助手平台。"}],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class _StubClassifier:
|
||||
def __init__(self, result: ChatTurnClassification | None = None, exc: Exception | None = None) -> None:
|
||||
self.result = result
|
||||
self.exc = exc
|
||||
|
||||
def classify(self, message: str, *, state_summary: dict[str, Any]) -> ChatTurnClassification:
|
||||
if self.exc:
|
||||
raise self.exc
|
||||
assert self.result is not None
|
||||
return self.result
|
||||
|
||||
|
||||
def _agent(classifier: Any) -> Any:
|
||||
return SimpleNamespace(expander=SimpleNamespace(expand=lambda entry, *, context: {}), _chat_intent_classifier=classifier)
|
||||
|
||||
|
||||
def _components(transition: Any) -> list[dict[str, Any]]:
|
||||
return [block["data"] for block in transition.turn["blocks"] if block.get("type") == "component"]
|
||||
|
||||
|
||||
def test_rescue_off_mode_returns_none(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"app.builder_conversation.rescue.load_settings",
|
||||
lambda: Settings(llm_provider="rule", intent_router_mode="off"),
|
||||
)
|
||||
agent = SimpleNamespace()
|
||||
assert llm_intent_rescue(agent, {"job_type": "campus"}, "帮我重新优化描述", RESUME) is None
|
||||
|
||||
|
||||
def test_rescue_revise_regenerates_candidate_card() -> None:
|
||||
agent = _agent(_StubClassifier(ChatTurnClassification(
|
||||
intent="revise_proposal", confidence=0.9,
|
||||
target_entry_hint="AI Career Copilot", revision_instruction="重新优化",
|
||||
)))
|
||||
profile: dict[str, Any] = {"job_type": "campus"}
|
||||
transition = llm_intent_rescue(agent, profile, "帮我重新优化AI Career Copilot描述内容", RESUME)
|
||||
|
||||
assert transition is not None
|
||||
card = next(data for data in _components(transition) if data.get("component_name") == "ExperienceConfirmCard")
|
||||
assert card["ai_proposal"]["optimized_description"] == "全栈求职助手平台。"
|
||||
state = transition.profile["builder"]
|
||||
assert state["editing_entry_id"] == "e1"
|
||||
assert state["pending_entry"]["_proposal"]
|
||||
|
||||
|
||||
def test_rescue_edit_entry_begins_edit_flow() -> None:
|
||||
agent = _agent(_StubClassifier(ChatTurnClassification(
|
||||
intent="edit_entry", confidence=0.8, target_entry_hint="AI Career Copilot",
|
||||
)))
|
||||
transition = llm_intent_rescue(agent, {"job_type": "campus"}, "帮我改下AI Career Copilot这段", RESUME)
|
||||
|
||||
assert transition is not None
|
||||
assert "我找到了这段" in transition.turn["content"]
|
||||
assert transition.profile["builder"]["editing_entry_id"] == "e1"
|
||||
|
||||
|
||||
def test_rescue_edit_prefers_named_section_over_recent_entry() -> None:
|
||||
"""点名板块的修改必须落到该板块条目,而不是最近确认条目(教育→校园 错位根因)。"""
|
||||
resume = {
|
||||
"sections": [
|
||||
{"kind": "campus_experience", "items": [{"id": "c1", "organization": "学生会", "description": "招新宣传。"}]},
|
||||
{"kind": "education", "items": [{"id": "e9", "school": "Example University", "description": "主修课程。"}]},
|
||||
]
|
||||
}
|
||||
agent = _agent(_StubClassifier(ChatTurnClassification(
|
||||
intent="edit_entry", confidence=0.9, target_section="education",
|
||||
)))
|
||||
profile: dict[str, Any] = {"job_type": "campus", "builder": {"last_confirmed_entry": {"entry_id": "c1"}}}
|
||||
transition = llm_intent_rescue(agent, profile, "帮我重新优化教育经历", resume)
|
||||
|
||||
assert transition is not None
|
||||
assert transition.profile["builder"]["editing_entry_id"] == "e9"
|
||||
|
||||
|
||||
def test_rescue_edit_derives_section_from_message_when_classifier_omits_it() -> None:
|
||||
"""分类器没给 target_section 时,消息里的板块名必须确定性生效(项目→教育 错位根因)。"""
|
||||
resume = {
|
||||
"sections": [
|
||||
{"kind": "education", "items": [{"id": "e9", "school": "Example University", "description": "主修课程。"}]},
|
||||
{"kind": "project_experience", "items": [{"id": "p1", "project_name": "AI Career Copilot", "description": "全栈平台。"}]},
|
||||
]
|
||||
}
|
||||
agent = _agent(_StubClassifier(ChatTurnClassification(intent="edit_entry", confidence=0.9)))
|
||||
profile: dict[str, Any] = {"job_type": "campus", "builder": {"last_confirmed_entry": {"entry_id": "e9"}}}
|
||||
transition = llm_intent_rescue(agent, profile, "帮我重新优化项目经历", resume)
|
||||
|
||||
assert transition is not None
|
||||
assert transition.profile["builder"]["editing_entry_id"] == "p1"
|
||||
|
||||
|
||||
def test_rescue_edit_normalizes_chinese_section_label() -> None:
|
||||
"""分类器把 target_section 填成中文板块名时,先归一化到内部 kind 再定位。"""
|
||||
resume = {
|
||||
"sections": [
|
||||
{"kind": "education", "items": [{"id": "e9", "school": "Example University", "description": "主修课程。"}]},
|
||||
{"kind": "project_experience", "items": [{"id": "p1", "project_name": "AI Career Copilot", "description": "全栈平台。"}]},
|
||||
]
|
||||
}
|
||||
agent = _agent(_StubClassifier(ChatTurnClassification(
|
||||
intent="edit_entry", confidence=0.9, target_section="项目经历",
|
||||
)))
|
||||
profile: dict[str, Any] = {"job_type": "campus", "builder": {"last_confirmed_entry": {"entry_id": "e9"}}}
|
||||
transition = llm_intent_rescue(agent, profile, "帮我重新优化项目经历", resume)
|
||||
|
||||
assert transition is not None
|
||||
assert transition.profile["builder"]["editing_entry_id"] == "p1"
|
||||
|
||||
|
||||
def test_rescue_new_entry_offers_section_card() -> None:
|
||||
agent = _agent(_StubClassifier(ChatTurnClassification(
|
||||
intent="new_entry", confidence=0.8, target_section="internship_experience",
|
||||
)))
|
||||
transition = llm_intent_rescue(agent, {"job_type": "campus"}, "我还想补一段实习", RESUME)
|
||||
|
||||
assert transition is not None
|
||||
assert "实习经历" in transition.turn["content"]
|
||||
assert any(data.get("component_name") == "RecordFields" for data in _components(transition))
|
||||
|
||||
|
||||
def test_rescue_declines_low_confidence_and_failures() -> None:
|
||||
low = _agent(_StubClassifier(ChatTurnClassification(intent="edit_entry", confidence=0.4, target_entry_hint="AI Career Copilot")))
|
||||
assert llm_intent_rescue(low, {"job_type": "campus"}, "改下AI Career Copilot", RESUME) is None
|
||||
failing = _agent(_StubClassifier(exc=RuntimeError("boom")))
|
||||
assert llm_intent_rescue(failing, {"job_type": "campus"}, "随便一句", RESUME) is None
|
||||
unknown = _agent(_StubClassifier(ChatTurnClassification(intent="unclear", confidence=0.9)))
|
||||
assert llm_intent_rescue(unknown, {"job_type": "campus"}, "嗯", RESUME) is None
|
||||
|
||||
|
||||
def test_bottom_fallback_unchanged_without_opt_in(client: Any) -> None:
|
||||
session_id, body = create_builder_session(client)
|
||||
card = start_education(client, session_id, body)
|
||||
proposal = finish_education(client, session_id, card, "完成数据库课程项目。GPA: 4.3/5.0。")
|
||||
confirm_card(client, session_id, proposal)
|
||||
reply = send_message(client, session_id, "帮我重新优化Example University这段经历的描述")
|
||||
assert reply["turn"]["content"].startswith("可以。")
|
||||
|
||||
|
||||
def test_bottom_fallback_rescued_by_llm_classifier(client: Any, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
agent = client.app.state.resume_agent
|
||||
stub = _StubClassifier(ChatTurnClassification(
|
||||
intent="revise_proposal", confidence=0.92,
|
||||
target_entry_hint="Example University", revision_instruction="重新优化描述",
|
||||
))
|
||||
monkeypatch.setattr(agent, "_chat_intent_classifier", stub, raising=False)
|
||||
|
||||
session_id, body = create_builder_session(client)
|
||||
card = start_education(client, session_id, body)
|
||||
proposal = finish_education(client, session_id, card, "完成数据库课程项目。GPA: 4.3/5.0。")
|
||||
confirm_card(client, session_id, proposal)
|
||||
reply = send_message(client, session_id, "帮我重新优化Example University这段经历的描述")
|
||||
|
||||
assert active_component(reply)["data"]["component"] == "experience_confirm_card"
|
||||
assert "重新" in reply["turn"]["content"]
|
||||
Reference in New Issue
Block a user