회기 연속성과 멀티 케이스 계약을 영속화

This commit is contained in:
Yun Chan 2026-09-01 11:45:16 +09:00
parent be08c0b573
commit 72353ecd82
26 changed files with 2170 additions and 127 deletions

View file

@ -28,7 +28,15 @@ from ..deps import Principal, Role, require_role
from ..engine_client import EngineError, engine_client
from ..runtime_policy import runtime_fallback_allowed
from ..session_evaluation_input import enriched_masked_turns
from ..session_read_model import StageLabel, stage_label_or_none
from ..session_evaluation_timeout import (
session_evaluation_transport_timeout_seconds,
)
from ..session_read_model import (
ReviewEvaluationFailure,
StageLabel,
stage_label_or_none,
teacher_evaluation_failure,
)
from ..services import evaluator
from ..services.evaluator import SessionEvaluation, TurnEvaluation
from ..store import InProcSession
@ -51,12 +59,13 @@ class TurnReevaluateRequest(BaseModel):
class EvaluationSummary(BaseModel):
"""회기 평가 조회 응답(분포 + deep 결과 합본)."""
"""회기 평가 조회 응답(분포 + deep 결과 합본, provider 오류 원문 제외)."""
session_id: str
stage: StageLabel | None = None
status: str | None = None
error: str | None = None
failure: ReviewEvaluationFailure | None = None
durable: bool = False
deep: Optional[dict[str, Any]] = None
distribution: dict[str, Any] = Field(default_factory=dict)
@ -108,6 +117,15 @@ def _session_evaluation_error_status(error: str) -> int:
return status.HTTP_502_BAD_GATEWAY
def _safe_session_evaluation_retry_detail() -> str:
"""재시도 HTTP 응답에서는 provider 예외 원문을 내보내지 않는다.
원인은 durable evaluation record에 서버 전용으로 보존하고, 교수자 화면은 review의
안전 분류(evaluationFailure) 다음 행동만 안내한다.
"""
return "AI 평가를 완료하지 못했습니다. 최신 평가 상태를 확인해 주세요."
# ════════════════════════════════════════════════════════════════════════════
# 회기 deep-loop 재평가 트리거 (교수자/관리자)
# ════════════════════════════════════════════════════════════════════════════
@ -120,7 +138,8 @@ async def reevaluate_session(
"""회기 전체 deep-loop 재평가(슈퍼바이저 rationale/critique + 개선점 + 대안발화).
저장된 마스킹 축어록을 evaluator.evaluate_session 으로 평가한다.
엔진 장애는 503 으로 변환(평가는 비치명적이지만 트리거는 사용자 명시 요청이라 에러 노출).
엔진 장애는 503으로 변환한다. provider 예외 원문은 durable 기록에만 남기고 HTTP에는
안전한 안내만 반환한다.
"""
sess = await _load_session_or_404(session_id, principal)
counselor_identity = getattr(sess, "learner_label", None)
@ -144,6 +163,7 @@ async def reevaluate_session(
theory_mode=_theory_mode_of(sess),
scope=body.scope if body.scope in ("session_end", "stage_transition") else "session_end",
audit_hook=session_persistence.record_llm_call_audit,
timeout=session_evaluation_transport_timeout_seconds(),
)
except EngineError as e:
detail = f"engine unavailable: {e}"
@ -163,7 +183,10 @@ async def reevaluate_session(
session_id,
write.error,
)
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail=detail)
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
detail=_safe_session_evaluation_retry_detail(),
)
write = session_persistence.SessionEvaluationWrite.from_result(
session_id=session_id,
@ -178,7 +201,10 @@ async def reevaluate_session(
logger.error("%s: session_id=%s status=%s", detail, session_id, write.status)
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail=detail)
if result.error:
raise HTTPException(_session_evaluation_error_status(result.error), detail=result.error)
raise HTTPException(
_session_evaluation_error_status(result.error),
detail=_safe_session_evaluation_retry_detail(),
)
return result
@ -256,7 +282,10 @@ async def reevaluate_turn(
)
learner.evaluation = result_payload
if result.error:
raise HTTPException(_session_evaluation_error_status(result.error), detail=result.error)
raise HTTPException(
_session_evaluation_error_status(result.error),
detail=_safe_session_evaluation_retry_detail(),
)
return result
@ -285,13 +314,18 @@ async def get_session_evaluation(
distribution={},
)
payload = record.get("payload")
deep = payload if isinstance(payload, dict) else {}
deep = dict(payload) if isinstance(payload, dict) else {}
# SessionEvaluation.to_dict()에는 server-side error가 함께 저장된다. deep 객체도
# 교수자 API 경계에서는 같은 원칙으로 제거한다.
deep.pop("error", None)
distribution = deep.get("distribution")
failure = teacher_evaluation_failure(record)
return EvaluationSummary(
session_id=session_id,
stage=_summary_stage(record.get("stage") or deep.get("stage")),
status=str(record.get("status") or "") or None,
error=str(record.get("error") or "") or None,
error=_safe_session_evaluation_retry_detail() if failure is not None else None,
failure=failure,
durable=durable,
deep=deep,
distribution=distribution if isinstance(distribution, dict) else {},