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

170 lines
7.7 KiB
Markdown

# 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.
```powershell
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:
```json
{
"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:
```text
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:
```dotenv
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:
```python
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:
```dotenv
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
```powershell
pytest -q
```
To verify only imports and configuration after installation:
```powershell
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.