generated from kgod/ai-review-template
253 lines
11 KiB
Python
253 lines
11 KiB
Python
"""HTTP routes for reviewable resume imports."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
from typing import Any, Callable
|
|
from uuid import uuid4
|
|
|
|
from fastapi import FastAPI, File, Header, UploadFile, status
|
|
|
|
from .fsm import FSMError
|
|
from .models import ActionResponse, Stage
|
|
from .resume_document import merge_ids
|
|
from .resume_import_models import ApplyResumeImportRequest, ResumeImportView
|
|
from .resume_import_service import ResumeImportService
|
|
from .validators import mask_phone
|
|
from . import builder_conversation
|
|
|
|
|
|
def register_resume_import_routes(
|
|
application: FastAPI,
|
|
agent: Any,
|
|
service: ResumeImportService,
|
|
prefix: str,
|
|
*,
|
|
authorize_session_request: Callable[[str, str | None], str | None] | None = None,
|
|
) -> None:
|
|
authorize_session_request = authorize_session_request or (
|
|
lambda _session_id, _authorization: None
|
|
)
|
|
@application.post(
|
|
f"{prefix}/sessions/{{session_id}}/resume-imports",
|
|
response_model=ResumeImportView,
|
|
status_code=status.HTTP_201_CREATED,
|
|
tags=["resume-agent"],
|
|
)
|
|
async def create_resume_import(
|
|
session_id: str,
|
|
file: UploadFile = File(...),
|
|
authorization: str | None = Header(default=None),
|
|
) -> ResumeImportView:
|
|
authorize_session_request(session_id, authorization)
|
|
_require_import_path(agent, session_id)
|
|
with agent.database.transaction() as connection:
|
|
if agent.database.fetch_resume(connection, session_id) is not None:
|
|
raise FSMError(
|
|
"resume_import_not_allowed",
|
|
"当前简历预览已有内容,重新开始后才能导入新的简历。",
|
|
)
|
|
content = await file.read()
|
|
try:
|
|
prepared = service.prepare(
|
|
file_name=file.filename or "upload",
|
|
declared_mime=file.content_type,
|
|
content=content,
|
|
)
|
|
except ValueError as exc:
|
|
raise FSMError(str(exc), "Resume import could not be processed", status_code=422) from exc
|
|
with agent.database.transaction(immediate=True) as connection:
|
|
session = agent.database.fetch_session(connection, session_id)
|
|
if session is None:
|
|
service.remove(prepared["object_key"])
|
|
raise FSMError("session_not_found", "Session not found", status_code=404)
|
|
_require_import_profile(session["profile"])
|
|
if agent.database.fetch_resume(connection, session_id) is not None:
|
|
service.remove(prepared["object_key"])
|
|
raise FSMError(
|
|
"resume_import_not_allowed",
|
|
"当前简历预览已有内容,重新开始后才能导入新的简历。",
|
|
)
|
|
existing = agent.database.find_resume_import_by_sha256(connection, session_id, prepared["sha256"])
|
|
if existing is not None:
|
|
service.remove(prepared["object_key"])
|
|
record = existing
|
|
else:
|
|
record = agent.database.create_resume_import(
|
|
connection,
|
|
import_id=f"import_{uuid4().hex}",
|
|
session_id=session_id,
|
|
**prepared,
|
|
)
|
|
return ResumeImportView.model_validate(record)
|
|
|
|
@application.get(
|
|
f"{prefix}/sessions/{{session_id}}/resume-imports/{{import_id}}",
|
|
response_model=ResumeImportView,
|
|
tags=["resume-agent"],
|
|
)
|
|
def get_resume_import(
|
|
session_id: str,
|
|
import_id: str,
|
|
authorization: str | None = Header(default=None),
|
|
) -> ResumeImportView:
|
|
authorize_session_request(session_id, authorization)
|
|
with agent.database.transaction() as connection:
|
|
record = agent.database.fetch_resume_import(connection, session_id, import_id)
|
|
if record is None:
|
|
raise FSMError("resume_import_not_found", "Resume import not found", status_code=404)
|
|
return ResumeImportView.model_validate(record)
|
|
|
|
@application.post(
|
|
f"{prefix}/sessions/{{session_id}}/resume-imports/{{import_id}}/apply",
|
|
response_model=ActionResponse,
|
|
tags=["resume-agent"],
|
|
)
|
|
def apply_resume_import(
|
|
session_id: str,
|
|
import_id: str,
|
|
request: ApplyResumeImportRequest,
|
|
authorization: str | None = Header(default=None),
|
|
) -> ActionResponse:
|
|
external_token = authorize_session_request(session_id, authorization)
|
|
|
|
def mutate() -> ActionResponse:
|
|
with agent.database.transaction(immediate=True) as connection:
|
|
session = agent.database.fetch_session(connection, session_id)
|
|
if session is None:
|
|
raise FSMError("session_not_found", "Session not found", status_code=404)
|
|
_require_import_profile(session["profile"])
|
|
record = agent.database.fetch_resume_import(connection, session_id, import_id)
|
|
if record is None:
|
|
raise FSMError("resume_import_not_found", "Resume import not found", status_code=404)
|
|
if record["status"] != "awaiting_review":
|
|
raise FSMError("resume_import_not_applicable", "Resume import is not awaiting review")
|
|
if agent.database.fetch_resume(connection, session_id) is not None:
|
|
raise FSMError(
|
|
"resume_import_not_allowed",
|
|
"当前简历预览已有内容,重新开始后才能导入新的简历。",
|
|
)
|
|
if request.expected_revision != 0:
|
|
raise FSMError("revision_conflict", "Resume was modified; refresh before importing", status_code=409)
|
|
resume_id = f"resume_{uuid4().hex}"
|
|
imported_document = record["document"]
|
|
persisted_document = deepcopy(imported_document)
|
|
persisted_basics = persisted_document.get("basics")
|
|
if isinstance(persisted_basics, dict):
|
|
raw_phone = persisted_basics.pop("phone", None)
|
|
masked_phone = mask_phone(raw_phone)
|
|
if masked_phone:
|
|
persisted_basics["masked_phone"] = masked_phone
|
|
agent.database.insert_resume(
|
|
connection,
|
|
resume_id=resume_id,
|
|
session_id=session_id,
|
|
idempotency_key=None,
|
|
content=merge_ids(None, persisted_document),
|
|
)
|
|
profile, welcome_turn = builder_conversation.welcome_turn(
|
|
_profile_for_imported_resume(session["profile"], imported_document),
|
|
resume_id,
|
|
imported=True,
|
|
)
|
|
session = agent.database.update_session(
|
|
connection,
|
|
session_id,
|
|
stage=Stage.BUILDER_CONVERSATION,
|
|
profile=profile,
|
|
resume_id=resume_id,
|
|
)
|
|
agent.database.supersede_active_components(connection, session_id)
|
|
turn_id = agent.database.insert_turn(
|
|
connection,
|
|
session_id=session_id,
|
|
**welcome_turn,
|
|
)
|
|
agent.database.update_resume_import_status(connection, session_id, import_id, "applied")
|
|
return agent._action_response(session, agent.database.get_turn(turn_id))
|
|
|
|
if external_token is None:
|
|
return mutate()
|
|
return agent.mutate_with_offerpai_sync(
|
|
session_id,
|
|
external_token,
|
|
mutate,
|
|
)
|
|
|
|
@application.delete(
|
|
f"{prefix}/sessions/{{session_id}}/resume-imports/{{import_id}}",
|
|
response_model=ResumeImportView,
|
|
tags=["resume-agent"],
|
|
)
|
|
def cancel_resume_import(
|
|
session_id: str,
|
|
import_id: str,
|
|
authorization: str | None = Header(default=None),
|
|
) -> ResumeImportView:
|
|
authorize_session_request(session_id, authorization)
|
|
with agent.database.transaction(immediate=True) as connection:
|
|
record = agent.database.fetch_resume_import(connection, session_id, import_id)
|
|
if record is None:
|
|
raise FSMError("resume_import_not_found", "Resume import not found", status_code=404)
|
|
if record["status"] == "applied":
|
|
raise FSMError("resume_import_not_cancellable", "Applied resume imports cannot be cancelled")
|
|
updated = agent.database.update_resume_import_status(connection, session_id, import_id, "cancelled")
|
|
service.remove(record.get("object_key"))
|
|
return ResumeImportView.model_validate(updated)
|
|
|
|
|
|
def _profile_for_imported_resume(profile: dict[str, Any], document: dict[str, Any]) -> dict[str, Any]:
|
|
"""Retain the session contract so the final supplement card can continue safely."""
|
|
updated = dict(profile)
|
|
basics = document.get("basics") if isinstance(document.get("basics"), dict) else {}
|
|
target = document.get("target") if isinstance(document.get("target"), dict) else {}
|
|
for field in ("name", "phone", "email", "city", "portfolio_url"):
|
|
if basics.get(field):
|
|
updated[field] = basics[field]
|
|
if basics.get("phone"):
|
|
updated["phone_source"] = "imported_resume"
|
|
if target.get("position"):
|
|
updated["target_position"] = target["position"]
|
|
job_type = _normalize_job_type(target.get("job_type"))
|
|
if job_type:
|
|
updated["job_type"] = job_type
|
|
updated.setdefault("records", {})
|
|
updated.setdefault("tags", {"skills": [], "certificates": []})
|
|
updated.setdefault("experiences", [])
|
|
updated["builder"] = {
|
|
"active_section": None,
|
|
"identity_draft": {},
|
|
"pending_entry": None,
|
|
"imported": True,
|
|
}
|
|
updated["imported_resume"] = True
|
|
return updated
|
|
|
|
|
|
def _normalize_job_type(value: Any) -> str | None:
|
|
normalized = str(value or "").strip().lower()
|
|
return {
|
|
"campus": "campus", "校招": "campus", "校园招聘": "campus",
|
|
"social": "social", "社招": "social", "社会招聘": "social",
|
|
"internship": "internship", "实习": "internship", "实习招聘": "internship",
|
|
}.get(normalized)
|
|
|
|
|
|
def _require_session(agent: Any, session_id: str) -> None:
|
|
if agent.database.get_session(session_id) is None:
|
|
raise FSMError("session_not_found", "Session not found", status_code=404)
|
|
|
|
|
|
def _require_import_path(agent: Any, session_id: str) -> None:
|
|
session = agent.database.get_session(session_id)
|
|
if session is None:
|
|
raise FSMError("session_not_found", "Session not found", status_code=404)
|
|
_require_import_profile(session["profile"])
|
|
|
|
|
|
def _require_import_profile(profile: dict[str, Any]) -> None:
|
|
if not profile.get("privacy_accepted"):
|
|
raise FSMError("privacy_consent_required", "Privacy consent is required before importing", status_code=409)
|
|
if profile.get("resume_source") != "import":
|
|
raise FSMError("resume_import_not_selected", "Select resume import before uploading", status_code=409)
|