generated from kgod/ai-review-template
170 lines
7.7 KiB
Markdown
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.
|