generated from kgod/ai-review-template
feat: improve resume optimization and import reliability
This commit is contained in:
+230
-138
@@ -8,7 +8,7 @@ from threading import Lock
|
||||
from typing import Any, Callable
|
||||
from uuid import uuid4
|
||||
|
||||
from .database import Database
|
||||
from .database import Database, SessionRevisionConflict
|
||||
from .enrichment import prepare_rewrite_confirmation, process_rewrite_confirmation
|
||||
from .fsm import (
|
||||
FSMError,
|
||||
@@ -914,7 +914,9 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
def component_event(
|
||||
self, session_id: str, request: ComponentEventRequest
|
||||
) -> ActionResponse:
|
||||
with self.database.transaction(immediate=True) as connection:
|
||||
# Snapshot state before model-backed transition work. The write below
|
||||
# verifies these versions before persisting to prevent stale results.
|
||||
with self.database.transaction() as connection:
|
||||
session = self.database.fetch_session(connection, session_id)
|
||||
if session is None:
|
||||
raise FSMError("session_not_found", "Session not found", status_code=404)
|
||||
@@ -934,109 +936,166 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
"Use POST /sessions/{session_id}/create for resume creation",
|
||||
status_code=422,
|
||||
)
|
||||
if Stage(session["stage"]) == Stage.BUILDER_CONVERSATION:
|
||||
resume = self.database.fetch_resume(connection, session_id)
|
||||
if resume is None:
|
||||
raise FSMError("resume_not_created", "Create the resume before using Builder cards")
|
||||
transition = builder_conversation.process_component_event(
|
||||
session["profile"],
|
||||
block["data"],
|
||||
request.action,
|
||||
request.payload,
|
||||
resume["content"],
|
||||
self.skill_suggester,
|
||||
)
|
||||
elif Stage(session["stage"]) == Stage.CONTENT_READY and block["data"].get(
|
||||
"confirmation_kind"
|
||||
) == "rewrite":
|
||||
transition = process_rewrite_confirmation(
|
||||
session["profile"], request.action, request.payload
|
||||
)
|
||||
else:
|
||||
transition = process_component_event(
|
||||
stage=Stage(session["stage"]),
|
||||
profile=session["profile"],
|
||||
component_data=block["data"],
|
||||
action=request.action,
|
||||
payload=request.payload,
|
||||
)
|
||||
if getattr(transition, "polish_description", False):
|
||||
self._polish_module_entry(transition)
|
||||
if getattr(transition, "propose_anchor_optimization", False):
|
||||
self._propose_anchor_optimization(transition)
|
||||
if getattr(transition, "suggest_skills", False):
|
||||
self._suggest_skills(transition)
|
||||
if getattr(transition, "suggest_target_positions", False):
|
||||
self._suggest_target_positions(transition)
|
||||
anchor_proposal = transition.profile.get("anchor_proposal")
|
||||
if transition.stage == Stage.MINIMUM_READY:
|
||||
transition.profile.pop("anchor_proposal", None)
|
||||
if (
|
||||
isinstance(anchor_proposal, dict)
|
||||
and isinstance(transition.profile.get("anchor"), dict)
|
||||
and request.payload.get("use_optimized") is True
|
||||
):
|
||||
transition.profile["anchor"]["description"] = anchor_proposal[
|
||||
"optimized_description"
|
||||
]
|
||||
transition.profile["anchor"]["provenance"] = anchor_proposal["source"]
|
||||
elif transition.stage == Stage.ANCHOR_COLLECTING:
|
||||
transition.profile.pop("anchor_proposal", None)
|
||||
self.database.update_block(
|
||||
connection,
|
||||
block["id"],
|
||||
lifecycle=transition.lifecycle,
|
||||
)
|
||||
draft_id = session.get("draft_id")
|
||||
if transition.create_draft:
|
||||
draft_id = draft_id or f"draft_{uuid4().hex}"
|
||||
preview = merge_ids(None, self.rewriter.rewrite(transition.profile))
|
||||
transition.turn["blocks"].insert(
|
||||
-1,
|
||||
{
|
||||
"type": "resume_patch",
|
||||
"lifecycle": "submitted",
|
||||
"data": {"draft_id": draft_id, "operation": "replace", "value": preview},
|
||||
},
|
||||
)
|
||||
resume_content = getattr(transition, "resume_content", None)
|
||||
if resume_content is None and getattr(transition, "refresh_resume", False):
|
||||
resume_content = self.rewriter.rewrite(transition.profile)
|
||||
transition.resume_content = resume_content
|
||||
resume = None
|
||||
if resume_content is not None:
|
||||
resume = self.database.fetch_resume(connection, session_id)
|
||||
if resume is None:
|
||||
raise FSMError("resume_not_created", "Create the resume before confirming content")
|
||||
resume_content = (
|
||||
merge_profile_refresh(resume["content"], resume_content)
|
||||
if getattr(transition, "refresh_resume", False)
|
||||
else merge_ids(resume["content"], resume_content)
|
||||
)
|
||||
if getattr(transition, "generate_profile_summary", False):
|
||||
resume = resume or self.database.fetch_resume(connection, session_id)
|
||||
if resume is None:
|
||||
raise FSMError("resume_not_created", "Create the resume before finishing content")
|
||||
base_content = resume_content if resume_content is not None else resume["content"]
|
||||
summary = base_content.get("profile_summary")
|
||||
should_generate_summary = not isinstance(summary, dict) or summary.get("stale") is True
|
||||
if should_generate_summary:
|
||||
try:
|
||||
summary_text = self.profile_summary_generator.generate(base_content)
|
||||
resume_content = set_generated_profile_summary(
|
||||
base_content, summary_text, replace_stale=True
|
||||
)
|
||||
resume = self.database.fetch_resume(connection, session_id)
|
||||
|
||||
except Exception as exc:
|
||||
log_ai_event(
|
||||
"profile_summary_generation_failed",
|
||||
level=logging.WARNING,
|
||||
reason_code=getattr(exc, "reason_code", type(exc).__name__),
|
||||
exception=type(exc).__name__,
|
||||
)
|
||||
expected_session_revision = session["revision"]
|
||||
expected_block_version = block["version"]
|
||||
expected_resume_revision = resume["revision"] if resume is not None else None
|
||||
if Stage(session["stage"]) == Stage.BUILDER_CONVERSATION:
|
||||
if resume is None:
|
||||
raise FSMError("resume_not_created", "Create the resume before using Builder cards")
|
||||
transition = builder_conversation.process_component_event(
|
||||
session["profile"],
|
||||
block["data"],
|
||||
request.action,
|
||||
request.payload,
|
||||
resume["content"],
|
||||
self.skill_suggester,
|
||||
)
|
||||
elif Stage(session["stage"]) == Stage.CONTENT_READY and block["data"].get(
|
||||
"confirmation_kind"
|
||||
) == "rewrite":
|
||||
transition = process_rewrite_confirmation(
|
||||
session["profile"], request.action, request.payload
|
||||
)
|
||||
else:
|
||||
transition = process_component_event(
|
||||
stage=Stage(session["stage"]),
|
||||
profile=session["profile"],
|
||||
component_data=block["data"],
|
||||
action=request.action,
|
||||
payload=request.payload,
|
||||
)
|
||||
if getattr(transition, "polish_description", False):
|
||||
self._polish_module_entry(transition)
|
||||
if getattr(transition, "propose_anchor_optimization", False):
|
||||
self._propose_anchor_optimization(transition)
|
||||
if getattr(transition, "suggest_skills", False):
|
||||
self._suggest_skills(transition)
|
||||
if getattr(transition, "suggest_target_positions", False):
|
||||
self._suggest_target_positions(transition)
|
||||
anchor_proposal = transition.profile.get("anchor_proposal")
|
||||
if transition.stage == Stage.MINIMUM_READY:
|
||||
transition.profile.pop("anchor_proposal", None)
|
||||
if (
|
||||
isinstance(anchor_proposal, dict)
|
||||
and isinstance(transition.profile.get("anchor"), dict)
|
||||
and request.payload.get("use_optimized") is True
|
||||
):
|
||||
transition.profile["anchor"]["description"] = anchor_proposal[
|
||||
"optimized_description"
|
||||
]
|
||||
transition.profile["anchor"]["provenance"] = anchor_proposal["source"]
|
||||
elif transition.stage == Stage.ANCHOR_COLLECTING:
|
||||
transition.profile.pop("anchor_proposal", None)
|
||||
draft_id = session.get("draft_id")
|
||||
if transition.create_draft:
|
||||
draft_id = draft_id or f"draft_{uuid4().hex}"
|
||||
preview = merge_ids(None, self.rewriter.rewrite(transition.profile))
|
||||
transition.turn["blocks"].insert(
|
||||
-1,
|
||||
{
|
||||
"type": "resume_patch",
|
||||
"lifecycle": "submitted",
|
||||
"data": {"draft_id": draft_id, "operation": "replace", "value": preview},
|
||||
},
|
||||
)
|
||||
resume_content = getattr(transition, "resume_content", None)
|
||||
if resume_content is None and getattr(transition, "refresh_resume", False):
|
||||
resume_content = self.rewriter.rewrite(transition.profile)
|
||||
transition.resume_content = resume_content
|
||||
if resume_content is not None:
|
||||
if resume is None:
|
||||
raise FSMError("resume_not_created", "Create the resume before confirming content")
|
||||
resume_content = (
|
||||
merge_profile_refresh(resume["content"], resume_content)
|
||||
if getattr(transition, "refresh_resume", False)
|
||||
else merge_ids(resume["content"], resume_content)
|
||||
)
|
||||
if getattr(transition, "generate_profile_summary", False):
|
||||
if resume is None:
|
||||
raise FSMError("resume_not_created", "Create the resume before finishing content")
|
||||
base_content = resume_content if resume_content is not None else resume["content"]
|
||||
summary = base_content.get("profile_summary")
|
||||
should_generate_summary = not isinstance(summary, dict) or summary.get("stale") is True
|
||||
if should_generate_summary:
|
||||
try:
|
||||
summary_text = self.profile_summary_generator.generate(base_content)
|
||||
resume_content = set_generated_profile_summary(
|
||||
base_content, summary_text, replace_stale=True
|
||||
)
|
||||
except Exception as exc:
|
||||
log_ai_event(
|
||||
"profile_summary_generation_failed",
|
||||
level=logging.WARNING,
|
||||
reason_code=getattr(exc, "reason_code", type(exc).__name__),
|
||||
exception=type(exc).__name__,
|
||||
)
|
||||
|
||||
with self.database.transaction(immediate=True) as connection:
|
||||
current_session = self.database.fetch_session(connection, session_id)
|
||||
if current_session is None:
|
||||
raise FSMError("session_not_found", "Session not found", status_code=404)
|
||||
if current_session["revision"] != expected_session_revision:
|
||||
raise FSMError(
|
||||
"revision_conflict",
|
||||
"Conversation changed while processing the component; retry with the latest version",
|
||||
status_code=409,
|
||||
)
|
||||
current_block = self.database.fetch_block(connection, session_id, request.component_id)
|
||||
if current_block is None:
|
||||
raise FSMError("component_not_found", "Component not found", status_code=404)
|
||||
if current_block["type"] != "component" or current_block["lifecycle"] != "active":
|
||||
raise FSMError("component_not_active", "Component was already handled")
|
||||
if current_block["version"] != expected_block_version:
|
||||
raise FSMError(
|
||||
"revision_conflict",
|
||||
"Component changed while processing; retry with the latest version",
|
||||
status_code=409,
|
||||
)
|
||||
current_resume = self.database.fetch_resume(connection, session_id, for_update=True)
|
||||
if expected_resume_revision is None:
|
||||
if current_resume is not None:
|
||||
raise FSMError(
|
||||
"revision_conflict",
|
||||
"Resume changed while processing the component; retry with the latest version",
|
||||
status_code=409,
|
||||
)
|
||||
elif current_resume is None or current_resume["revision"] != expected_resume_revision:
|
||||
raise FSMError(
|
||||
"revision_conflict",
|
||||
"Resume changed while processing the component; retry with the latest version",
|
||||
status_code=409,
|
||||
)
|
||||
try:
|
||||
self.database.update_block(
|
||||
connection,
|
||||
block["id"],
|
||||
lifecycle=transition.lifecycle,
|
||||
expected_version=expected_block_version,
|
||||
)
|
||||
except SessionRevisionConflict as exc:
|
||||
raise FSMError(
|
||||
"revision_conflict",
|
||||
"Component changed while processing; retry with the latest version",
|
||||
status_code=409,
|
||||
) from exc
|
||||
if resume_content is not None:
|
||||
assert resume is not None
|
||||
resume = self.database.update_resume(connection, session_id, resume_content)
|
||||
if current_resume is None:
|
||||
raise FSMError("resume_not_created", "Create the resume before confirming content")
|
||||
try:
|
||||
resume = self.database.update_resume(
|
||||
connection,
|
||||
session_id,
|
||||
resume_content,
|
||||
expected_revision=expected_resume_revision,
|
||||
)
|
||||
except SessionRevisionConflict as exc:
|
||||
raise FSMError(
|
||||
"revision_conflict",
|
||||
"Resume changed while processing the component; retry with the latest version",
|
||||
status_code=409,
|
||||
) from exc
|
||||
if transition.stage == Stage.BUILDER_CONVERSATION:
|
||||
builder_conversation.reconcile_last_confirmed_entry(
|
||||
transition.profile, resume["content"]
|
||||
@@ -1054,13 +1113,21 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
},
|
||||
},
|
||||
)
|
||||
updated = self.database.update_session(
|
||||
connection,
|
||||
session_id,
|
||||
stage=transition.stage,
|
||||
profile=transition.profile,
|
||||
draft_id=draft_id,
|
||||
)
|
||||
try:
|
||||
updated = self.database.update_session(
|
||||
connection,
|
||||
session_id,
|
||||
stage=transition.stage,
|
||||
profile=transition.profile,
|
||||
draft_id=draft_id,
|
||||
expected_revision=expected_session_revision,
|
||||
)
|
||||
except SessionRevisionConflict as exc:
|
||||
raise FSMError(
|
||||
"revision_conflict",
|
||||
"Conversation changed while processing the component; retry with the latest version",
|
||||
status_code=409,
|
||||
) from exc
|
||||
turn_id = self.database.insert_turn(
|
||||
connection,
|
||||
session_id=session_id,
|
||||
@@ -1074,7 +1141,7 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
return response
|
||||
|
||||
def add_message(self, session_id: str, request: MessageRequest) -> ActionResponse:
|
||||
with self.database.transaction(immediate=True) as connection:
|
||||
with self.database.transaction() as connection:
|
||||
session = self.database.fetch_session(connection, session_id)
|
||||
if session is None:
|
||||
raise FSMError("session_not_found", "Session not found", status_code=404)
|
||||
@@ -1088,6 +1155,36 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
resume = self.database.fetch_resume(connection, session_id)
|
||||
if resume is None:
|
||||
raise FSMError("resume_not_created", "Create the resume before using Builder chat")
|
||||
expected_session_revision = session["revision"]
|
||||
expected_resume_revision = resume["revision"]
|
||||
transition = builder_conversation.process_message(
|
||||
self, session["profile"], request.content, resume["content"]
|
||||
)
|
||||
|
||||
with self.database.transaction(immediate=True) as connection:
|
||||
current_resume = self.database.fetch_resume(connection, session_id, for_update=True)
|
||||
if current_resume is None:
|
||||
raise FSMError("resume_not_created", "Create the resume before using Builder chat")
|
||||
if current_resume["revision"] != expected_resume_revision:
|
||||
raise FSMError(
|
||||
"revision_conflict",
|
||||
"Resume changed while the message was being processed; retry with the latest version",
|
||||
status_code=409,
|
||||
)
|
||||
try:
|
||||
updated = self.database.update_session(
|
||||
connection,
|
||||
session_id,
|
||||
stage=transition.stage,
|
||||
profile=transition.profile,
|
||||
expected_revision=expected_session_revision,
|
||||
)
|
||||
except SessionRevisionConflict as exc:
|
||||
raise FSMError(
|
||||
"revision_conflict",
|
||||
"Conversation changed while the message was being processed; retry with the latest version",
|
||||
status_code=409,
|
||||
) from exc
|
||||
self.database.insert_turn(
|
||||
connection,
|
||||
session_id=session_id,
|
||||
@@ -1096,20 +1193,14 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
composer_mode=ComposerMode.CHAT,
|
||||
blocks=[{"type": "text", "lifecycle": "submitted", "data": {"text": request.content}}],
|
||||
)
|
||||
transition = builder_conversation.process_message(self, session["profile"], request.content, resume["content"])
|
||||
self.database.supersede_active_components(connection, session_id)
|
||||
updated = self.database.update_session(
|
||||
connection,
|
||||
session_id,
|
||||
stage=transition.stage,
|
||||
profile=transition.profile,
|
||||
)
|
||||
turn_id = self.database.insert_turn(connection, session_id=session_id, **transition.turn)
|
||||
response = self._action_response(updated, self.database.get_turn(turn_id))
|
||||
response.builder_stream_phases = list(
|
||||
((transition.profile.get("builder") or {}).get("last_stream_phases") or [])
|
||||
)
|
||||
return response
|
||||
|
||||
def _polish_module_entry(self, transition: Any) -> None:
|
||||
"""Generate a proposal without mutating the user's original description."""
|
||||
draft = (transition.profile.get("enrichment") or {}).get("module_draft") or {}
|
||||
@@ -1260,7 +1351,7 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
def _create_resume_transaction(
|
||||
self, session_id: str, request: CreateResumeRequest
|
||||
) -> CreateResumeResponse:
|
||||
with self.database.transaction(immediate=True) as connection:
|
||||
with self.database.transaction() as connection:
|
||||
session = self.database.fetch_session(connection, session_id)
|
||||
if session is None:
|
||||
raise FSMError("session_not_found", "Session not found", status_code=404)
|
||||
@@ -1280,24 +1371,23 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
"The first-anchor gate is not satisfied",
|
||||
missing_fields=missing_fields(session["profile"]),
|
||||
)
|
||||
creating = self.database.update_session(
|
||||
connection,
|
||||
session_id,
|
||||
stage=Stage.RESUME_CREATING,
|
||||
profile=session["profile"],
|
||||
)
|
||||
self.database.supersede_active_components(connection, session_id)
|
||||
creating_status = component("CreatingStatusCard", status="creating")
|
||||
creating_status["lifecycle"] = "submitted"
|
||||
self.database.insert_turn(
|
||||
connection,
|
||||
session_id=session_id,
|
||||
**assistant_turn(
|
||||
"Creating your resume.",
|
||||
[creating_status],
|
||||
),
|
||||
)
|
||||
content = merge_ids(None, self.rewriter.rewrite(creating["profile"]))
|
||||
expected_revision = session["revision"]
|
||||
source_profile = deepcopy(session["profile"])
|
||||
content = merge_ids(None, self.rewriter.rewrite(source_profile))
|
||||
with self.database.transaction(immediate=True) as connection:
|
||||
current = self.database.fetch_session(connection, session_id)
|
||||
if current is None:
|
||||
raise FSMError("session_not_found", "Session not found", status_code=404)
|
||||
existing = self.database.fetch_resume(connection, session_id)
|
||||
if existing is not None:
|
||||
turn = self._last_turn(session_id)
|
||||
return self._create_response(current, existing, turn, created=False)
|
||||
if current["revision"] != expected_revision:
|
||||
raise FSMError(
|
||||
"revision_conflict",
|
||||
"Conversation changed while resume generation was running; retry with the latest version",
|
||||
status_code=409,
|
||||
)
|
||||
resume_id = f"resume_{uuid4().hex}"
|
||||
resume = self.database.insert_resume(
|
||||
connection,
|
||||
@@ -1307,7 +1397,7 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
content=content,
|
||||
)
|
||||
profile, ready_turn = builder_conversation.welcome_turn(
|
||||
deepcopy(creating["profile"]), resume_id, resume_content=content
|
||||
deepcopy(current["profile"]), resume_id, resume_content=content
|
||||
)
|
||||
updated = self.database.update_session(
|
||||
connection,
|
||||
@@ -1315,7 +1405,9 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
stage=Stage.BUILDER_CONVERSATION,
|
||||
profile=profile,
|
||||
resume_id=resume_id,
|
||||
expected_revision=expected_revision,
|
||||
)
|
||||
self.database.supersede_active_components(connection, session_id)
|
||||
ready_turn["blocks"].insert(
|
||||
1,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user