feat: builder 简历生成 + 轻度优化 + 简历导入交付副本

自内部仓库剥离深度优化与 RAG 知识库后的交付版本:
- Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强)
- 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护
- 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏
- PostgreSQL 运行时 + Alembic 迁移链

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
hyp
2026-08-05 11:08:30 +08:00
co-authored by Claude
commit ae2d9b128d
191 changed files with 27719 additions and 0 deletions
@@ -0,0 +1,118 @@
"""Chat intent classifier tests: rule fallback, LLM client, fallback composition."""
from __future__ import annotations
from typing import Any
from app.chat_intents import CHAT_INTENT_REGISTRY_VERSION, ChatIntent, ChatTurnClassification
from app.chat_intent_classifier import (
ChatIntentClassifier,
FallbackChatIntentClassifier,
LLMChatIntentClassifier,
RuleBasedChatIntentClassifier,
build_chat_intent_classifier,
build_chat_state_summary,
)
from app.settings import Settings
PROFILE = {
"job_type": "campus",
"target_position": "后端工程师",
"resume_content": {
"sections": [
{"kind": "project_experience", "items": [{"project_name": "AI Career Copilot"}]},
]
},
}
SUMMARY = build_chat_state_summary(PROFILE, {"draft": {"section": "education"}})
def classify_rule(message: str) -> ChatTurnClassification:
return RuleBasedChatIntentClassifier().classify(message, state_summary=SUMMARY)
def test_rule_classifier_implements_protocol() -> None:
assert isinstance(RuleBasedChatIntentClassifier(), ChatIntentClassifier)
def test_rule_classifier_maps_legacy_keyword_signals() -> None:
assert classify_rule("没有").intent is ChatIntent.NO_INFO
revise = classify_rule("保留原文,不要用这版优化稿")
assert revise.intent is ChatIntent.REVISE_PROPOSAL
assert revise.revision_instruction
assert classify_rule("把学校名字改成东莞城市学院").intent is ChatIntent.EDIT_IDENTITY
new_entry = classify_rule("新增一段教育经历")
assert new_entry.intent is ChatIntent.NEW_ENTRY
assert new_entry.target_section == "education"
def test_rule_classifier_routes_edit_question_chitchat_and_facts() -> None:
edit = classify_rule("修改一下我之前写的那个 AI Career Copilot 项目经历")
assert edit.intent is ChatIntent.EDIT_ENTRY
assert edit.target_entry_hint == "AI Career Copilot"
question = classify_rule("这段经历怎么写比较好?")
assert question.intent is ChatIntent.ASK_QUESTION
assert question.user_question
assert classify_rule("好的,谢谢").intent is ChatIntent.CHITCHAT
facts = classify_rule("负责后端接口开发,使用 Python 和 FastAPI")
assert facts.intent is ChatIntent.PROVIDE_FACTS
assert facts.facts and facts.facts[0].text
assert facts.confidence < 0.5
def test_state_summary_compacts_profile_and_draft() -> None:
assert SUMMARY["job_type"] == "campus"
assert SUMMARY["target_position"] == "后端工程师"
assert SUMMARY["confirmed_entries"] == [
{"section": "project_experience", "label": "AI Career Copilot"}
]
assert SUMMARY["draft_section"] == "education"
assert build_chat_state_summary(PROFILE)["draft_section"] is None
class _StubClient:
def __init__(self, result: Any) -> None:
self.result = result
self.calls: list[dict[str, Any]] = []
def complete(self, **kwargs: Any) -> Any:
self.calls.append(kwargs)
return self.result
def test_llm_classifier_uses_registry_prompt_and_schema() -> None:
expected = ChatTurnClassification(intent="ask_question", user_question="怎么写?")
client = _StubClient(expected)
result = LLMChatIntentClassifier(client).classify("怎么写?", state_summary=SUMMARY)
assert result is expected
call = client.calls[0]
assert call["schema"] is ChatTurnClassification
assert call["schema_name"] == "chat_intent_classification"
assert call["payload"]["message"] == "怎么写?"
assert call["payload"]["state_summary"] is SUMMARY
assert call["payload"]["registry_version"] == CHAT_INTENT_REGISTRY_VERSION
prompt = call["system_prompt"]
assert CHAT_INTENT_REGISTRY_VERSION in prompt
for intent in ChatIntent:
assert intent.value in prompt
assert "保留原文" in prompt # few-shot examples reach the prompt
class _FailingClassifier:
def classify(self, message: str, *, state_summary: dict[str, Any]) -> ChatTurnClassification:
raise RuntimeError("boom")
def test_fallback_classifier_degrades_to_rules_on_llm_failure() -> None:
classifier = FallbackChatIntentClassifier(_FailingClassifier(), RuleBasedChatIntentClassifier())
result = classifier.classify("没有", state_summary=SUMMARY)
assert result.intent is ChatIntent.NO_INFO
def test_factory_returns_rules_without_openai_and_fallback_with_openai() -> None:
rule_only = build_chat_intent_classifier(Settings(llm_provider="rule"))
assert isinstance(rule_only, RuleBasedChatIntentClassifier)
composed = build_chat_intent_classifier(Settings(llm_provider="openai", openai_api_key="k"))
assert isinstance(composed, FallbackChatIntentClassifier)
assert isinstance(composed.fallback, RuleBasedChatIntentClassifier)