generated from kgod/ai-review-template
feat: improve resume optimization and import reliability
This commit is contained in:
+192
-105
@@ -1,23 +1,18 @@
|
||||
"""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.
|
||||
"""
|
||||
"""Light entry expansion: pure LLM expander, fallback composition, and factory."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
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 .entry_expander import EntryExpander, RuleBasedEntryExpander, normalize_bullet_description
|
||||
from .fact_coverage import (
|
||||
FactRequirement,
|
||||
classify_fact_requirements,
|
||||
hard_fact_is_preserved,
|
||||
missing_hard_facts,
|
||||
)
|
||||
from .llm_services import (
|
||||
LLMServiceError,
|
||||
@@ -48,82 +43,90 @@ __all__ = [
|
||||
|
||||
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:
|
||||
_MIN_REPAIR_SECONDS = 6.0
|
||||
|
||||
def __init__(self, completion: Any, *, timeout_seconds: float | None = None) -> None:
|
||||
self.completion = completion
|
||||
settings = getattr(completion, "settings", None)
|
||||
configured_timeout = getattr(settings, "light_entry_timeout_seconds", None)
|
||||
self.timeout_seconds = timeout_seconds or configured_timeout
|
||||
|
||||
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)
|
||||
hard_required_facts, _ = classify_fact_requirements(fact_ledger)
|
||||
entry_type = str(context.get("entry_type") or "")
|
||||
primary_description = str(entry.get("description") or "").strip()
|
||||
output: EntryExpansionOutput = self.completion.complete(
|
||||
schema=EntryExpansionOutput,
|
||||
started_at = time.perf_counter()
|
||||
output: EntryExpansionOutput = self._complete(
|
||||
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),
|
||||
},
|
||||
payload=self._base_payload(
|
||||
facts_text=facts_text,
|
||||
primary_description=primary_description,
|
||||
entry_type=entry_type,
|
||||
context=context,
|
||||
hard_required_facts=hard_required_facts,
|
||||
),
|
||||
remaining_seconds=self._remaining_seconds(started_at),
|
||||
)
|
||||
|
||||
candidate = output.optimized_description.strip()
|
||||
candidate = _normalize_candidate(output.optimized_description, entry_type)
|
||||
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,
|
||||
remaining_seconds = self._remaining_seconds(started_at)
|
||||
if remaining_seconds is None or remaining_seconds >= self._MIN_REPAIR_SECONDS:
|
||||
log_ai_event("entry_expansion_repair_started", entry_type=entry_type, reason_code=repair_reason)
|
||||
output = self._complete_repair(
|
||||
facts_text=facts_text,
|
||||
primary_description=primary_description,
|
||||
entry_type=entry_type,
|
||||
context=context,
|
||||
hard_required_facts=hard_required_facts,
|
||||
optimized="",
|
||||
reason=repair_reason,
|
||||
missing_hard=[],
|
||||
started_at=started_at,
|
||||
)
|
||||
suggestions.extend(extra_suggestions)
|
||||
warnings.extend(extra_warnings)
|
||||
candidate = _normalize_candidate(output.optimized_description, entry_type)
|
||||
|
||||
optimized, suggestions, warnings = partition_entry_text(candidate, fact_ledger)
|
||||
if not optimized and primary_description:
|
||||
optimized = primary_description
|
||||
warnings.append("candidate_contains_unconfirmed_additions")
|
||||
|
||||
missing_hard = missing_hard_facts(hard_required_facts, optimized) if optimized else []
|
||||
if optimized and missing_hard:
|
||||
repair_reason = "hard_fact_omitted"
|
||||
log_ai_event(
|
||||
"entry_expansion_repair_started",
|
||||
entry_type=entry_type,
|
||||
reason_code=repair_reason,
|
||||
hard_fact_count=len(hard_required_facts),
|
||||
omitted_fact_count=len(missing_hard),
|
||||
)
|
||||
optimized, extra_suggestions, extra_warnings, output = self._repair_material_omissions(
|
||||
optimized,
|
||||
missing_hard,
|
||||
fact_ledger,
|
||||
facts_text=facts_text,
|
||||
primary_description=primary_description,
|
||||
entry_type=entry_type,
|
||||
context=context,
|
||||
hard_required_facts=hard_required_facts,
|
||||
reason=repair_reason,
|
||||
previous_output=output,
|
||||
started_at=started_at,
|
||||
)
|
||||
suggestions.extend(extra_suggestions)
|
||||
warnings.extend(extra_warnings)
|
||||
|
||||
if not optimized:
|
||||
fallback_reason = "repair_failed" if repair_reason else "insufficient_facts"
|
||||
log_ai_event(
|
||||
@@ -142,49 +145,100 @@ class OpenAIEntryExpander:
|
||||
"fallback_reason": fallback_reason,
|
||||
}
|
||||
|
||||
remaining_hard = missing_hard_facts(hard_required_facts, optimized)
|
||||
if remaining_hard:
|
||||
warnings.append("hard_fact_omitted_after_repair")
|
||||
return {
|
||||
"optimized_description": optimized,
|
||||
"changes": [item.strip() for item in output.changes if item.strip()][:5],
|
||||
"changes": [],
|
||||
"unconfirmed_suggestions": suggestions[:6],
|
||||
"validation_warnings": list(dict.fromkeys(warnings)),
|
||||
"uncovered_facts": remaining_hard[:8],
|
||||
"source": "ai_expanded",
|
||||
"generation_source": "llm",
|
||||
}
|
||||
|
||||
def _base_payload(
|
||||
self,
|
||||
*,
|
||||
facts_text: str,
|
||||
primary_description: str,
|
||||
entry_type: str,
|
||||
context: dict[str, Any],
|
||||
hard_required_facts: list[FactRequirement],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"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),
|
||||
"hard_required_facts": hard_required_facts,
|
||||
}
|
||||
|
||||
def _complete_repair(
|
||||
self,
|
||||
*,
|
||||
facts_text: str,
|
||||
primary_description: str,
|
||||
entry_type: str,
|
||||
context: dict[str, Any],
|
||||
hard_required_facts: list[FactRequirement],
|
||||
optimized: str,
|
||||
reason: str,
|
||||
missing_hard: list[str],
|
||||
started_at: float,
|
||||
) -> EntryExpansionOutput:
|
||||
payload = self._base_payload(
|
||||
facts_text=facts_text,
|
||||
primary_description=primary_description,
|
||||
entry_type=entry_type,
|
||||
context=context,
|
||||
hard_required_facts=hard_required_facts,
|
||||
)
|
||||
payload.update({
|
||||
"rejected_candidate": optimized,
|
||||
"rejected_reason": reason,
|
||||
"omitted_facts": missing_hard,
|
||||
})
|
||||
return self._complete(
|
||||
schema_name="entry_expansion_repair",
|
||||
system_prompt=_repair_prompt(entry_type),
|
||||
payload=payload,
|
||||
remaining_seconds=self._remaining_seconds(started_at),
|
||||
)
|
||||
|
||||
def _repair_material_omissions(
|
||||
self,
|
||||
optimized: str,
|
||||
missing: list[str],
|
||||
missing_hard: 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.
|
||||
"""
|
||||
hard_required_facts: list[FactRequirement],
|
||||
reason: str,
|
||||
previous_output: EntryExpansionOutput,
|
||||
started_at: float,
|
||||
) -> tuple[str, list[str], list[str], EntryExpansionOutput]:
|
||||
"""Run at most one repair pass; semantic source text is never raw-appended."""
|
||||
remaining_seconds = self._remaining_seconds(started_at)
|
||||
if remaining_seconds is not None and remaining_seconds < self._MIN_REPAIR_SECONDS:
|
||||
return optimized, [], ["repair_skipped_budget"], previous_output
|
||||
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,
|
||||
},
|
||||
repaired = self._complete_repair(
|
||||
facts_text=facts_text,
|
||||
primary_description=primary_description,
|
||||
entry_type=entry_type,
|
||||
context=context,
|
||||
hard_required_facts=hard_required_facts,
|
||||
optimized=optimized,
|
||||
reason=reason,
|
||||
missing_hard=missing_hard,
|
||||
started_at=started_at,
|
||||
)
|
||||
except Exception as exc:
|
||||
log_ai_event(
|
||||
@@ -193,25 +247,57 @@ class OpenAIEntryExpander:
|
||||
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
|
||||
return optimized, [], ["repair_failed"], EntryExpansionOutput(
|
||||
optimized_description=optimized,
|
||||
)
|
||||
repaired_text, extra_suggestions, repair_warnings = partition_entry_text(
|
||||
_normalize_candidate(repaired.optimized_description, entry_type), 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, []
|
||||
return optimized, [], ["repair_failed"], previous_output
|
||||
preserved_initial = [
|
||||
fact for fact in hard_required_facts if hard_fact_is_preserved(fact, optimized)
|
||||
]
|
||||
repaired_missing = missing_hard_facts(hard_required_facts, repaired_text)
|
||||
if (
|
||||
len(repaired_missing) >= len(missing_hard)
|
||||
or any(not hard_fact_is_preserved(fact, repaired_text) for fact in preserved_initial)
|
||||
or _repair_regresses_structure(optimized, repaired_text)
|
||||
):
|
||||
return optimized, [], ["repair_rejected_quality_regression"], previous_output
|
||||
return repaired_text, extra_suggestions, repair_warnings, repaired
|
||||
|
||||
def _remaining_seconds(self, started_at: float) -> float | None:
|
||||
if self.timeout_seconds is None:
|
||||
return None
|
||||
return max(0.1, self.timeout_seconds - (time.perf_counter() - started_at))
|
||||
|
||||
def _complete(
|
||||
self, *, schema_name: str, system_prompt: str, payload: dict[str, Any], remaining_seconds: float | None
|
||||
) -> EntryExpansionOutput:
|
||||
kwargs: dict[str, Any] = {
|
||||
"schema": EntryExpansionOutput,
|
||||
"schema_name": schema_name,
|
||||
"system_prompt": system_prompt,
|
||||
"payload": payload,
|
||||
}
|
||||
if remaining_seconds is not None:
|
||||
kwargs.update(timeout_seconds=remaining_seconds, max_attempts=1)
|
||||
return self.completion.complete(**kwargs)
|
||||
|
||||
|
||||
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)
|
||||
]
|
||||
def _repair_regresses_structure(original: str, repaired: str) -> bool:
|
||||
original_lines = [line for line in original.splitlines() if line.strip()]
|
||||
repaired_lines = [line for line in repaired.splitlines() if line.strip()]
|
||||
if len(original_lines) >= 2 and len(repaired_lines) < len(original_lines):
|
||||
return True
|
||||
return len(original) >= 120 and len(repaired) < len(original) * 0.65
|
||||
|
||||
def _normalize_candidate(candidate: str, entry_type: str) -> str:
|
||||
text = candidate.strip()
|
||||
if not text or entry_type == "education":
|
||||
return text
|
||||
return normalize_bullet_description(text)
|
||||
|
||||
|
||||
class FallbackEntryExpander:
|
||||
@@ -278,4 +364,5 @@ def build_expander(settings: Settings, client: Any | None = None) -> EntryExpand
|
||||
if not settings.use_openai:
|
||||
return rules
|
||||
completion = OpenAICompatibleStructuredClient(settings, client)
|
||||
return FallbackEntryExpander(OpenAIEntryExpander(completion), rules)
|
||||
primary = OpenAIEntryExpander(completion)
|
||||
return FallbackEntryExpander(primary, rules) if settings.fallback_to_rules else primary
|
||||
Reference in New Issue
Block a user