세션 평가와 교수자 분석 보강
This commit is contained in:
parent
5c4ac04e06
commit
fe2796f05a
51 changed files with 4928 additions and 240 deletions
|
|
@ -17,6 +17,7 @@ in-proc cache/session fallback 은 local dev 에서만 허용한다.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Annotated, Any, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
|
@ -33,6 +34,7 @@ from ..store import InProcSession
|
|||
from ..store import store
|
||||
|
||||
router = APIRouter(prefix="/eval", tags=["eval"])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 교수자/관리자만 평가 조회·트리거 (학습자 비노출)
|
||||
TeacherOrAdmin = Annotated[Principal, Depends(require_role(Role.TEACHER, Role.ADMIN))]
|
||||
|
|
@ -52,6 +54,9 @@ class EvaluationSummary(BaseModel):
|
|||
|
||||
session_id: str
|
||||
stage: StageLabel | None = None
|
||||
status: str | None = None
|
||||
error: str | None = None
|
||||
durable: bool = False
|
||||
deep: Optional[dict[str, Any]] = None
|
||||
distribution: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
|
@ -76,11 +81,14 @@ async def _load_session_or_404(session_id: str, principal: Principal) -> InProcS
|
|||
|
||||
|
||||
def _theory_mode_of(sess) -> Optional[str]:
|
||||
# 회기에서 학습자가 명시 선택한 이론 모드가 최우선이다.
|
||||
sess_theory = str(getattr(sess, "theory_mode", "") or "").strip()
|
||||
if sess_theory:
|
||||
return sess_theory
|
||||
tt = getattr(sess.persona, "theory_target", None)
|
||||
if isinstance(tt, (list, tuple)) and tt:
|
||||
return ", ".join(str(x) for x in tt)
|
||||
# store 가 theory_mode 문자열도 보유(InProcSession.theory_mode)
|
||||
return getattr(sess, "theory_mode", None)
|
||||
return None
|
||||
|
||||
|
||||
def _summary_stage(value: object) -> StageLabel | None:
|
||||
|
|
@ -93,6 +101,12 @@ async def eval_health() -> dict[str, str]:
|
|||
return {"status": "ok", "owner": "features:evaluator", "loops": "fast,deep"}
|
||||
|
||||
|
||||
def _session_evaluation_error_status(error: str) -> int:
|
||||
if error.startswith("engine_error"):
|
||||
return status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
return status.HTTP_502_BAD_GATEWAY
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 회기 deep-loop 재평가 트리거 (교수자/관리자)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -131,18 +145,35 @@ async def reevaluate_session(
|
|||
audit_hook=session_persistence.record_llm_call_audit,
|
||||
)
|
||||
except EngineError as e:
|
||||
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail=f"engine unavailable: {e}")
|
||||
|
||||
if result.error and result.error.startswith("engine_error"):
|
||||
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail=result.error)
|
||||
|
||||
await session_persistence.save_session_evaluation(
|
||||
session_persistence.SessionEvaluationWrite.from_result(
|
||||
detail = f"engine unavailable: {e}"
|
||||
write = session_persistence.SessionEvaluationWrite.from_error(
|
||||
session_id=session_id,
|
||||
learner_id=sess.learner_id,
|
||||
result=result,
|
||||
scope=body.scope if body.scope in ("session_end", "stage_transition") else "session_end",
|
||||
stage=sess.state.stage.value,
|
||||
error=detail,
|
||||
)
|
||||
saved = await session_persistence.save_session_evaluation(write)
|
||||
if not saved:
|
||||
logger.error(
|
||||
"session evaluation retry error record did not reach durable store: session_id=%s error=%s",
|
||||
session_id,
|
||||
write.error,
|
||||
)
|
||||
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail=detail)
|
||||
|
||||
write = session_persistence.SessionEvaluationWrite.from_result(
|
||||
session_id=session_id,
|
||||
learner_id=sess.learner_id,
|
||||
result=result,
|
||||
)
|
||||
saved = await session_persistence.save_session_evaluation(write)
|
||||
if not saved:
|
||||
detail = "session evaluation retry result was generated but could not be saved"
|
||||
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)
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -193,6 +224,7 @@ async def reevaluate_turn(
|
|||
learner_text_masked=learner.text_masked,
|
||||
state_after=sess.state, # 조회 시점 상태(정밀 재현은 DB 스냅샷 도입 시)
|
||||
memory=TurnMemory(recent_turns=recent),
|
||||
theory_mode=_theory_mode_of(sess),
|
||||
)
|
||||
|
||||
result = await evaluator.evaluate_turn(
|
||||
|
|
@ -219,15 +251,26 @@ async def get_session_evaluation(
|
|||
아직 평가 트리거가 없었다면 deep=None + 빈 분포.
|
||||
"""
|
||||
await _load_session_or_404(session_id, principal)
|
||||
record, _durable = await session_persistence.load_session_evaluation(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=None, deep=None, distribution={})
|
||||
return EvaluationSummary(
|
||||
session_id=session_id,
|
||||
stage=None,
|
||||
status=None,
|
||||
error=None,
|
||||
durable=durable,
|
||||
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=_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,
|
||||
durable=durable,
|
||||
deep=deep,
|
||||
distribution=distribution if isinstance(distribution, dict) else {},
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue