Merge origin/dev into feat/rag-clinical-guidelines
CI / ruff · pytest (pull_request) Successful in 2m9s

Conflict in app/api/routes.py: /demo/personas keeps the dev version (demo console, DB patients
with scenarios); the patient app gets its own /patient/demo-personas (synthetic ЕМИАС accounts).
Anonymous sign-in now always creates a doctor-less pseudonymous profile, since the dialog API
of #12 serves known profiles only.

Refs #77

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 15:22:29 +03:00
co-authored by Claude Opus 5.5
82 changed files with 9945 additions and 153 deletions
+13 -6
View File
@@ -27,6 +27,7 @@ from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import select
from app.api import fixtures
from app.api.schemas import DemoPersona
from app.core import clock
from app.core.instrument_catalog import Catalog, catalog
from app.core.intake_questions import DIARY_KINDS
@@ -133,9 +134,15 @@ async def auth_emias(body: EmiasCodeIn) -> AuthOut:
return AuthOut(token=person.token, mode="emias")
@router.get("/patient/demo-personas", response_model=list[DemoPersona])
async def patient_demo_personas() -> list[DemoPersona]:
"""Synthetic ЕМИАС accounts for the patient app sign-in (the demo console has its own `/demo/personas`)."""
return fixtures.DEMO_PERSONAS
@router.post("/auth/emias/demo", response_model=AuthOut)
async def auth_emias_demo(body: EmiasDemoIn) -> AuthOut:
"""Demo ЕМИАС account: signs in as one of the synthetic personas (`/demo/personas`)."""
"""Demo ЕМИАС account: signs in as one of the synthetic personas (`/patient/demo-personas`)."""
persona = next((p for p in fixtures.DEMO_PERSONAS if p.token == body.persona_token), None)
if persona is None:
raise HTTPException(404, "persona not found")
@@ -148,13 +155,13 @@ async def auth_emias_demo(body: EmiasDemoIn) -> AuthOut:
async def auth_anonymous(body: AnonymousIn | None = None) -> AuthOut:
"""Anonymous sign-in: a fresh random token; no identity record, no doctor.
With `focus` a doctor-less pseudonymous profile keeps only the chosen module, so the engine starts the
matching onboarding instead of going straight to the menu. Without it nothing is stored at all.
The clinical side gets a doctor-less pseudonymous profile (the dialog API serves known profiles only). It holds
no personal data — only the module of the chosen `focus`; the stage 1 screening then sets the modules itself.
"""
token = secrets.token_hex(TOKEN_LEN // 2)
if body is not None and body.focus is not None:
async with get_clinical_session() as s:
await repo.upsert_profile(s, token=token, doctor_id="", modules=[_FOCUS_MODULE[body.focus]])
focus = body.focus if body is not None else None
async with get_clinical_session() as s:
await repo.upsert_profile(s, token=token, doctor_id="", modules=[_FOCUS_MODULE[focus]] if focus else [])
log.info("web auth: anonymous tok=%s", token[:6])
return AuthOut(token=token, mode="anonymous")