generated from kgod/ai-review-template
自内部仓库剥离深度优化与 RAG 知识库后的交付版本: - Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强) - 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护 - 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏 - PostgreSQL 运行时 + Alembic 迁移链 Co-Authored-By: Claude <noreply@anthropic.com>
282 lines
11 KiB
Python
282 lines
11 KiB
Python
"""Light entry expansion: pure LLM expander, fallback composition, and factory.
|
|
|
|
The RAG knowledge base was removed (it only ever served the deep-optimization track).
|
|
Expansion is the model rewriting the user's own confirmed facts; every candidate still
|
|
passes through claim validation so unconfirmed additions never silently enter a resume.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from pydantic import Field
|
|
|
|
from .claim_validator import partition_entry_text, quantified_fact_contexts
|
|
from .entry_expander import EntryExpander, RuleBasedEntryExpander
|
|
from .experience_optimizer import (
|
|
_fact_text_is_preserved,
|
|
normalize_fact_ledger,
|
|
required_material_fact_ids,
|
|
)
|
|
from .llm_services import (
|
|
LLMServiceError,
|
|
OpenAICompatibleStructuredClient,
|
|
StrictSchema,
|
|
log_ai_event,
|
|
)
|
|
from .resume_expansion_prompts import (
|
|
_EDUCATION_PROMPT,
|
|
_EXPANSION_REPAIR_PROMPT,
|
|
_repair_prompt,
|
|
_system_prompt,
|
|
)
|
|
from .settings import Settings
|
|
|
|
__all__ = [
|
|
"EntryExpansionOutput",
|
|
"OpenAIEntryExpander",
|
|
"FallbackEntryExpander",
|
|
"build_expander",
|
|
"_EDUCATION_PROMPT",
|
|
"_EXPANSION_REPAIR_PROMPT",
|
|
"_system_prompt",
|
|
"_entry_fact_ledger",
|
|
"_entry_facts",
|
|
]
|
|
|
|
|
|
class EntryExpansionOutput(StrictSchema):
|
|
optimized_description: str
|
|
changes: list[str] = Field(max_length=5)
|
|
exemplar_titles: list[str] = Field(max_length=3)
|
|
|
|
|
|
class OpenAIEntryExpander:
|
|
"""LLM expander over user-confirmed facts only (no retrieval)."""
|
|
|
|
def __init__(self, completion: Any) -> None:
|
|
self.completion = completion
|
|
|
|
def expand(self, entry: dict[str, Any], *, context: dict[str, Any]) -> dict[str, Any]:
|
|
facts_text = _entry_facts(entry)
|
|
fact_ledger = _entry_fact_ledger(entry)
|
|
entry_type = str(context.get("entry_type") or "")
|
|
primary_description = str(entry.get("description") or "").strip()
|
|
output: EntryExpansionOutput = self.completion.complete(
|
|
schema=EntryExpansionOutput,
|
|
schema_name="entry_expansion",
|
|
system_prompt=_system_prompt(entry_type),
|
|
payload={
|
|
"entry_facts": facts_text,
|
|
"primary_description": primary_description,
|
|
"entry_type": entry_type or None,
|
|
"target_position": context.get("target_position"),
|
|
"instruction": context.get("instruction"),
|
|
"protected_quantity_facts": quantified_fact_contexts(facts_text),
|
|
},
|
|
)
|
|
|
|
candidate = output.optimized_description.strip()
|
|
repair_reason: str | None = None
|
|
if not candidate and primary_description:
|
|
repair_reason = "empty_result"
|
|
log_ai_event(
|
|
"entry_expansion_repair_started",
|
|
entry_type=entry_type,
|
|
reason_code=repair_reason,
|
|
)
|
|
repaired: EntryExpansionOutput = self.completion.complete(
|
|
schema=EntryExpansionOutput,
|
|
schema_name="entry_expansion_repair",
|
|
system_prompt=_repair_prompt(entry_type),
|
|
payload={
|
|
"entry_facts": facts_text,
|
|
"primary_description": primary_description,
|
|
"entry_type": entry_type or None,
|
|
"target_position": context.get("target_position"),
|
|
"instruction": context.get("instruction"),
|
|
"protected_quantity_facts": quantified_fact_contexts(facts_text),
|
|
"rejected_candidate": "",
|
|
"rejected_reason": repair_reason,
|
|
},
|
|
)
|
|
output = repaired
|
|
candidate = repaired.optimized_description.strip()
|
|
|
|
optimized, suggestions, warnings = partition_entry_text(candidate, fact_ledger)
|
|
if not optimized and primary_description:
|
|
# A model result composed only of unconfirmed additions must not become a failed
|
|
# card operation. Preserve the user's confirmed text and surface the additions.
|
|
optimized = primary_description
|
|
warnings.append("candidate_contains_unconfirmed_additions")
|
|
if optimized:
|
|
missing = _missing_material_facts(fact_ledger, optimized)
|
|
if missing:
|
|
optimized, extra_suggestions, extra_warnings = self._repair_material_omissions(
|
|
optimized,
|
|
missing,
|
|
fact_ledger,
|
|
facts_text=facts_text,
|
|
primary_description=primary_description,
|
|
entry_type=entry_type,
|
|
context=context,
|
|
)
|
|
suggestions.extend(extra_suggestions)
|
|
warnings.extend(extra_warnings)
|
|
if not optimized:
|
|
fallback_reason = "repair_failed" if repair_reason else "insufficient_facts"
|
|
log_ai_event(
|
|
"entry_expansion_rejected",
|
|
level=logging.WARNING,
|
|
entry_type=entry_type,
|
|
reason_code=fallback_reason,
|
|
)
|
|
return {
|
|
"optimized_description": "",
|
|
"changes": [],
|
|
"unconfirmed_suggestions": suggestions,
|
|
"validation_warnings": list(dict.fromkeys(warnings)),
|
|
"source": "ai_expanded",
|
|
"generation_source": "llm",
|
|
"fallback_reason": fallback_reason,
|
|
}
|
|
|
|
return {
|
|
"optimized_description": optimized,
|
|
"changes": [item.strip() for item in output.changes if item.strip()][:5],
|
|
"unconfirmed_suggestions": suggestions[:6],
|
|
"validation_warnings": list(dict.fromkeys(warnings)),
|
|
"source": "ai_expanded",
|
|
"generation_source": "llm",
|
|
}
|
|
|
|
|
|
def _repair_material_omissions(
|
|
self,
|
|
optimized: str,
|
|
missing: list[str],
|
|
fact_ledger: list[dict[str, str]],
|
|
*,
|
|
facts_text: str,
|
|
primary_description: str,
|
|
entry_type: str,
|
|
context: dict[str, Any],
|
|
) -> tuple[str, list[str], list[str]]:
|
|
"""One repair pass for candidates that dropped confirmed material facts.
|
|
|
|
Feature lists, product intros, and outcomes must not vanish while the
|
|
tech stack survives. The pre-repair candidate is kept when the repair
|
|
call fails or partitions to nothing: an omission never vetoes the draft.
|
|
"""
|
|
try:
|
|
repaired: EntryExpansionOutput = self.completion.complete(
|
|
schema=EntryExpansionOutput,
|
|
schema_name="entry_expansion_repair",
|
|
system_prompt=_repair_prompt(entry_type),
|
|
payload={
|
|
"entry_facts": facts_text,
|
|
"primary_description": primary_description,
|
|
"entry_type": entry_type or None,
|
|
"target_position": context.get("target_position"),
|
|
"instruction": context.get("instruction"),
|
|
"protected_quantity_facts": quantified_fact_contexts(facts_text),
|
|
"rejected_candidate": optimized,
|
|
"rejected_reason": "material_fact_omitted",
|
|
"omitted_facts": missing,
|
|
},
|
|
)
|
|
except Exception as exc:
|
|
log_ai_event(
|
|
"entry_expansion_coverage_repair_failed",
|
|
level=logging.WARNING,
|
|
entry_type=entry_type,
|
|
reason_code=getattr(exc, "reason_code", type(exc).__name__),
|
|
)
|
|
return optimized, [], ["material_fact_omitted"]
|
|
repaired_text, extra_suggestions, _ = partition_entry_text(
|
|
repaired.optimized_description.strip(), fact_ledger
|
|
)
|
|
if not repaired_text:
|
|
return optimized, [], ["material_fact_omitted"]
|
|
if _missing_material_facts(fact_ledger, repaired_text):
|
|
return repaired_text, extra_suggestions, ["material_fact_omitted_after_repair"]
|
|
return repaired_text, extra_suggestions, []
|
|
|
|
|
|
def _missing_material_facts(facts: list[dict[str, str]], narrative: str) -> list[str]:
|
|
ledger = normalize_fact_ledger(facts)
|
|
required = set(required_material_fact_ids(ledger))
|
|
return [
|
|
fact["text"]
|
|
for fact in ledger
|
|
if fact["id"] in required and not _fact_text_is_preserved(fact["id"], ledger, narrative)
|
|
]
|
|
|
|
|
|
class FallbackEntryExpander:
|
|
def __init__(self, primary: EntryExpander, fallback: EntryExpander) -> None:
|
|
self.primary = primary
|
|
self.fallback = fallback
|
|
|
|
def expand(self, entry: dict[str, Any], *, context: dict[str, Any]) -> dict[str, Any]:
|
|
try:
|
|
return self.primary.expand(entry, context=context)
|
|
except Exception as exc:
|
|
reason = exc.reason_code if isinstance(exc, LLMServiceError) else type(exc).__name__.lower()[:48]
|
|
log_ai_event(
|
|
"entry_expansion_failed",
|
|
level=logging.ERROR,
|
|
entry_type=str(context.get("entry_type") or ""),
|
|
reason_code=reason,
|
|
trace_id=getattr(exc, "trace_id", None),
|
|
exception=type(exc).__name__,
|
|
)
|
|
fallback = self.fallback.expand(entry, context=context)
|
|
optimized = str(fallback.get("optimized_description") or "").strip()
|
|
if optimized:
|
|
return {
|
|
**fallback,
|
|
"source": str(fallback.get("source") or "rule_polish"),
|
|
"generation_source": "rule_fallback",
|
|
"fallback_reason": reason,
|
|
}
|
|
return {
|
|
"optimized_description": "",
|
|
"changes": [],
|
|
"unconfirmed_suggestions": [],
|
|
"source": "rule_polish",
|
|
"generation_source": "unavailable",
|
|
"fallback_reason": reason,
|
|
}
|
|
|
|
|
|
def _entry_facts(entry: dict[str, Any]) -> str:
|
|
return "\n".join(item["text"] for item in _entry_fact_ledger(entry))
|
|
|
|
|
|
def _entry_fact_ledger(entry: dict[str, Any]) -> list[dict[str, str]]:
|
|
keys = (
|
|
"title", "organization", "role", "company", "position", "project_name", "project_role",
|
|
"school", "major", "degree", "start_date", "end_date_or_present", "name", "award", "date",
|
|
"description",
|
|
)
|
|
ledger: list[dict[str, str]] = []
|
|
for key in keys:
|
|
value = str(entry.get(key) or "").strip()
|
|
if value:
|
|
ledger.append({"id": f"entry_{key}", "field": key, "text": value})
|
|
for index, value in enumerate(entry.get("highlights") or [], start=1):
|
|
clean = str(value).strip()
|
|
if clean:
|
|
ledger.append({"id": f"entry_highlight_{index}", "field": "highlight", "text": clean})
|
|
return ledger
|
|
|
|
|
|
def build_expander(settings: Settings, client: Any | None = None) -> EntryExpander:
|
|
rules = RuleBasedEntryExpander()
|
|
if not settings.use_openai:
|
|
return rules
|
|
completion = OpenAICompatibleStructuredClient(settings, client)
|
|
return FallbackEntryExpander(OpenAIEntryExpander(completion), rules)
|