# ALAS Backend API FastAPI backend for the ALAS workplace soft-skills coaching app (`main.py`). It drives an LLM-played character through a scripted scenario, extracts live coaching notes as the conversation progresses, and produces a rubric-based assessment on demand. - **Base URL (local):** `http://localhost:8000` - **Run:** `uv run uvicorn main:app --reload --port 8000` - **Interactive docs:** `/docs` (Swagger UI) and `/redoc`, auto-generated by FastAPI - **Content type:** `application/json` for all requests/responses except `/chats/{chat_id}/stream`, which returns `text/event-stream` ## Session model State is kept **in memory** (`store: dict[chat_id -> session]`) — there is no database, and all sessions are lost on server restart. A session holds: | Field | Description | |---|---| | `agent` | `CharacterAgent` instance (LangChain-backed) that plays the AI role and holds conversation history | | `scenario_id` | The scenario this chat was started with | | `messages` | Ordered list of `{role, text, turn}` — the full transcript | | `notes` | Accumulated coaching notes, appended asynchronously | | `pending_turns` | Trainee turn numbers whose note extraction is still running in the background | A session must be created via `POST /chats` before any of the `/chats/{chat_id}/...` endpoints can be used, and every endpoint that takes a `chat_id` returns **404** if it doesn't exist (see [Errors](#errors)). ## Endpoints ### `GET /scenarios` All available scenarios, for a scenario picker UI. **Response 200** — `ScenariosResponse` ```json { "scenarios": [ { "id": "onboarding_report", "title": "string", "skill_area": "string", "ai_name": "string", "ai_role": "string", "learner_role": "string", "learning_goal": "string", "has_autoplay": true } ] } ``` Known scenario ids in this repo: `onboarding_report` (default), `coaching_employee`, `guest_price_complaint`. --- ### `POST /chats` Start a new chat session for a scenario. Returns a fresh `chat_id` and any scripted opening message from the AI character. **Request body** — `NewChatRequest` (optional; defaults to `onboarding_report`) ```json { "scenario_id": "onboarding_report" } ``` **Response 201** — `NewChatResponse` ```json { "chat_id": "uuid", "scenario_id": "onboarding_report", "messages": [ { "role": "manager", "text": "string", "turn": 0 } ] } ``` `messages` is empty if the scenario doesn't open with an AI line (`opens == "learner"`). **Errors** - `400` — unknown `scenario_id` --- ### `GET /chats/{chat_id}` Retrieve the full message history for a session. **Response 200** — `ChatHistoryResponse` ```json { "chat_id": "uuid", "messages": [ { "role": "trainee", "text": "string", "turn": 1 }, { "role": "manager", "text": "string", "turn": 1 } ] } ``` **Errors** - `404` — unknown `chat_id` --- ### `POST /chats/{chat_id}/message` Send a trainee message and get the full AI reply back in one blocking response. Note extraction for this turn is kicked off as a FastAPI background task (non-blocking). **Request body** — `ChatRequest` ```json { "message": "string" } ``` **Response 200** — `ChatResponse` ```json { "chat_id": "uuid", "reply": "string", "turn": 1, "messages": [ { "role": "trainee|manager", "text": "string", "turn": 1 } ] } ``` **Errors** - `404` — unknown `chat_id` --- ### `POST /chats/{chat_id}/stream` Same as `/message`, but streams the AI reply token-by-token over **Server-Sent Events**. **Request body** — `ChatRequest` (same as above) **Response 200** — `text/event-stream` ``` data: Hel data: lo data: ! data: [DONE] ``` Each `data:` line is a raw token chunk (not JSON — concatenate to reassemble the reply). The stream always ends with a `data: [DONE]` sentinel. Note extraction is scheduled on a background thread once streaming completes and is *not* reflected in this response — poll `GET /chats/{chat_id}/notes` and watch `pending_turns`. **Errors** - `404` — unknown `chat_id` --- ### `GET /skills` The rubric's skill list, in display order. Same set for every scenario — lets a UI render empty sections before any notes exist. **Response 200** — `SkillsResponse` ```json { "skills": [ { "slug": "string", "name": "string", "order": 0 } ] } ``` --- ### `GET /chats/{chat_id}/notes` Flat, insertion-ordered list of coaching notes extracted so far. Grouping by skill is left to the caller. **Response 200** — `NotesResponse` ```json { "chat_id": "uuid", "notes": [ { "skill": "string", "note": "string", "evidence": "string", "turn": 1 } ], "pending_turns": [2] } ``` `pending_turns` lists trainee turns whose extraction is still running in the background — poll this endpoint until a turn drops out of the list to know its notes (possibly none) are final. **Errors** - `404` — unknown `chat_id` --- ### `POST /chats/{chat_id}/assessment` Score the conversation so far against the rubric's skills. The result is **cached on the session** — repeat calls for the same `chat_id` return the same assessment without re-invoking the LLM. **Response 200** — `Assessment` ```json { "overall_summary": "string", "grades": [ { "skill": "string", "level": "Low | Medium | High", "reasoning": "string", "evidence_turns": [1, 3] } ], "key_moment": "string", "coaching_suggestion": "string", "readiness_score": 7 } ``` **Errors** - `400` — no messages yet to assess, or no rubric skills configured for the scenario - `404` — unknown `chat_id` --- ### `DELETE /chats/{chat_id}` Drop a session and free its in-memory state. **Response** — `204 No Content` **Errors** - `404` — unknown `chat_id` --- ### `GET /health` Liveness/readiness probe. **Response 200** ```json { "status": "ok", "active_sessions": 3 } ``` ## Errors All error responses use FastAPI's default shape: ```json { "detail": "chat_id '...' not found" } ``` | Status | Meaning | |---|---| | 400 | Bad request — unknown `scenario_id`, empty conversation on assessment, or no rubric configured | | 404 | `chat_id` not found (session expired, deleted, or server restarted) | | 422 | Request body failed Pydantic validation (e.g. missing `message` field) | ## Typical flow 1. `GET /scenarios` → let the user pick a scenario 2. `POST /chats` → get a `chat_id` 3. Repeat: `POST /chats/{chat_id}/message` (or `/stream`) for each trainee turn 4. `GET /chats/{chat_id}/notes` → poll for live coaching notes, checking `pending_turns` 5. `POST /chats/{chat_id}/assessment` → final rubric-based score once the conversation is done 6. `DELETE /chats/{chat_id}` → clean up when finished A minimal reference client for this exact flow lives in [api_client.py](api_client.py).