Stabilize runtime auth and E2E coverage
This commit is contained in:
parent
6a3e3b541c
commit
188e899394
133 changed files with 55987 additions and 6775 deletions
|
|
@ -11,8 +11,8 @@ services/evaluator.py 의 2-loop 평가(fast/deep)를 교수자(TEACHER)·관리
|
|||
POST /eval/sessions/{id}/reevaluate — 회기 deep-loop 재평가 트리거(전체 축어록)
|
||||
GET /eval/sessions/{id}/evaluation — 회기 평가 조회(분포 + 최근 deep 결과)
|
||||
|
||||
DB(feedback_scores/supervisor_comment) SoR 적재는 Phase 2. 현재는 in-proc store + 엔진 직접 호출
|
||||
(degraded). DB 가 붙으면 조회 경로를 turns.evaluation / supervisor_comment 조인으로 교체한다.
|
||||
평가 결과는 session_persistence 의 DB-backed evaluation 저장소를 사용한다. DB 미가용 시
|
||||
in-proc cache/session fallback 은 local dev 에서만 허용한다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -22,10 +22,13 @@ from typing import Annotated, Any, Optional
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .. import session_persistence
|
||||
from ..deps import Principal, Role, require_role
|
||||
from ..engine_client import EngineError, engine_client
|
||||
from ..runtime_policy import runtime_fallback_allowed
|
||||
from ..services import evaluator
|
||||
from ..services.evaluator import SessionEvaluation, TurnEvaluation
|
||||
from ..store import InProcSession
|
||||
from ..store import store
|
||||
|
||||
router = APIRouter(prefix="/eval", tags=["eval"])
|
||||
|
|
@ -52,14 +55,12 @@ class EvaluationSummary(BaseModel):
|
|||
distribution: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
# ── in-proc 평가 결과 캐시 (DB 적재 전 degraded 보관) ───────────────────────
|
||||
# DB 가 붙으면 turns.evaluation / supervisor_comment 로 대체. 지금은 트리거 결과를 보관해
|
||||
# 조회 GET 이 재호출 없이 마지막 deep 결과를 돌려주게 한다.
|
||||
_DEEP_CACHE: dict[str, SessionEvaluation] = {}
|
||||
|
||||
|
||||
def _load_session_or_404(session_id: str):
|
||||
sess = store.get(session_id)
|
||||
async def _load_session_or_404(session_id: str, principal: Principal) -> InProcSession:
|
||||
sess = await session_persistence.load_session(session_id, principal, allow_ended=True)
|
||||
if sess is not None:
|
||||
store.put(sess)
|
||||
elif runtime_fallback_allowed():
|
||||
sess = store.get(session_id)
|
||||
if sess is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="session not found")
|
||||
return sess
|
||||
|
|
@ -90,10 +91,10 @@ async def reevaluate_session(
|
|||
) -> SessionEvaluation:
|
||||
"""회기 전체 deep-loop 재평가(슈퍼바이저 rationale/critique + 개선점 + 대안발화).
|
||||
|
||||
in-proc store 의 마스킹 축어록을 evaluator.evaluate_session 으로 평가한다.
|
||||
저장된 마스킹 축어록을 evaluator.evaluate_session 으로 평가한다.
|
||||
엔진 장애는 503 으로 변환(평가는 비치명적이지만 트리거는 사용자 명시 요청이라 에러 노출).
|
||||
"""
|
||||
sess = _load_session_or_404(session_id)
|
||||
sess = await _load_session_or_404(session_id, principal)
|
||||
masked = sess.masked_turns()
|
||||
# 발화 seq 보강(deep 프롬프트 가독성 — store 가 seq 미포함이라 인덱스로 부여)
|
||||
enriched: list[dict[str, Any]] = []
|
||||
|
|
@ -121,7 +122,16 @@ async def reevaluate_session(
|
|||
if result.error and result.error.startswith("engine_error"):
|
||||
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail=result.error)
|
||||
|
||||
_DEEP_CACHE[session_id] = result
|
||||
await session_persistence.save_session_evaluation(
|
||||
session_id=session_id,
|
||||
learner_id=sess.learner_id,
|
||||
status="error" if result.error else "ready",
|
||||
source="engine",
|
||||
scope=result.scope,
|
||||
stage=result.stage,
|
||||
payload=result.to_dict(),
|
||||
error=result.error,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -136,10 +146,10 @@ async def reevaluate_turn(
|
|||
) -> TurnEvaluation:
|
||||
"""단일 상담자 발화 fast-loop 재평가(기법/내담자상태/적절성/의도이탈).
|
||||
|
||||
store 의 축어록에서 해당 turn_seq 상담자 발화 + 직후 내담자 응답을 재구성해
|
||||
저장된 축어록에서 해당 turn_seq 상담자 발화 + 직후 내담자 응답을 재구성해
|
||||
경량 TurnContext 로 evaluator.evaluate_turn 을 호출한다.
|
||||
"""
|
||||
sess = _load_session_or_404(session_id)
|
||||
sess = await _load_session_or_404(session_id, principal)
|
||||
|
||||
# 대상 상담자 발화 + 직후 내담자 응답 찾기
|
||||
target_idx: Optional[int] = None
|
||||
|
|
@ -188,18 +198,20 @@ async def get_session_evaluation(
|
|||
session_id: str,
|
||||
principal: TeacherOrAdmin,
|
||||
) -> EvaluationSummary:
|
||||
"""회기 평가 조회(읽기) — 마지막 deep 재평가 결과 + 기법 분포.
|
||||
"""회기 평가 조회(읽기) — 저장된 마지막 deep 재평가 결과 + 기법 분포.
|
||||
|
||||
DB 적재 전 degraded: deep 결과는 reevaluate 트리거가 보관한 캐시에서, 분포는 그 결과에서.
|
||||
아직 평가 트리거가 없었다면 deep=None + 빈 분포.
|
||||
"""
|
||||
_load_session_or_404(session_id)
|
||||
cached = _DEEP_CACHE.get(session_id)
|
||||
if cached is None:
|
||||
await _load_session_or_404(session_id, principal)
|
||||
record, _durable = await session_persistence.load_session_evaluation(session_id, principal)
|
||||
if record is None:
|
||||
return EvaluationSummary(session_id=session_id, stage="", deep=None, distribution={})
|
||||
payload = record.get("payload")
|
||||
deep = payload if isinstance(payload, dict) else {}
|
||||
distribution = deep.get("distribution")
|
||||
return EvaluationSummary(
|
||||
session_id=session_id,
|
||||
stage=cached.stage,
|
||||
deep=cached.to_dict(),
|
||||
distribution=cached.distribution.model_dump(),
|
||||
stage=str(record.get("stage") or deep.get("stage") or ""),
|
||||
deep=deep,
|
||||
distribution=distribution if isinstance(distribution, dict) else {},
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue