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 asprivacy_consent_carddata.component_name: canonical UI name such asPrivacyConsentCard
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.