"""Transactional resume patch and entry optimization orchestration.""" from __future__ import annotations from copy import deepcopy from typing import Any, Callable from .fsm import FSMError from .llm_services import log_ai_event from .models import ActionResponse, OptimizeEntryRequest, OptimizeRequest, ResumePatchRequest from .resume_document import ( DocumentError, apply_delete_bullet, apply_delete_entry, apply_update_basics, apply_update_bullet, apply_update_entry, apply_update_skill_groups, apply_update_profile_summary, confirm_profile_summary_proposal, reject_profile_summary_proposal, set_profile_summary_proposal, confirm_proposal, find_entry, reject_proposal, set_pending_proposal, undo_entry, ) DocumentOperation = Callable[[dict[str, Any], str], dict[str, Any]] class ResumeEditingMixin: database: Any expander: Any profile_summary_generator: Any def patch_resume(self, session_id: str, request: ResumePatchRequest) -> ActionResponse: with self.database.transaction(immediate=True) as connection: session = self._session_or_404(connection, session_id) resume = self._resume_or_409(connection, session_id) self._expect_revision(resume, request.expected_revision) try: content = self._apply_patch(resume["content"], request) except DocumentError as exc: raise _to_fsm(exc) from exc self.database.update_resume(connection, session_id, content) phone = (request.operation.fields or {}).get("phone") if request.operation.type == "update_basics" else None if phone: profile = dict(session["profile"]) profile["phone"] = str(phone).strip() profile["phone_source"] = "resume_edit" session = self.database.update_session( connection, session_id, stage=session["stage"], profile=profile, ) return self._action_response(session, None) def generate_profile_summary(self, session_id: str) -> ActionResponse: with self.database.transaction(immediate=True) as connection: session = self._session_or_404(connection, session_id) resume = self._resume_or_409(connection, session_id) try: summary_text = self.profile_summary_generator.generate(resume["content"]) content = set_profile_summary_proposal(resume["content"], summary_text) except DocumentError as exc: raise _to_fsm(exc) from exc except Exception as exc: log_ai_event( "profile_summary_regeneration_failed", reason_code=getattr(exc, "reason_code", type(exc).__name__), exception=type(exc).__name__, ) raise FSMError( "profile_summary_generation_failed", "\u4e2a\u4eba\u4ecb\u7ecd\u751f\u6210\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5", status_code=503, ) from exc self.database.update_resume(connection, session_id, content) return self._action_response(session, None) def confirm_profile_summary(self, session_id: str) -> ActionResponse: return self._profile_summary_op(session_id, confirm_profile_summary_proposal) def reject_profile_summary(self, session_id: str) -> ActionResponse: return self._profile_summary_op(session_id, reject_profile_summary_proposal) def _profile_summary_op( self, session_id: str, operation: Callable[[dict[str, Any]], dict[str, Any]] ) -> ActionResponse: with self.database.transaction(immediate=True) as connection: session = self._session_or_404(connection, session_id) resume = self._resume_or_409(connection, session_id) try: content = operation(resume["content"]) except DocumentError as exc: raise _to_fsm(exc) from exc self.database.update_resume(connection, session_id, content) return self._action_response(session, None) def optimize_entry(self, session_id: str, request: OptimizeRequest) -> ActionResponse: with self.database.transaction(immediate=True) as connection: session = self._session_or_404(connection, session_id) resume = self._resume_or_409(connection, session_id) found = find_entry(resume["content"], request.entry_id) if found is None: raise FSMError("entry_not_found", "Entry not found in resume", status_code=404) profile = session["profile"] section, entry = found context = { "job_type": profile.get("job_type"), "target_position": profile.get("target_position"), "instruction": request.instruction, "entry_type": section.get("kind"), } proposal = self.expander.expand(deepcopy(entry), context=context) try: content = set_pending_proposal( resume["content"], request.entry_id, proposal.get("optimized_description") or "", source=proposal.get("source", "ai_expanded"), changes=proposal.get("changes") or [], ) except DocumentError as exc: raise _to_fsm(exc) from exc self.database.update_resume(connection, session_id, content) return self._action_response(session, None) def confirm_optimize( self, session_id: str, request: OptimizeEntryRequest ) -> ActionResponse: return self._proposal_op(session_id, request.entry_id, confirm_proposal) def reject_optimize( self, session_id: str, request: OptimizeEntryRequest ) -> ActionResponse: return self._proposal_op(session_id, request.entry_id, reject_proposal) def undo_optimize( self, session_id: str, request: OptimizeEntryRequest ) -> ActionResponse: return self._proposal_op(session_id, request.entry_id, undo_entry) def _proposal_op( self, session_id: str, entry_id: str, operation: DocumentOperation ) -> ActionResponse: with self.database.transaction(immediate=True) as connection: session = self._session_or_404(connection, session_id) resume = self._resume_or_409(connection, session_id) try: content = operation(resume["content"], entry_id) except DocumentError as exc: raise _to_fsm(exc) from exc self.database.update_resume(connection, session_id, content) return self._action_response(session, None) @staticmethod def _apply_patch(content: dict[str, Any], request: ResumePatchRequest) -> dict[str, Any]: op = request.operation if op.type == "update_basics": return apply_update_basics(content, op.fields or {}) if op.type == "update_entry": return apply_update_entry(content, op.entry_id or "", op.fields or {}) if op.type == "update_skill_groups": return apply_update_skill_groups(content, op.skills or []) if op.type == "update_profile_summary": return apply_update_profile_summary(content, (op.fields or {}).get("content", "")) if op.type == "update_bullet": return apply_update_bullet(content, op.entry_id or "", op.bullet_id or "", op.text or "") if op.type == "delete_entry": return apply_delete_entry(content, op.entry_id or "") return apply_delete_bullet(content, op.entry_id or "", op.bullet_id or "") def _session_or_404(self, connection: Any, session_id: str) -> dict[str, Any]: session = self.database.fetch_session(connection, session_id) if session is None: raise FSMError("session_not_found", "Session not found", status_code=404) return session def _resume_or_409(self, connection: Any, session_id: str) -> dict[str, Any]: resume = self.database.fetch_resume(connection, session_id) if resume is None: raise FSMError("resume_not_created", "Create the resume before editing it") return resume @staticmethod def _expect_revision(resume: dict[str, Any], expected: int) -> None: if resume["revision"] != expected: raise FSMError("revision_conflict", "Resume was modified; refresh before editing") def _to_fsm(exc: DocumentError) -> FSMError: status = 404 if exc.code in {"entry_not_found", "bullet_not_found"} else 422 if exc.code == "proposal_stale": status = 409 return FSMError(exc.code, exc.message, status_code=status)