# Privoid Action — local inference API A minimal FastAPI service (`privoid_action_api/`) that serves the Action adapter on the original Granite base, or a validated merged model, from **local files only**. ## Run ```powershell cd Privoid-Action-1B copy api\config.example.json api\config.json # then set adapter_path to this repository's root folder python -m privoid_action_api --config api\config.json --check-config python -m privoid_action_api --config api\config.json ``` * Binds to `127.0.0.1:8095` by default. Any non-loopback host is refused unless `allow_non_loopback: true` is set. * Startup sets `HF_HUB_OFFLINE=1` / `TRANSFORMERS_OFFLINE=1` before any Hugging Face import and loads with `local_files_only=True`. It never downloads, trains or prepares data. If artifacts are missing or invalid, the server still starts, `/health` reports `model_state: "unavailable"`, and `/extract` returns 503. * The loader refuses adapters whose base is not Granite 4.0 1B. * The model loads at startup (`load_on_startup: true`). One generation runs at a time (`max_concurrent: 1`, sized for a 12 GB GPU), with up to `max_queue` requests waiting. ## Endpoints ### `GET /health` Process health. Never loads or downloads anything. ```json {"status": "ok", "model_state": "ready", "request_id": "3f1c0a9e5b7d2c41"} ``` `model_state` is one of `not_loaded`, `loading`, `ready`, `unavailable`. ### `GET /model-info` Model identity (base ID and revision, adapter folder name and a short weight hash), schema version, and limits (`max_prompt_tokens`, `max_new_tokens`, `max_body_bytes`, timeout, concurrency, contract limits). Filesystem paths are not exposed. ### `POST /extract` Body: one transcript segment in the input schema (`docs/SCHEMA.md` §1). Example requests are in `examples/`: ```powershell curl.exe -s -X POST http://127.0.0.1:8095/extract -H "content-type: application/json" ` --data-binary "@examples/request_accepted_request.json" ``` Response 200 (abridged; the full illustrative response is in `examples/response_accepted_request.json`. It was produced by the deterministic finalizer from a hand-written output, **not** by the model): ```json { "request_id": "…", "schema_version": "1.0", "result": { "schema_version": "1.0", "meeting_id": "meeting-001", "actions": [{ "id": "A1", "task": "Send the revised proposal.", "owner_speaker_ids": ["S2"], "owner_text": "Omar", "assignment_status": "accepted", "deadline_text": "Friday", "conditions": [], "status": "active", "evidence": [{"turn_id": "T1", "quote": "Omar, can you send the revised proposal by Friday?"}, {"turn_id": "T2", "quote": "Yes, I will send it by Friday."}], "needs_review": false, "review_reasons": [], "pipeline_flags": ["deadline_not_normalized"], "owner_names": ["Omar"], "deadline_normalized": null, "deadline_normalization": {"rule": null, "reason": "bare_weekday_ambiguous"}}], "decisions": [], "unresolved_requests": [], "rejected_records": []}, "validation": {"raw_output_fully_valid": true, "rejected_record_count": 0, "grounded_record_count": 0}, "usage": {"prompt_tokens": 882, "seconds": 1.9}, "model": {"backend": "transformers", "base_model_id": "ibm-granite/granite-4.0-1b", "…": "…"}, "notice": "Records are extracted from the transcript for human review. They do not create tasks, …" } ``` (The `seconds` value above is a placeholder, not a measurement.) ### `POST /extract-meeting` (experimental, disabled by default) Enable with `enable_meeting_endpoint: true`. The endpoint cuts the meeting into whole-turn segments (`max_segment_tokens`, `segment_overlap_turns`), extracts each segment, and reconciles the results with the documented rules (`docs/SCHEMA.md` §5). The response is marked `"experimental": true` and includes `segments`, `reconciliation_log`, `limitations`, `segment_failures` and `complete`. Its quality is measured separately (notebook §10.5). Cross-segment cancellations are flagged, not applied. ## Errors All errors are JSON: `{"error": code, "message": text, "request_id": id, …}`. They never contain stack traces or filesystem paths. | Status | `error` | When | |---|---|---| | 400 | `invalid_json` | Body is not JSON | | 413 | `input_too_large` | Body > `max_body_bytes`, a contract size limit, or prompt tokens > `max_prompt_tokens`. **Nothing is truncated**; split the transcript. | | 422 | `invalid_input` | Schema problems (unknown speaker, duplicate turn ID, invalid date or timezone, control sequences …), listed in `problems` | | 429 | `busy` | All slots busy and the wait queue is full | | 502 | `model_output_invalid` | The model output was not one schema-valid JSON object. No records are returned and none are invented. | | 503 | `model_unavailable` | Model not loaded (see the server log) | | 504 | `timeout` | Generation exceeded `request_timeout_s`. Generation is also bounded by `max_new_tokens` and a matching `max_time`, and the slot stays occupied until the worker thread really finishes. | | 500 | `internal_error` | Unexpected failure (details only in the server log, keyed by request ID) | A record that fails validation (quote not found, unknown speaker, inconsistent status) is returned in `result.rejected_records` with its errors, and `validation.raw_output_fully_valid` is `false`. Valid records are still returned. With `ground_evidence: true` (the default), quotes that the model tidied (fillers or repeated words dropped) or attached to a neighbouring turn are first re-anchored to the transcript's verbatim words; see `docs/SCHEMA.md` §2.5. Every record changed this way carries a pipeline flag, and `validation.grounded_record_count` counts them. Set `ground_evidence: false` to reject such records instead. ## Privacy behaviour * Transcript bodies are never logged and never written to disk. Log lines carry the request ID, path, status, latency, turn count, prompt-token count and validation counts only. Uvicorn's access log is disabled. * No cloud fallback exists. The only network listener is the configured local address. * Validation, and any constrained decoding a client might add, ensures the output is well-formed. It is **not proof of semantic correctness**: owners, statuses and deadlines must still be reviewed by a human before anyone acts on them. * Extracted records never send messages, create tasks in external systems, or commit participants to anything. Privacy of a deployment also depends on the surrounding application, its logs and storage, the host OS and its telemetry, and the network configuration. Those are outside this API. ## Tests `python -m unittest discover -s tests -t .` runs the API tests against a **mocked** backend. They cover every route, error code, oversize rejection, invalid-output rejection, record-level rejection, timeouts, the concurrency bound, a full queue (429), hidden internal errors, the loopback-only default, no transcript text in logs, and a check that importing the API does not import torch/transformers/peft/trl. Real-model checks are in notebook §12 (`ALLOW_API_CHECKS`).