세션 평가와 교수자 분석 보강
This commit is contained in:
parent
5c4ac04e06
commit
fe2796f05a
51 changed files with 4928 additions and 240 deletions
|
|
@ -12,6 +12,7 @@ from ..deps import Principal, Role, require_role
|
|||
from ..runtime_policy import require_runtime_fallback_allowed
|
||||
from ..session_read_model import StageLabel, stage_label
|
||||
from ..services import session_metrics
|
||||
from ..stage_contract import STAGE_LABEL_VALUES
|
||||
from ..store import InProcSession, store
|
||||
|
||||
router = APIRouter(prefix="/teacher", tags=["teacher"])
|
||||
|
|
@ -86,6 +87,28 @@ class TeacherLearnerGrowth(BaseModel):
|
|||
points: list[TeacherGrowthPoint] = Field(default_factory=list)
|
||||
|
||||
|
||||
class TeacherStageBreakdown(BaseModel):
|
||||
stage: StageLabel
|
||||
sessions: int = 0
|
||||
turns: int = 0
|
||||
|
||||
|
||||
class TeacherLearnerAnalysisResponse(BaseModel):
|
||||
source: str = "in_memory"
|
||||
learner_id: str
|
||||
learner_label: str
|
||||
total_sessions: int
|
||||
active_sessions: int
|
||||
ended_sessions: int
|
||||
pending_reviews: int
|
||||
closed_reviews: int
|
||||
summary: TeacherLearnerGrowth
|
||||
points: list[TeacherGrowthPoint] = Field(default_factory=list)
|
||||
stage_breakdown: list[TeacherStageBreakdown] = Field(default_factory=list)
|
||||
sessions: list[TeacherSessionSummary] = Field(default_factory=list)
|
||||
message: str
|
||||
|
||||
|
||||
class TeacherSafetyAlert(BaseModel):
|
||||
id: str
|
||||
session_id: str
|
||||
|
|
@ -134,11 +157,17 @@ def _growth_point(point: session_metrics.SessionGrowthPoint) -> TeacherGrowthPoi
|
|||
)
|
||||
|
||||
|
||||
def _build_learner_growth(sessions: list[InProcSession]) -> list[TeacherLearnerGrowth]:
|
||||
def _build_learner_growth(
|
||||
sessions: list[InProcSession],
|
||||
*,
|
||||
limit: int | None = 12,
|
||||
point_limit: int | None = 6,
|
||||
) -> list[TeacherLearnerGrowth]:
|
||||
metrics = session_metrics.build_learner_growth(
|
||||
sessions,
|
||||
learner_label=_learner_label,
|
||||
limit=12,
|
||||
limit=limit,
|
||||
point_limit=point_limit,
|
||||
)
|
||||
return [
|
||||
TeacherLearnerGrowth(
|
||||
|
|
@ -160,6 +189,26 @@ def _build_learner_growth(sessions: list[InProcSession]) -> list[TeacherLearnerG
|
|||
]
|
||||
|
||||
|
||||
def _stage_breakdown(sessions: list[InProcSession]) -> list[TeacherStageBreakdown]:
|
||||
buckets = {
|
||||
stage: {"sessions": 0, "turns": 0}
|
||||
for stage in STAGE_LABEL_VALUES
|
||||
}
|
||||
for sess in sessions:
|
||||
label = stage_label(sess.state.stage)
|
||||
bucket = buckets[label]
|
||||
bucket["sessions"] += 1
|
||||
bucket["turns"] += len(sess.turns)
|
||||
return [
|
||||
TeacherStageBreakdown(
|
||||
stage=stage,
|
||||
sessions=buckets[stage]["sessions"],
|
||||
turns=buckets[stage]["turns"],
|
||||
)
|
||||
for stage in STAGE_LABEL_VALUES
|
||||
]
|
||||
|
||||
|
||||
def _review_status_value(record: dict[str, object] | None) -> Literal["pending", "viewed", "closed"]:
|
||||
value = str((record or {}).get("status") or "pending")
|
||||
if value in {"viewed", "closed"}:
|
||||
|
|
@ -274,6 +323,64 @@ async def teacher_dashboard(principal: TeacherPrincipal) -> TeacherDashboardResp
|
|||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/learners/{learner_id}/analysis",
|
||||
response_model=TeacherLearnerAnalysisResponse,
|
||||
)
|
||||
async def learner_analysis(
|
||||
learner_id: str,
|
||||
principal: TeacherPrincipal,
|
||||
) -> TeacherLearnerAnalysisResponse:
|
||||
"""Return one learner's full teacher-visible session timeline and trend."""
|
||||
sessions, durable = await session_persistence.list_sessions(
|
||||
principal,
|
||||
include_turn_evaluation=True,
|
||||
)
|
||||
if not durable:
|
||||
require_runtime_fallback_allowed("teacher learner analysis")
|
||||
sessions = sorted(store.list(), key=lambda sess: sess.created_at, reverse=True)
|
||||
|
||||
learner_sessions = [sess for sess in sessions if sess.learner_id == learner_id]
|
||||
if not learner_sessions:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="learner not found")
|
||||
|
||||
ordered = sorted(
|
||||
learner_sessions,
|
||||
key=lambda sess: (sess.created_at, sess.session_no, sess.session_id),
|
||||
)
|
||||
review_statuses, _ = await session_persistence.list_session_review_statuses(
|
||||
[sess.session_id for sess in ordered if sess.ended],
|
||||
principal,
|
||||
)
|
||||
summaries = [
|
||||
_summary(sess, review_statuses.get(sess.session_id))
|
||||
for sess in ordered
|
||||
]
|
||||
growth = _build_learner_growth(ordered, limit=None, point_limit=None)[0]
|
||||
pending_reviews = sum(
|
||||
1
|
||||
for item in summaries
|
||||
if item.status == "ended" and item.review_status != "closed"
|
||||
)
|
||||
closed_reviews = sum(1 for item in summaries if item.review_status == "closed")
|
||||
|
||||
return TeacherLearnerAnalysisResponse(
|
||||
source="database" if durable else "runtime",
|
||||
learner_id=learner_id,
|
||||
learner_label=growth.learner_label,
|
||||
total_sessions=len(ordered),
|
||||
active_sessions=sum(1 for sess in ordered if not sess.ended),
|
||||
ended_sessions=sum(1 for sess in ordered if sess.ended),
|
||||
pending_reviews=pending_reviews,
|
||||
closed_reviews=closed_reviews,
|
||||
summary=growth,
|
||||
points=growth.points,
|
||||
stage_breakdown=_stage_breakdown(ordered),
|
||||
sessions=summaries,
|
||||
message=f"{growth.learner_label}의 실제 회기 {len(ordered)}건을 오래된 순서로 표시합니다.",
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/sessions/{session_id}/review-status",
|
||||
response_model=TeacherSessionReviewStatusResponse,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue