generated from kgod/ai-review-template
207 lines
8.8 KiB
Python
207 lines
8.8 KiB
Python
"""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)
|
|
|
|
|
|
|