"""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)