세션 평가·라이브코치·교수자 분석 라운드 마감 + 문서 정리 + 코드품질 리팩터
- 누적 작업트리 커밋: 회기 평가 복구·durable 저장, 라이브 코치 이력/근거, 교수자 학생분석, 음성 비언어 메타, PII 마스킹, 운영 티켓/헬스 등 - 문서: 완료 기록 docs/archive/ 냉동 보관, docs/ 단일 인덱스(docs/README.md)+통합 TODO(docs/TODO.md)로 정리 - 리팩터(행위 보존): Stage enum SSOT(taxonomy 소유·state_machine re-export), store recent/masked_turns 중복 제거, speaker_ko_label 단일 헬퍼, _list_sessions N+1 제거(state/turns 배치 + 턴평가 하이드레이션 배치) - 검증: 백엔드 pytest 352 passed, _list_sessions E2E chromium-single-run 2 passed
This commit is contained in:
parent
7c41c3ce79
commit
778e8526d4
108 changed files with 6457 additions and 455 deletions
|
|
@ -18,7 +18,7 @@ import hashlib
|
|||
import time
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.websockets import WebSocketState
|
||||
|
||||
|
|
@ -97,6 +97,13 @@ _PROVIDER_EVENT_TAXONOMY = {
|
|||
"noise": ("background_noise", "audio_quality"),
|
||||
"background_noise": ("background_noise", "audio_quality"),
|
||||
}
|
||||
|
||||
|
||||
def _is_turn_persistence_unavailable(exc: Exception) -> bool:
|
||||
if not isinstance(exc, HTTPException) or exc.status_code != 503:
|
||||
return False
|
||||
detail = str(exc.detail or "")
|
||||
return "turn append" in detail and "persistence unavailable" in detail
|
||||
_PROVIDER_EVENT_TYPE_FIELDS = ("event_type", "type", "kind", "label")
|
||||
|
||||
|
||||
|
|
@ -303,7 +310,18 @@ async def voice_ws(websocket: WebSocket) -> None:
|
|||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception as e:
|
||||
await _safe_send_json(websocket, {"type": "error", "detail": f"voice ws error: {e}"})
|
||||
if _is_turn_persistence_unavailable(e):
|
||||
await _safe_send_json(
|
||||
websocket,
|
||||
{
|
||||
"type": "error",
|
||||
"code": "turn_persistence_unavailable",
|
||||
"detail": "voice turn persistence unavailable; retry the utterance",
|
||||
},
|
||||
)
|
||||
await _safe_send_json(websocket, {"type": "state", "state": "idle"})
|
||||
else:
|
||||
await _safe_send_json(websocket, {"type": "error", "detail": f"voice ws error: {e}"})
|
||||
finally:
|
||||
await _safe_close(websocket)
|
||||
|
||||
|
|
@ -609,6 +627,17 @@ async def _principal_from_websocket(websocket: WebSocket) -> Principal | None:
|
|||
)
|
||||
|
||||
|
||||
async def _practice_access_error(principal: Principal) -> str | None:
|
||||
if (
|
||||
principal.profile_completed_at is None
|
||||
and not await user_onboarding_complete(principal.user_id)
|
||||
):
|
||||
return "onboarding_required"
|
||||
if principal.consent_at is None and not await user_has_consent(principal.user_id):
|
||||
return "consent_required"
|
||||
return None
|
||||
|
||||
|
||||
async def _bind_session(
|
||||
websocket: WebSocket,
|
||||
principal: Principal,
|
||||
|
|
@ -616,6 +645,9 @@ async def _bind_session(
|
|||
"""Bind an existing session or create a dev-only voice session."""
|
||||
qp = websocket.query_params
|
||||
explicit_preset = qp.get("preset")
|
||||
access_error = await _practice_access_error(principal)
|
||||
if access_error is not None:
|
||||
return None, None, access_error, {}
|
||||
|
||||
session_id = qp.get("session_id")
|
||||
if session_id:
|
||||
|
|
@ -636,13 +668,6 @@ async def _bind_session(
|
|||
persona_code = qp.get("persona_code")
|
||||
if not persona_code:
|
||||
return None, None, "session_id or persona_code query required", {}
|
||||
if (
|
||||
principal.profile_completed_at is None
|
||||
and not await user_onboarding_complete(principal.user_id)
|
||||
):
|
||||
return None, None, "onboarding_required", {}
|
||||
if principal.consent_at is None and not await user_has_consent(principal.user_id):
|
||||
return None, None, "consent_required", {}
|
||||
try:
|
||||
catalog_persona = await get_catalog_persona(persona_code)
|
||||
except Exception:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue