세션 계약과 메모리 경계 보강

This commit is contained in:
Yun Chan 2026-06-28 23:52:18 +09:00
parent 391639c1de
commit 2bb052f624
12 changed files with 836 additions and 116 deletions

View file

@ -11,27 +11,26 @@ import re
from collections import Counter
from dataclasses import dataclass
from datetime import datetime
from typing import Literal, Optional
from typing import Literal, Optional, cast
from pydantic import BaseModel, Field
from . import turn_runtime
from .config import settings
from .services import session_metrics
from .stage_contract import (
ReviewPhaseKey,
StageLabel,
review_phase_key,
stage_label as _normalize_stage_label,
stage_label_or_none,
)
from .store import InProcSession, TurnRecord
StageLabel = Literal["라포", "탐색", "개입", "정리"]
WorksheetSpeaker = Literal["learner", "client"]
WorksheetItemSpec = tuple[str, str, list[str], WorksheetSpeaker | None]
WorksheetSectionSpec = tuple[str, str, list[WorksheetItemSpec]]
LEARNER_VISIBLE_AI_ROLE = "counselor"
_PHASE_KEY_BY_LABEL = {
"라포": "rapport",
"탐색": "explore",
"개입": "intervene",
"정리": "closing",
}
class LearnerSessionSummary(BaseModel):
@ -204,8 +203,8 @@ class ReviewTurn(BaseModel):
class ReviewPhaseSegment(BaseModel):
key: str
label: str
key: ReviewPhaseKey
label: StageLabel
weight: float
@ -330,6 +329,9 @@ class SessionTeacherReviewStatus(BaseModel):
reviewerId: str | None = None
reviewedAt: str | None = None
updatedAt: str | None = None
worksheetStatus: Literal["pending", "approved", "changes_requested", "rejected"] = "pending"
worksheetNote: str = ""
worksheetReviewedAt: str | None = None
class SessionReviewResponse(BaseModel):
@ -338,7 +340,7 @@ class SessionReviewResponse(BaseModel):
date: str
durationLabel: str
durationSeconds: int
reachedPhase: str
reachedPhase: StageLabel
sessionSignal: str
supervisorState: str
supervisorName: str
@ -385,8 +387,8 @@ class SessionReviewReadInput:
now_ts: float | None = None
def stage_label(stage: object) -> str:
return turn_runtime.stage_label(stage)
def stage_label(stage: object) -> StageLabel:
return cast(StageLabel, _normalize_stage_label(stage))
def iso(ts: float | None) -> str | None:
@ -658,7 +660,7 @@ def _client_name(raw: str) -> str:
return name or raw.strip() or "내담자"
def _review_summary(*, client_name: str, reached_phase: str, turns: list[ReviewTurn]) -> str:
def _review_summary(*, client_name: str, reached_phase: StageLabel, turns: list[ReviewTurn]) -> str:
if not turns:
return (
"아직 실제 발화가 없어 리뷰를 만들 수 없습니다. 회기를 진행한 뒤 종료하면 "
@ -674,11 +676,11 @@ def _review_summary(*, client_name: str, reached_phase: str, turns: list[ReviewT
)
def _phase_segments(stage_labels: list[str]) -> list[ReviewPhaseSegment]:
def _phase_segments(stage_labels: list[StageLabel]) -> list[ReviewPhaseSegment]:
counts = Counter(stage_labels)
return [
ReviewPhaseSegment(
key=_PHASE_KEY_BY_LABEL.get(label, label),
key=review_phase_key(label),
label=label,
weight=float(count),
)
@ -959,6 +961,15 @@ def saved_case_worksheet_from_payload(payload: dict[str, object] | None) -> Revi
return worksheet.model_copy(update={"status": "saved_by_learner"})
def _worksheet_review_status_value(
value: object,
) -> Literal["pending", "approved", "changes_requested", "rejected"]:
raw = str(value or "pending")
if raw in {"approved", "changes_requested", "rejected"}:
return raw # type: ignore[return-value]
return "pending"
def _worksheet_share_highlights(worksheet: ReviewCaseWorksheet, *, limit: int = 4) -> list[dict[str, str]]:
highlights: list[dict[str, str]] = []
for section in worksheet.sections:
@ -1342,6 +1353,9 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
reviewerId=str(review_status.get("reviewer_id") or "") or None,
reviewedAt=str(review_status.get("reviewed_at") or "") or None,
updatedAt=str(review_status.get("updated_at") or "") or None,
worksheetStatus=_worksheet_review_status_value(review_status.get("worksheet_status")),
worksheetNote=str(review_status.get("worksheet_note") or ""),
worksheetReviewedAt=str(review_status.get("worksheet_reviewed_at") or "") or None,
)
return SessionReviewResponse(