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,425 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.claim_validator import validate_proposal
|
||||
from app.experience_optimizer import (
|
||||
FallbackExperienceOptimizer,
|
||||
OpenAIExperienceOptimizer,
|
||||
RuleStructuredExperienceOptimizer,
|
||||
_OPTIMIZATION_REPAIR_PROMPT,
|
||||
normalize_fact_ledger,
|
||||
required_material_fact_ids,
|
||||
)
|
||||
|
||||
|
||||
class FakeCompletion:
|
||||
def __init__(self, output: dict[str, Any]) -> None:
|
||||
self.output = output
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
self.prompts: list[str] = []
|
||||
|
||||
def complete(self, *, schema, schema_name, system_prompt, payload):
|
||||
self.calls.append({"schema_name": schema_name, "payload": payload})
|
||||
self.prompts.append(system_prompt)
|
||||
return schema.model_validate(self.output)
|
||||
|
||||
|
||||
class SequentialFakeCompletion:
|
||||
def __init__(self, outputs: list[dict[str, Any]]) -> None:
|
||||
self.outputs = outputs
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
self.prompts: list[str] = []
|
||||
|
||||
def complete(self, *, schema, schema_name, system_prompt, payload):
|
||||
self.calls.append({"schema_name": schema_name, "payload": payload})
|
||||
self.prompts.append(system_prompt)
|
||||
output_index = min(len(self.calls) - 1, len(self.outputs) - 1)
|
||||
return schema.model_validate(self.outputs[output_index])
|
||||
|
||||
|
||||
class FakeRetriever:
|
||||
def retrieve(self, **kwargs):
|
||||
return [
|
||||
{
|
||||
"id": "rag_1",
|
||||
"title": "Backend reference",
|
||||
"content": "A reference example reports an 80% throughput gain.",
|
||||
"original": "Built a service.",
|
||||
"optimized": "Improved throughput by 80%.",
|
||||
"points": "Action and result",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class FakeEmbedder:
|
||||
pass
|
||||
|
||||
|
||||
def fact_ledger() -> list[dict[str, str]]:
|
||||
return [
|
||||
{
|
||||
"id": "fact_1",
|
||||
"source": "user_form",
|
||||
"field": "description",
|
||||
"text": "Built the backend for a course submission system.",
|
||||
},
|
||||
{
|
||||
"id": "fact_2",
|
||||
"source": "user_answer",
|
||||
"field": "answer",
|
||||
"text": "It supported 20 classmates submitting assignments.",
|
||||
},
|
||||
{
|
||||
"id": "fact_3",
|
||||
"source": "user_answer",
|
||||
"field": "answer",
|
||||
"text": "Used FastAPI and PostgreSQL.",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def valid_output() -> dict[str, Any]:
|
||||
text = (
|
||||
"Used FastAPI and PostgreSQL to build the course submission backend, "
|
||||
"supporting 20 classmates submitting assignments."
|
||||
)
|
||||
return {
|
||||
"optimized_description": text,
|
||||
"bullets": [text],
|
||||
"star": {
|
||||
"situation": "Course submission scenario",
|
||||
"task": "Backend development",
|
||||
"action": "Implemented the API with FastAPI and PostgreSQL",
|
||||
"result": "Supported 20 classmates",
|
||||
},
|
||||
"changes": ["Reorganized the action and result"],
|
||||
"missing_facts": [],
|
||||
"claims": [
|
||||
{
|
||||
"text": text,
|
||||
"evidence_ids": ["fact_1", "fact_2", "fact_3"],
|
||||
"claim_type": "action",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_openai_optimizer_keeps_rag_as_style_reference_only() -> None:
|
||||
completion = FakeCompletion(valid_output())
|
||||
optimizer = OpenAIExperienceOptimizer(completion, FakeRetriever(), FakeEmbedder())
|
||||
|
||||
proposal = optimizer.optimize(
|
||||
{"description": "Built the backend for a course submission system."},
|
||||
context={"target_position": "Backend Engineer", "entry_type": "project_experience"},
|
||||
facts=fact_ledger(),
|
||||
)
|
||||
|
||||
payload = completion.calls[0]["payload"]
|
||||
assert payload["user_fact_ledger"] == fact_ledger()
|
||||
assert payload["style_references"][0]["id"] == "rag_1"
|
||||
assert "80%" in str(payload["style_references"])
|
||||
assert all("80%" not in fact["text"] for fact in payload["user_fact_ledger"])
|
||||
assert "用户提供的经历" in completion.prompts[0]
|
||||
assert proposal["source"] == "ai_expanded"
|
||||
|
||||
|
||||
def test_claim_validator_filters_rag_claim_without_rejecting_grounded_text() -> None:
|
||||
proposal = valid_output()
|
||||
proposal["claims"].append(
|
||||
{
|
||||
"text": "Improved throughput by 80%.",
|
||||
"evidence_ids": ["rag_1"],
|
||||
"claim_type": "result",
|
||||
}
|
||||
)
|
||||
|
||||
validated = validate_proposal(proposal, fact_ledger())
|
||||
|
||||
assert validated["optimized_description"] == valid_output()["optimized_description"]
|
||||
assert len(validated["claims"]) == 1
|
||||
assert "unsupported_evidence_reference" in validated["validation_warnings"]
|
||||
|
||||
|
||||
def test_unconfirmed_metric_is_retained_as_model_written_resume_prose() -> None:
|
||||
proposal = valid_output()
|
||||
proposal["optimized_description"] = (
|
||||
"Used FastAPI and PostgreSQL to build the course submission backend. "
|
||||
"Improved submission efficiency by 80%."
|
||||
)
|
||||
proposal["bullets"] = [proposal["optimized_description"]]
|
||||
|
||||
validated = validate_proposal(proposal, fact_ledger())
|
||||
|
||||
assert "80%" in validated["optimized_description"]
|
||||
assert not validated["unconfirmed_suggestions"]
|
||||
|
||||
|
||||
def test_counted_object_expansion_is_retained_not_a_hard_failure() -> None:
|
||||
proposal = valid_output()
|
||||
proposal["optimized_description"] = "Delivered 20 features for the course platform."
|
||||
proposal["bullets"] = [proposal["optimized_description"]]
|
||||
|
||||
validated = validate_proposal(proposal, fact_ledger())
|
||||
|
||||
assert validated["optimized_description"] == "Delivered 20 features for the course platform."
|
||||
assert not validated["unconfirmed_suggestions"]
|
||||
|
||||
|
||||
def test_new_technical_term_is_retained_for_controlled_role_expansion() -> None:
|
||||
proposal = valid_output()
|
||||
proposal["optimized_description"] = "Built the backend with FastAPI, PostgreSQL, and Redis."
|
||||
proposal["bullets"] = [proposal["optimized_description"]]
|
||||
|
||||
validated = validate_proposal(proposal, fact_ledger())
|
||||
|
||||
assert "Redis" in validated["optimized_description"]
|
||||
|
||||
|
||||
def test_omitted_material_fact_triggers_single_repair() -> None:
|
||||
initial = valid_output()
|
||||
initial["optimized_description"] = "Built the course submission backend with FastAPI and PostgreSQL."
|
||||
initial["bullets"] = [initial["optimized_description"]]
|
||||
completion = SequentialFakeCompletion([initial, valid_output()])
|
||||
optimizer = OpenAIExperienceOptimizer(completion, FakeRetriever(), FakeEmbedder())
|
||||
|
||||
proposal = optimizer.optimize(
|
||||
{"description": "Built the backend for a course submission system."},
|
||||
context={"entry_type": "project_experience"},
|
||||
facts=fact_ledger(),
|
||||
)
|
||||
|
||||
assert len(completion.calls) == 2
|
||||
assert completion.calls[1]["schema_name"] == "experience_optimization_repair"
|
||||
assert "20 classmates" in proposal["optimized_description"]
|
||||
assert proposal["omitted_fact_ids"] == []
|
||||
|
||||
|
||||
def test_omitted_material_fact_after_repair_is_flagged_and_relaxed() -> None:
|
||||
incomplete = valid_output()
|
||||
incomplete["optimized_description"] = "Built the course submission backend with FastAPI and PostgreSQL."
|
||||
incomplete["bullets"] = [incomplete["optimized_description"]]
|
||||
completion = SequentialFakeCompletion([incomplete, incomplete])
|
||||
optimizer = OpenAIExperienceOptimizer(completion, FakeRetriever(), FakeEmbedder())
|
||||
|
||||
proposal = optimizer.optimize(
|
||||
{"description": "Built the backend for a course submission system."},
|
||||
context={"entry_type": "project_experience"},
|
||||
facts=fact_ledger(),
|
||||
)
|
||||
|
||||
assert len(completion.calls) == 2
|
||||
assert proposal["omitted_fact_ids"] == ["fact_2"]
|
||||
assert "material_fact_omitted_after_repair" in proposal.get("validation_warnings", [])
|
||||
|
||||
|
||||
def test_imported_long_description_triggers_repair_when_candidate_is_compressed() -> None:
|
||||
imported_facts = [
|
||||
{
|
||||
"id": "imported_description",
|
||||
"source": "user_form",
|
||||
"field": "description",
|
||||
"text": (
|
||||
"搭建全栈求职平台,支持 WebSocket 流式预览;使用 sentence-transformers "
|
||||
"与 pgvector 实现岗位语义检索;通过 ASGI 部署和 asyncpg 缓解并发瓶颈;"
|
||||
"交付 58 个 API 接口,使用 Docker Compose 编排 7 个服务,并支持 PDF/DOCX 导出。"
|
||||
),
|
||||
}
|
||||
]
|
||||
compressed = valid_output()
|
||||
compressed["optimized_description"] = "构建全栈求职平台,集成简历生成、JD 分析和模拟面试功能。"
|
||||
compressed["bullets"] = [compressed["optimized_description"]]
|
||||
completion = SequentialFakeCompletion([compressed, compressed])
|
||||
optimizer = OpenAIExperienceOptimizer(completion, FakeRetriever(), FakeEmbedder())
|
||||
|
||||
proposal = optimizer.optimize(
|
||||
{"description": imported_facts[0]["text"]},
|
||||
context={"entry_type": "project_experience"},
|
||||
facts=imported_facts,
|
||||
)
|
||||
|
||||
assert len(completion.calls) == 2
|
||||
assert "material_fact_omitted_after_repair" in proposal.get("validation_warnings", [])
|
||||
assert proposal["omitted_fact_ids"] == [
|
||||
"imported_description_part_1",
|
||||
"imported_description_part_2",
|
||||
"imported_description_part_3",
|
||||
"imported_description_part_4",
|
||||
]
|
||||
|
||||
def test_optimizer_passes_completed_deep_interview_context_to_model() -> None:
|
||||
completion = FakeCompletion(valid_output())
|
||||
optimizer = OpenAIExperienceOptimizer(completion, FakeRetriever(), FakeEmbedder())
|
||||
history = [{"question_id": "q_1", "dimension": "personal_contribution", "answer": "Implemented API endpoints.", "status": "answered"}]
|
||||
|
||||
optimizer.optimize(
|
||||
{"description": "Built the backend for a course submission system."},
|
||||
context={
|
||||
"entry_type": "project_experience",
|
||||
"optimization_mode": "deep",
|
||||
"interview_completion": {"is_sufficient": True, "blocking_gaps": []},
|
||||
"completed_dimensions": ["personal_contribution"],
|
||||
"question_history": history,
|
||||
},
|
||||
facts=fact_ledger(),
|
||||
)
|
||||
|
||||
payload = completion.calls[0]["payload"]
|
||||
assert payload["deep_interview"]["completion"]["is_sufficient"] is True
|
||||
assert payload["deep_interview"]["completed_dimensions"] == ["personal_contribution"]
|
||||
assert payload["deep_interview"]["question_history"] == history
|
||||
|
||||
|
||||
def test_optimizer_keeps_model_prose_when_it_expands_beyond_literal_evidence() -> None:
|
||||
output = valid_output()
|
||||
output["optimized_description"] = (
|
||||
"Built the backend for a course submission system. "
|
||||
"It supported 20 classmates submitting assignments. "
|
||||
"Used FastAPI and PostgreSQL. "
|
||||
"Migrated 20 services to Redis and improved throughput by 80%."
|
||||
)
|
||||
output["bullets"] = [output["optimized_description"]]
|
||||
completion = FakeCompletion(output)
|
||||
optimizer = OpenAIExperienceOptimizer(completion, FakeRetriever(), FakeEmbedder())
|
||||
|
||||
proposal = optimizer.optimize(
|
||||
{"description": "Built the backend for a course submission system."},
|
||||
context={"entry_type": "project_experience"},
|
||||
facts=fact_ledger(),
|
||||
)
|
||||
|
||||
assert "Migrated 20 services" in proposal["optimized_description"]
|
||||
assert not proposal["unconfirmed_suggestions"]
|
||||
assert len(completion.calls) == 1
|
||||
|
||||
|
||||
def test_optimizer_succeeds_when_retriever_has_no_documents() -> None:
|
||||
class EmptyRetriever:
|
||||
def retrieve(self, **kwargs):
|
||||
return []
|
||||
|
||||
completion = FakeCompletion(valid_output())
|
||||
optimizer = OpenAIExperienceOptimizer(completion, EmptyRetriever(), FakeEmbedder())
|
||||
|
||||
proposal = optimizer.optimize(
|
||||
{"description": "Built the backend for a course submission system."},
|
||||
context={"entry_type": "project_experience"},
|
||||
facts=fact_ledger(),
|
||||
)
|
||||
|
||||
assert proposal["optimized_description"]
|
||||
assert completion.calls[0]["payload"]["style_references"] == []
|
||||
|
||||
|
||||
def test_optimizer_succeeds_when_retriever_is_unavailable() -> None:
|
||||
class BrokenRetriever:
|
||||
def retrieve(self, **kwargs):
|
||||
raise RuntimeError("vector store unavailable")
|
||||
|
||||
completion = FakeCompletion(valid_output())
|
||||
optimizer = OpenAIExperienceOptimizer(completion, BrokenRetriever(), FakeEmbedder())
|
||||
|
||||
proposal = optimizer.optimize(
|
||||
{"description": "Built the backend for a course submission system."},
|
||||
context={"entry_type": "project_experience"},
|
||||
facts=fact_ledger(),
|
||||
)
|
||||
|
||||
assert proposal["optimized_description"]
|
||||
assert completion.calls[0]["payload"]["style_references"] == []
|
||||
|
||||
|
||||
def test_rule_optimizer_reports_insufficient_facts_without_creating_content() -> None:
|
||||
proposal = RuleStructuredExperienceOptimizer().optimize({}, context={}, facts=[])
|
||||
|
||||
assert proposal["optimized_description"] == ""
|
||||
assert proposal["fallback_reason"] == "insufficient_user_facts"
|
||||
|
||||
|
||||
def test_fallback_optimizer_marks_rule_source_when_model_fails() -> None:
|
||||
class BrokenOptimizer:
|
||||
def optimize(self, entry, *, context, facts):
|
||||
raise RuntimeError("model unavailable")
|
||||
|
||||
optimizer = FallbackExperienceOptimizer(
|
||||
BrokenOptimizer(), RuleStructuredExperienceOptimizer()
|
||||
)
|
||||
proposal = optimizer.optimize(
|
||||
{"description": "Built an API."}, context={}, facts=fact_ledger()[:1]
|
||||
)
|
||||
|
||||
assert proposal["source"] == "rule_structured"
|
||||
assert proposal["optimized_description"]
|
||||
|
||||
def test_claim_validator_decodes_literal_unicode_escapes_in_suggestions() -> None:
|
||||
proposal = valid_output()
|
||||
proposal["unconfirmed_suggestions"] = [r"\u8FD8\u53EF\u8865\u5145\u7ED3\u679C"]
|
||||
|
||||
validated = validate_proposal(proposal, fact_ledger())
|
||||
|
||||
assert validated["unconfirmed_suggestions"] == ["还可补充结果"]
|
||||
|
||||
def test_normalize_fact_ledger_splits_multiline_description_into_part_facts() -> None:
|
||||
facts = [
|
||||
{
|
||||
"id": "fact_1",
|
||||
"source": "user_form",
|
||||
"field": "description",
|
||||
"text": (
|
||||
"全栈 AI 求职助手平台,包含 5 大功能模块:\n"
|
||||
"1. AI 对话式简历生成助手\n"
|
||||
"2. 简历导入 (PDF/DOCX 智能解析)\n"
|
||||
"技术栈: 前端 Next.js 14.2 + React 18.3"
|
||||
),
|
||||
}
|
||||
]
|
||||
ledger = normalize_fact_ledger(facts)
|
||||
|
||||
parts = [fact for fact in ledger if fact["field"] == "description_part"]
|
||||
assert [part["id"] for part in parts] == [
|
||||
"fact_1_part_1",
|
||||
"fact_1_part_2",
|
||||
"fact_1_part_3",
|
||||
"fact_1_part_4",
|
||||
]
|
||||
assert parts[1]["text"] == "AI 对话式简历生成助手"
|
||||
assert parts[3]["text"] == "前端 Next.js 14.2 + React 18.3"
|
||||
assert any(fact["field"] == "description" for fact in ledger)
|
||||
|
||||
|
||||
def test_normalize_fact_ledger_keeps_single_sentence_description_unsplit() -> None:
|
||||
facts = [
|
||||
{
|
||||
"id": "fact_1",
|
||||
"source": "user_form",
|
||||
"field": "description",
|
||||
"text": "Built the backend for a course submission system.",
|
||||
}
|
||||
]
|
||||
ledger = normalize_fact_ledger(facts)
|
||||
|
||||
assert [fact["id"] for fact in ledger] == ["fact_1"]
|
||||
|
||||
|
||||
def test_required_fact_ids_prefer_description_parts_over_parent() -> None:
|
||||
facts = [
|
||||
{
|
||||
"id": "fact_1",
|
||||
"source": "user_form",
|
||||
"field": "description",
|
||||
"text": "全栈 AI 求职助手平台,包含 5 大功能模块:\n1. AI 对话式简历生成助手\n2. 简历导入智能解析",
|
||||
},
|
||||
{"id": "fact_2", "source": "user_answer", "field": "answer", "text": "服务 300 名学生。"},
|
||||
]
|
||||
ledger = normalize_fact_ledger(facts)
|
||||
|
||||
required = required_material_fact_ids(ledger)
|
||||
|
||||
assert "fact_1" not in required
|
||||
assert {"fact_1_part_1", "fact_1_part_2", "fact_1_part_3", "fact_2"} <= set(required)
|
||||
|
||||
|
||||
|
||||
def test_optimization_repair_prompt_keeps_star_structure() -> None:
|
||||
"""修复稿与首发同构:STAR 结构要求不得在修复阶段丢失。"""
|
||||
assert "STAR" in _OPTIMIZATION_REPAIR_PROMPT
|
||||
Reference in New Issue
Block a user