Files
resume-agent/backend/README.md
T
2026-07-20 14:48:41 +08:00

7.7 KiB

Resume Agent MVP backend

An intentionally small FastAPI service for building a first usable resume through an explicit finite-state machine. State is persisted in SQLite; there is no LangChain or LangGraph dependency.

Run locally

Python 3.11 or newer is required.

cd F:\offerpai_web\resume-agent-mvp\backend
python -m pip install -r requirements.txt
Copy-Item .env.example .env
# Leave OPENAI_API_KEY empty for offline rules, or configure the live LLM values below.
python -m uvicorn app.main:app --reload --port 8000

The default database is data/resume_agent.db. Override it with RESUME_AGENT_DATABASE. CORS defaults to http://localhost:5173 and http://127.0.0.1:5173; set a comma-separated RESUME_AGENT_CORS_ORIGINS to change it.

OpenAPI is available at http://localhost:8000/docs and the health check at GET /health.

Public API

All workflow routes use /ai-api/resume-agent:

Method Path Purpose
POST /sessions Start a session; body is optional and may contain account_phone and metadata
GET /sessions/{session_id}/timeline Return the session and ordered conversation turns
POST /sessions/{session_id}/component-events Apply an event to one active component
POST /sessions/{session_id}/messages Describe the first anchor or add enrichment text
POST /sessions/{session_id}/create Idempotently create the business resume
DELETE /sessions/{session_id} Delete a session and its related data

Component events have one uniform shape:

{
  "component_id": "block_...",
  "event": "submit",
  "payload": {"field": "school", "value": "示例大学"}
}

Canonical actions are accept_privacy, decline_privacy, use_account_phone, use_other_phone, submit_manual_phone, submit_name, select_job_type, select_anchor_type, submit_field, submit_date_range, select_choice, confirm_anchor, edit_anchor, continue_enriching, and finish_enrichment. Generic UI actions (accept, consent, select, submit, confirm, edit) are normalized according to the active component.

POST /create accepts an optional idempotency_key. Creation is idempotent by session, so retries return the existing resume_id with created: false, even if a different key is sent.

Workflow and gates

The core order is:

PRIVACY_CONSENT
  -> PHONE_SELECTION -> MANUAL_PHONE_INPUT (only when selected)
  -> NAME_CAPTURE -> JOB_TYPE_SELECT
  -> ANCHOR_TYPE_SELECT (only for other/fallback)
  -> ANCHOR_COLLECTING -> ANCHOR_CONFIRM -> MINIMUM_READY
  -> RESUME_CREATING -> RESUME_ENRICHING -> CONTENT_READY

The first anchor starts with an open chat prompt. Explicit facts are extracted from the user's description, and only the remaining structural gaps are rendered as inline components. CONTENT_DISAMBIGUATION asks for more detail when an enrichment message is too vague. CREATE_FAILED exposes a retry card if the replaceable writer fails.

After the business resume exists, an AI rewrite is held as a proposed patch. The user must confirm the ExperienceConfirmCard before the resume revision is updated and formal_content_ready becomes true.

First-anchor gates are exact:

  • Education: school, major, degree, start_date, end_date_or_present
  • Work or internship: company, position, start_date, end_date_or_present
  • Project: project_name, project_role, start_date, end_date_or_present

Campus recruitment selects education automatically; social recruitment selects work experience; other asks the user to choose education, work, internship, or project.

Manual phones must exactly match ^1[3-9]\d{9}$. Responses expose only masked_phone and phone_source; the raw account/manual phone is not included in the timeline, blocks, draft, or resume response.

Conversation protocol

Every ConversationTurn contains ordered ComponentBlock objects. Block type is one of text, component, resume_patch, status, or error. Interactive blocks carry both:

  • data.component: stable full snake_case name such as privacy_consent_card
  • data.component_name: canonical UI name such as PrivacyConsentCard

A handled block remains in the timeline with a read-only lifecycle such as submitted or confirmed. New events are accepted only for the current active block, preventing duplicate or stale transitions.

OpenAI-compatible LLM

ExperienceExtractor and ResumeRewriter remain vendor-neutral protocols. When OPENAI_API_KEY is non-empty, the default application uses the official OpenAI Python SDK against the configured compatible endpoint:

RESUME_AGENT_LLM_PROVIDER=auto
OPENAI_API_KEY=your-key
OPENAI_BASE_URL=https://re.94xy.cn
OPENAI_MODEL=your-gateway-model-id

The adapter is implemented in app/llm_services.py with the same SDK shape as:

from openai import OpenAI

client = OpenAI(
    api_key=settings.openai_api_key,
    base_url=settings.openai_base_url,
    timeout=settings.openai_timeout_seconds,
    max_retries=settings.openai_max_retries,
)
response = client.chat.completions.create(
    model=settings.openai_model,
    messages=messages,
    response_format=response_format,
)

The configured base URL is passed directly to the SDK. Do not add /chat/completions; add /v1 only if the gateway's documentation requires it.

The adapter calls chat.completions.create with JSON Schema structured output and then validates every response with Pydantic. Set OPENAI_STRUCTURED_OUTPUT_MODE=json_object only when a compatible gateway does not support json_schema. SDK transport behavior is controlled by OPENAI_TIMEOUT_SECONDS and OPENAI_MAX_RETRIES; malformed structured responses use OPENAI_STRUCTURED_OUTPUT_RETRIES.

RESUME_AGENT_LLM_PROVIDER=rule forces deterministic local extraction for tests or offline development. With RESUME_AGENT_LLM_FALLBACK_TO_RULES=true, an unavailable or invalid model response falls back to those deterministic services. Set it to false when upstream failures should surface as workflow errors.

For a real SDK smoke test, use:

RESUME_AGENT_LLM_PROVIDER=openai
RESUME_AGENT_LLM_FALLBACK_TO_RULES=false

This prevents the rule fallback from making a failed gateway call look successful. Automated tests inject fake clients, so pytest does not send requests or consume model quota.

Then execute python scripts\smoke_llm.py. It makes one extraction request with the configured SDK client, disables rule fallback for that request, and never prints the key.

The LLM receives only an allow-listed facts DTO. Account/manual phone numbers, account_phone, session metadata, the user's name, and raw internal profile state are never sent to the model. Phone-like strings, email addresses, and labeled WeChat IDs typed into free text are redacted again at the final SDK boundary. The model cannot select a Stage, component, gate, or database action.

For dependency injection tests, pass settings= and openai_client= to create_app, or pass explicit extractor= / rewriter= implementations. The FSM and API contract do not depend on the model vendor.

Test

pytest -q

To verify only imports and configuration after installation:

python -c "from openai import OpenAI; from app.main import app; print(app.title)"

If the compatible gateway rejects response_format.type=json_schema, change OPENAI_STRUCTURED_OUTPUT_MODE=json_object. If it returns a model-not-found error, replace OPENAI_MODEL with the exact model ID supported by that gateway.

Tests cover the full campus flow, social/other anchor gates, strict and private phone handling, component retries/lifecycle, idempotent resume creation, enrichment/disambiguation, CORS, and deletion.