generated from kgod/ai-review-template
feat: start resume sessions in new flow
This commit is contained in:
+27
-1
@@ -17,6 +17,7 @@ from .fsm import (
|
||||
gate_allowed,
|
||||
initial_turn,
|
||||
missing_fields,
|
||||
new_resume_transition,
|
||||
process_component_event,
|
||||
required_fields,
|
||||
)
|
||||
@@ -867,7 +868,7 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
return self.timeline(session_id)
|
||||
|
||||
def timeline(self, session_id: str) -> TimelineResponse:
|
||||
session = self._require_session(session_id)
|
||||
session = self._advance_legacy_resume_source_stage(session_id)
|
||||
turns = self.database.list_turns(session_id)
|
||||
gate = self._gate(session)
|
||||
resume = self._resume_view(session)
|
||||
@@ -885,6 +886,31 @@ class ResumeAgent(ResumeEditingMixin, OptimizationFlowMixin):
|
||||
trace_id=self._trace_id(),
|
||||
)
|
||||
|
||||
def _advance_legacy_resume_source_stage(self, session_id: str) -> dict[str, Any]:
|
||||
"""Move sessions parked on removed source/import screens into new-resume setup."""
|
||||
session = self._require_session(session_id)
|
||||
removed_stages = {Stage.RESUME_SOURCE_SELECT, Stage.RESUME_IMPORT_UPLOAD}
|
||||
if Stage(session["stage"]) not in removed_stages or session.get("resume_id"):
|
||||
return session
|
||||
|
||||
with self.database.transaction(immediate=True) as connection:
|
||||
current = self.database.fetch_session(connection, session_id, for_update=True)
|
||||
if current is None:
|
||||
raise FSMError("session_not_found", "Session not found", status_code=404)
|
||||
if Stage(current["stage"]) not in removed_stages or current.get("resume_id"):
|
||||
return current
|
||||
|
||||
transition = new_resume_transition(current["profile"])
|
||||
self.database.supersede_active_components(connection, session_id)
|
||||
updated = self.database.update_session(
|
||||
connection,
|
||||
session_id,
|
||||
stage=transition.stage,
|
||||
profile=transition.profile,
|
||||
)
|
||||
self.database.insert_turn(connection, session_id=session_id, **transition.turn)
|
||||
return updated
|
||||
|
||||
def component_event(
|
||||
self, session_id: str, request: ComponentEventRequest
|
||||
) -> ActionResponse:
|
||||
|
||||
@@ -160,8 +160,13 @@ class Database:
|
||||
self.insert_turn(connection, session_id=session_id, **initial_turn)
|
||||
|
||||
def fetch_session(
|
||||
self, connection: sqlite3.Connection, session_id: str
|
||||
self,
|
||||
connection: sqlite3.Connection,
|
||||
session_id: str,
|
||||
*,
|
||||
for_update: bool = False,
|
||||
) -> dict[str, Any] | None:
|
||||
del for_update
|
||||
row = connection.execute(
|
||||
"SELECT * FROM sessions WHERE id = ?", (session_id,)
|
||||
).fetchone()
|
||||
|
||||
+28
-43
@@ -213,6 +213,27 @@ def initial_turn() -> dict[str, Any]:
|
||||
)
|
||||
|
||||
|
||||
def new_resume_transition(profile: dict[str, Any]) -> Transition:
|
||||
"""Enter the new-resume flow without presenting an import/manual choice."""
|
||||
updated = deepcopy(profile)
|
||||
updated["resume_source"] = "manual"
|
||||
return Transition(
|
||||
Stage.PHONE_SELECTION,
|
||||
updated,
|
||||
assistant_turn(
|
||||
"请选择手机号来源。",
|
||||
[
|
||||
component(
|
||||
"ResumePhoneSelector",
|
||||
has_account_phone=bool(updated.get("account_phone")),
|
||||
masked_phone=mask_phone(updated.get("account_phone")),
|
||||
default_value=("account" if updated.get("account_phone") else None),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def required_fields(profile: dict[str, Any]) -> list[str]:
|
||||
"""The initial Builder resume only requires verified setup information."""
|
||||
return []
|
||||
@@ -267,56 +288,20 @@ def process_component_event(
|
||||
)
|
||||
_expect(action, "accept_privacy")
|
||||
updated["privacy_accepted"] = True
|
||||
return Transition(
|
||||
Stage.RESUME_SOURCE_SELECT,
|
||||
updated,
|
||||
assistant_turn(
|
||||
"\u8bf7\u9009\u62e9\u5f00\u59cb\u65b9\u5f0f\u3002",
|
||||
[
|
||||
component(
|
||||
"ChoiceChips",
|
||||
eyebrow="\u5f00\u59cb\u521b\u5efa",
|
||||
title="\u9009\u62e9\u521b\u5efa\u65b9\u5f0f",
|
||||
description="\u5bfc\u5165\u4f1a\u5148\u63d0\u53d6\u6587\u6863\u5185\u5bb9\uff0c\u518d\u6620\u5c04\u4e3a\u53ef\u7f16\u8f91\u7684\u7b80\u5386\u7ed3\u6784\u3002",
|
||||
options=[
|
||||
{"value": "import", "label": "\u5bfc\u5165\u5df2\u6709\u7b80\u5386", "description": "\u652f\u6301 PDF \u6216 DOCX"},
|
||||
{"value": "manual", "label": "\u521b\u5efa\u65b0\u7b80\u5386", "description": "\u4ece\u57fa\u7840\u4fe1\u606f\u548c\u7ecf\u5386\u5f00\u59cb\u586b\u5199"},
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
return new_resume_transition(updated)
|
||||
|
||||
if stage == Stage.RESUME_SOURCE_SELECT:
|
||||
_expect(action, "select_choice")
|
||||
source = str(payload.get("value") or "").strip()
|
||||
if source == "import":
|
||||
updated["resume_source"] = "import"
|
||||
return Transition(
|
||||
Stage.RESUME_IMPORT_UPLOAD,
|
||||
updated,
|
||||
assistant_turn("\u8bf7\u9009\u62e9\u9700\u8981\u5bfc\u5165\u7684 PDF \u6216 DOCX \u7b80\u5386\u3002", []),
|
||||
raise FSMError(
|
||||
"resume_import_disabled",
|
||||
"Resume import is no longer available; create a new resume instead",
|
||||
status_code=410,
|
||||
)
|
||||
if source == "manual":
|
||||
updated["resume_source"] = "manual"
|
||||
return Transition(
|
||||
Stage.PHONE_SELECTION,
|
||||
updated,
|
||||
assistant_turn(
|
||||
"\u8bf7\u9009\u62e9\u624b\u673a\u53f7\u6765\u6e90\u3002",
|
||||
[
|
||||
component(
|
||||
"ResumePhoneSelector",
|
||||
has_account_phone=bool(updated.get("account_phone")),
|
||||
masked_phone=mask_phone(updated.get("account_phone")),
|
||||
default_value=(
|
||||
"account" if updated.get("account_phone") else None
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
raise FSMError("invalid_resume_source", "Select import or manual", status_code=422)
|
||||
return new_resume_transition(updated)
|
||||
raise FSMError("invalid_resume_source", "Create a new resume", status_code=422)
|
||||
if stage == Stage.PHONE_SELECTION:
|
||||
if action == "use_other_phone":
|
||||
return Transition(
|
||||
|
||||
+1
-26
@@ -21,7 +21,7 @@ from .database import Database
|
||||
from .postgres_database import PostgresDatabase
|
||||
from .fsm import FSMError
|
||||
from .builder_sse import stream_builder_message
|
||||
from .llm_services import OpenAICompatibleStructuredClient, build_services
|
||||
from .llm_services import build_services
|
||||
from .models import (
|
||||
ActionResponse,
|
||||
ComponentEventRequest,
|
||||
@@ -34,9 +34,6 @@ from .models import (
|
||||
TimelineResponse,
|
||||
)
|
||||
from .resume_routes import register_resume_routes
|
||||
from .resume_import_routes import register_resume_import_routes
|
||||
from .resume_import_service import ResumeImportService, RuleBasedResumeImportParser
|
||||
from .import_parser import OpenAIResumeImportParser
|
||||
from .rate_limit import SlidingWindowRateLimiter
|
||||
from .services import (
|
||||
EntryExpander,
|
||||
@@ -77,7 +74,6 @@ def create_app(
|
||||
cors_origins: list[str] | None = None,
|
||||
settings: Settings | None = None,
|
||||
openai_client: Any | None = None,
|
||||
resume_import_service: ResumeImportService | None = None,
|
||||
profile_summary_generator: ProfileSummaryGenerator | None = None,
|
||||
offerpai_identity_provider: OfferPaiIdentityProvider | None = None,
|
||||
offerpai_resume_provider: OfferPaiResumeProvider | None = None,
|
||||
@@ -156,19 +152,6 @@ def create_app(
|
||||
application.state.resume_agent = agent
|
||||
application.state.offerpai_identity_provider = offerpai_identity_provider
|
||||
application.state.offerpai_resume_provider = offerpai_resume_provider
|
||||
if resume_import_service is None:
|
||||
import_fallback = RuleBasedResumeImportParser()
|
||||
import_parser = import_fallback
|
||||
if resolved_settings.use_openai:
|
||||
import_parser = OpenAIResumeImportParser(
|
||||
completion=OpenAICompatibleStructuredClient(resolved_settings, openai_client),
|
||||
fallback=import_fallback,
|
||||
)
|
||||
resume_import_service = ResumeImportService(
|
||||
storage_root=Path(__file__).resolve().parent.parent / "data" / "resume_imports",
|
||||
parser=import_parser,
|
||||
)
|
||||
application.state.resume_import_service = resume_import_service
|
||||
application.state.light_opt_limiter = SlidingWindowRateLimiter(
|
||||
limit=resolved_settings.light_opt_rate_limit,
|
||||
window_seconds=resolved_settings.light_opt_rate_window_seconds,
|
||||
@@ -366,14 +349,6 @@ def create_app(
|
||||
API_PREFIX,
|
||||
authorize_session_request=authorize_session_request,
|
||||
)
|
||||
register_resume_import_routes(
|
||||
application,
|
||||
agent,
|
||||
application.state.resume_import_service,
|
||||
API_PREFIX,
|
||||
authorize_session_request=authorize_session_request,
|
||||
)
|
||||
|
||||
@application.delete(
|
||||
f"{API_PREFIX}/sessions/{{session_id}}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
|
||||
@@ -46,9 +46,18 @@ class PostgresDatabase:
|
||||
))
|
||||
self.insert_turn(connection, session_id=session_id, **initial_turn)
|
||||
|
||||
def fetch_session(self, connection: Connection, session_id: str) -> dict[str, Any] | None:
|
||||
def fetch_session(
|
||||
self,
|
||||
connection: Connection,
|
||||
session_id: str,
|
||||
*,
|
||||
for_update: bool = False,
|
||||
) -> dict[str, Any] | None:
|
||||
sessions = self.tables["sessions"]
|
||||
row = connection.execute(select(sessions).where(sessions.c.id == session_id)).mappings().first()
|
||||
statement = select(sessions).where(sessions.c.id == session_id)
|
||||
if for_update:
|
||||
statement = statement.with_for_update()
|
||||
row = connection.execute(statement).mappings().first()
|
||||
if row is None:
|
||||
return None
|
||||
result = dict(row)
|
||||
|
||||
Reference in New Issue
Block a user