개선관리 요구사항과 Google 로그인을 완료
This commit is contained in:
parent
cc0a15b7c6
commit
2a39636163
112 changed files with 10166 additions and 527 deletions
|
|
@ -11,7 +11,7 @@ import re
|
|||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Literal, Optional, cast
|
||||
from typing import Any, Callable, Literal, Optional, cast
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
|
@ -36,6 +36,10 @@ MISSING_SESSION_EVALUATION_GRACE_SECONDS = 30.0
|
|||
MISSING_SESSION_EVALUATION_ERROR = (
|
||||
"회기말 평가가 제한 시간 이후에도 저장되지 않았습니다. AI 평가 재시도가 필요합니다."
|
||||
)
|
||||
TRAINING_EXPOSURE_VERSION = "training-exposure-dominant-share.v1"
|
||||
TRAINING_EXPOSURE_MIN_COMPLETED = 4
|
||||
TRAINING_EXPOSURE_ATTENTION_THRESHOLD = 0.75
|
||||
FIRST_SESSION_CHECKLIST_VERSION = "first-session-rapport-open-question.v1"
|
||||
|
||||
|
||||
class LearnerSessionSummary(BaseModel):
|
||||
|
|
@ -51,6 +55,7 @@ class LearnerSessionSummary(BaseModel):
|
|||
started_at: str
|
||||
ended_at: str | None = None
|
||||
review_ready: bool = False
|
||||
learner_feedback_enabled: bool = True
|
||||
archived: bool = False
|
||||
archived_at: str | None = None
|
||||
|
||||
|
|
@ -111,6 +116,23 @@ class LearnerDashboardPersonaProgress(BaseModel):
|
|||
rapport_percent: int = 0
|
||||
|
||||
|
||||
class LearnerDashboardTrainingExposure(BaseModel):
|
||||
version: str = TRAINING_EXPOSURE_VERSION
|
||||
status: Literal["insufficient", "attention", "balanced"] = "insufficient"
|
||||
label: Literal["판정 근거 부족", "훈련 집중 주의", "균형"] = "판정 근거 부족"
|
||||
completed_sessions: int = 0
|
||||
minimum_completed_sessions: int = TRAINING_EXPOSURE_MIN_COMPLETED
|
||||
attention_threshold: float = TRAINING_EXPOSURE_ATTENTION_THRESHOLD
|
||||
dominant_persona_code: str | None = None
|
||||
dominant_persona_name: str | None = None
|
||||
dominant_sessions: int = 0
|
||||
dominant_share: float | None = None
|
||||
definition: str = (
|
||||
"종료 회기의 페르소나별 최다 노출 비중을 보여 주는 투명한 훈련 노출 지표이며, "
|
||||
"공정성 평가나 임상진단이 아닙니다."
|
||||
)
|
||||
|
||||
|
||||
class LearnerDashboardAchievement(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
|
|
@ -137,6 +159,9 @@ class LearnerDashboardResponse(BaseModel):
|
|||
overview: LearnerDashboardOverview
|
||||
growth: LearnerDashboardGrowth
|
||||
persona_progress: list[LearnerDashboardPersonaProgress] = Field(default_factory=list)
|
||||
training_exposure: LearnerDashboardTrainingExposure = Field(
|
||||
default_factory=LearnerDashboardTrainingExposure
|
||||
)
|
||||
achievements: list[LearnerDashboardAchievement] = Field(default_factory=list)
|
||||
recent_feedback: list[LearnerDashboardFeedbackItem] = Field(default_factory=list)
|
||||
message: str
|
||||
|
|
@ -230,6 +255,8 @@ class SessionDetailTurn(BaseModel):
|
|||
class SessionDetailResponse(BaseModel):
|
||||
session_id: str
|
||||
case_id: str
|
||||
persona_id: str | None = None
|
||||
persona_version: int | None = None
|
||||
persona_code: str
|
||||
persona_name: str
|
||||
theory_mode: str
|
||||
|
|
@ -240,6 +267,7 @@ class SessionDetailResponse(BaseModel):
|
|||
ended_at: str | None = None
|
||||
turns: list[SessionDetailTurn] = Field(default_factory=list)
|
||||
review_ready: bool = False
|
||||
learner_feedback_enabled: bool = True
|
||||
# 시간 기반 회기(2026-07-13 회의 P1): 새로고침 복원 시 타이머·목표 표시의 기준.
|
||||
goal_stages: list[StageLabel] = Field(default_factory=list)
|
||||
duration_limit_seconds: int = 0
|
||||
|
|
@ -306,6 +334,31 @@ class ReviewRubricRow(BaseModel):
|
|||
freq: str
|
||||
|
||||
|
||||
class ReviewFirstSessionChecklistEvidence(BaseModel):
|
||||
turnId: str
|
||||
turnSeq: int
|
||||
quote: str
|
||||
|
||||
|
||||
class ReviewFirstSessionChecklistCriterion(BaseModel):
|
||||
criterionId: str
|
||||
label: str
|
||||
description: str
|
||||
status: Literal["met", "not_observed"]
|
||||
evidenceTurns: list[ReviewFirstSessionChecklistEvidence] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ReviewFirstSessionChecklist(BaseModel):
|
||||
version: str = FIRST_SESSION_CHECKLIST_VERSION
|
||||
applicable: bool = True
|
||||
status: Literal["ready", "not_applicable"] = "ready"
|
||||
title: str = "첫 회기 라포·개방질문 체크리스트"
|
||||
note: str = (
|
||||
"축어록에서 관찰 가능한 대화 행동을 규칙 기반으로 점검하며, 임상 평가나 성적 판정이 아닙니다."
|
||||
)
|
||||
criteria: list[ReviewFirstSessionChecklistCriterion] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ReviewPoint(BaseModel):
|
||||
title: str
|
||||
body: str
|
||||
|
|
@ -421,6 +474,7 @@ class SessionTeacherReviewStatus(BaseModel):
|
|||
|
||||
class SessionReviewResponse(BaseModel):
|
||||
session_id: str
|
||||
sessionNo: int = Field(ge=1)
|
||||
client: ReviewClient
|
||||
date: str
|
||||
durationLabel: str
|
||||
|
|
@ -437,6 +491,7 @@ class SessionReviewResponse(BaseModel):
|
|||
counselorBaseline: list[ReviewValencePoint] = Field(default_factory=list)
|
||||
turns: list[ReviewTurn] = Field(default_factory=list)
|
||||
rubric: list[ReviewRubricRow] = Field(default_factory=list)
|
||||
firstSessionChecklist: ReviewFirstSessionChecklist | None = None
|
||||
goodMoments: list[ReviewPoint] = Field(default_factory=list)
|
||||
growthPoints: list[ReviewPoint] = Field(default_factory=list)
|
||||
caseWorksheet: ReviewCaseWorksheet = Field(default_factory=ReviewCaseWorksheet)
|
||||
|
|
@ -446,6 +501,7 @@ class SessionReviewResponse(BaseModel):
|
|||
pdfExportUrl: Optional[str] = None
|
||||
degraded: bool = True
|
||||
reviewReady: bool = False
|
||||
learnerFeedbackEnabled: bool = True
|
||||
teacherReview: SessionTeacherReviewStatus | None = None
|
||||
|
||||
|
||||
|
|
@ -469,6 +525,8 @@ class SessionReviewReadInput:
|
|||
saved_worksheet_payload: dict[str, object] | None = None
|
||||
include_teacher_review: bool = False
|
||||
teacher_review_record: dict[str, object] | None = None
|
||||
learner_feedback_enabled: bool = True
|
||||
expose_learner_feedback: bool = True
|
||||
now_ts: float | None = None
|
||||
|
||||
|
||||
|
|
@ -494,6 +552,7 @@ def learner_summary(
|
|||
sess: InProcSession,
|
||||
*,
|
||||
review_ready: bool = False,
|
||||
learner_feedback_enabled: bool | None = None,
|
||||
archived: bool = False,
|
||||
archived_at: str | None = None,
|
||||
) -> LearnerSessionSummary:
|
||||
|
|
@ -513,6 +572,11 @@ def learner_summary(
|
|||
started_at=iso(sess.created_at) or "",
|
||||
ended_at=iso(sess.ended_at),
|
||||
review_ready=review_ready,
|
||||
learner_feedback_enabled=(
|
||||
sess.learner_feedback_enabled
|
||||
if learner_feedback_enabled is None
|
||||
else learner_feedback_enabled
|
||||
),
|
||||
archived=archived,
|
||||
archived_at=archived_at,
|
||||
)
|
||||
|
|
@ -591,6 +655,7 @@ def dashboard_growth(sessions: list[InProcSession]) -> LearnerDashboardGrowth:
|
|||
def dashboard_persona_progress(
|
||||
sessions: list[InProcSession],
|
||||
review_ready: dict[str, bool],
|
||||
learner_feedback_enabled: dict[str, bool] | None = None,
|
||||
) -> list[LearnerDashboardPersonaProgress]:
|
||||
grouped: dict[str, list[InProcSession]] = {}
|
||||
for sess in sessions:
|
||||
|
|
@ -600,8 +665,17 @@ def dashboard_persona_progress(
|
|||
for persona_code, items in grouped.items():
|
||||
ordered = sorted(items, key=session_metrics.session_activity_time)
|
||||
latest = ordered[-1]
|
||||
metric_sessions = (
|
||||
ordered
|
||||
if learner_feedback_enabled is None
|
||||
else [
|
||||
sess
|
||||
for sess in ordered
|
||||
if learner_feedback_enabled.get(sess.session_id, True)
|
||||
]
|
||||
)
|
||||
metrics = session_metrics.build_learner_growth(
|
||||
ordered,
|
||||
metric_sessions,
|
||||
learner_label=lambda _learner_id: "나",
|
||||
limit=1,
|
||||
)
|
||||
|
|
@ -632,6 +706,45 @@ def dashboard_persona_progress(
|
|||
)
|
||||
|
||||
|
||||
def dashboard_training_exposure(
|
||||
sessions: list[InProcSession],
|
||||
) -> LearnerDashboardTrainingExposure:
|
||||
"""Compute a transparent practice-exposure signal from ended sessions only."""
|
||||
|
||||
completed = [sess for sess in sessions if sess.ended]
|
||||
counts = Counter(sess.persona_code for sess in completed)
|
||||
if not counts:
|
||||
return LearnerDashboardTrainingExposure()
|
||||
|
||||
dominant_code, dominant_count = sorted(
|
||||
counts.items(),
|
||||
key=lambda item: (-item[1], item[0]),
|
||||
)[0]
|
||||
dominant_session = next(
|
||||
sess for sess in completed if sess.persona_code == dominant_code
|
||||
)
|
||||
total = len(completed)
|
||||
share = dominant_count / total
|
||||
if total < TRAINING_EXPOSURE_MIN_COMPLETED:
|
||||
status: Literal["insufficient", "attention", "balanced"] = "insufficient"
|
||||
label: Literal["판정 근거 부족", "훈련 집중 주의", "균형"] = "판정 근거 부족"
|
||||
elif share >= TRAINING_EXPOSURE_ATTENTION_THRESHOLD:
|
||||
status = "attention"
|
||||
label = "훈련 집중 주의"
|
||||
else:
|
||||
status = "balanced"
|
||||
label = "균형"
|
||||
return LearnerDashboardTrainingExposure(
|
||||
status=status,
|
||||
label=label,
|
||||
completed_sessions=total,
|
||||
dominant_persona_code=dominant_code,
|
||||
dominant_persona_name=dominant_session.persona.display_name,
|
||||
dominant_sessions=dominant_count,
|
||||
dominant_share=round(share, 4),
|
||||
)
|
||||
|
||||
|
||||
def _achievement_state(done: bool, available: bool) -> Literal["done", "available", "locked"]:
|
||||
if done:
|
||||
return "done"
|
||||
|
|
@ -705,11 +818,14 @@ def session_detail(
|
|||
sess: InProcSession,
|
||||
*,
|
||||
review_ready: bool = False,
|
||||
learner_feedback_enabled: bool | None = None,
|
||||
) -> SessionDetailResponse:
|
||||
turns = learner_visible_turns(sess)
|
||||
return SessionDetailResponse(
|
||||
session_id=sess.session_id,
|
||||
case_id=sess.case_id,
|
||||
persona_id=sess.persona_id,
|
||||
persona_version=sess.persona_version,
|
||||
persona_code=sess.persona_code,
|
||||
persona_name=sess.persona.display_name,
|
||||
theory_mode=sess.theory_mode,
|
||||
|
|
@ -729,6 +845,11 @@ def session_detail(
|
|||
for turn in turns
|
||||
],
|
||||
review_ready=review_ready,
|
||||
learner_feedback_enabled=(
|
||||
sess.learner_feedback_enabled
|
||||
if learner_feedback_enabled is None
|
||||
else learner_feedback_enabled
|
||||
),
|
||||
goal_stages=[stage_label(goal) for goal in (sess.goal_stages or [])],
|
||||
duration_limit_seconds=max(0, settings.session_duration_minutes) * 60,
|
||||
warning_before_end_seconds=max(0, settings.session_warning_minutes) * 60,
|
||||
|
|
@ -820,6 +941,87 @@ def _clip_text(text: str, limit: int = 180) -> str:
|
|||
return f"{compact[: max(0, limit - 1)].rstrip()}..."
|
||||
|
||||
|
||||
_FIRST_SESSION_RAPPORT_CUES = (
|
||||
"마음",
|
||||
"느껴",
|
||||
"느낌",
|
||||
"힘들",
|
||||
"이해",
|
||||
"그랬군",
|
||||
"그랬구나",
|
||||
"천천히",
|
||||
"버티",
|
||||
)
|
||||
_FIRST_SESSION_OPEN_QUESTION_WORDS = ("어떤", "어떻게", "무엇", "언제", "어디", "어느")
|
||||
_FIRST_SESSION_CLOSED_QUESTION_CUES = ("맞죠", "그렇죠", "아니에요?", "했나요?", "인가요?")
|
||||
|
||||
|
||||
def _first_session_evidence(
|
||||
turns: list[ReviewTurn],
|
||||
predicate: Callable[[str], bool],
|
||||
) -> list[ReviewFirstSessionChecklistEvidence]:
|
||||
evidence: list[ReviewFirstSessionChecklistEvidence] = []
|
||||
for index, turn in enumerate(turns, start=1):
|
||||
if turn.speaker != "learner" or not predicate(turn.text):
|
||||
continue
|
||||
evidence.append(
|
||||
ReviewFirstSessionChecklistEvidence(
|
||||
turnId=turn.id,
|
||||
turnSeq=index,
|
||||
quote=_clip_text(turn.text, 120),
|
||||
)
|
||||
)
|
||||
if len(evidence) >= 3:
|
||||
break
|
||||
return evidence
|
||||
|
||||
|
||||
def first_session_checklist(
|
||||
*,
|
||||
session_no: int,
|
||||
turns: list[ReviewTurn],
|
||||
) -> ReviewFirstSessionChecklist:
|
||||
"""Build a non-clinical, evidence-linked first-session behavior checklist."""
|
||||
|
||||
if session_no != 1:
|
||||
return ReviewFirstSessionChecklist(
|
||||
applicable=False,
|
||||
status="not_applicable",
|
||||
criteria=[],
|
||||
)
|
||||
|
||||
rapport_evidence = _first_session_evidence(
|
||||
turns,
|
||||
lambda text: any(cue in text for cue in _FIRST_SESSION_RAPPORT_CUES),
|
||||
)
|
||||
open_question_evidence = _first_session_evidence(
|
||||
turns,
|
||||
lambda text: (
|
||||
text.count("?") == 1
|
||||
and any(word in text for word in _FIRST_SESSION_OPEN_QUESTION_WORDS)
|
||||
and not any(cue in text for cue in _FIRST_SESSION_CLOSED_QUESTION_CUES)
|
||||
),
|
||||
)
|
||||
return ReviewFirstSessionChecklist(
|
||||
criteria=[
|
||||
ReviewFirstSessionChecklistCriterion(
|
||||
criterionId="first-session.rapport-reflection",
|
||||
label="라포를 위한 정서·경험 반영",
|
||||
description="내담자의 정서나 경험을 평가·조언보다 먼저 반영한 발화를 찾습니다.",
|
||||
status="met" if rapport_evidence else "not_observed",
|
||||
evidenceTurns=rapport_evidence,
|
||||
),
|
||||
ReviewFirstSessionChecklistCriterion(
|
||||
criterionId="first-session.open-question-one-focus",
|
||||
label="한 초점의 개방형 질문",
|
||||
description="한 번에 한 초점으로 탐색을 넓히는 개방형 질문을 찾습니다.",
|
||||
status="met" if open_question_evidence else "not_observed",
|
||||
evidenceTurns=open_question_evidence,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _point_title(text: str, fallback: str) -> str:
|
||||
compact = _clip_text(text, 72)
|
||||
for sep in (".", "。", "!", "?", "\n"):
|
||||
|
|
@ -1176,29 +1378,78 @@ def session_share_payload(review: SessionReviewResponse) -> dict[str, object]:
|
|||
}
|
||||
|
||||
|
||||
def _evaluation_payload(record: dict[str, object] | None) -> dict[str, object]:
|
||||
def _evaluation_payload(
|
||||
record: dict[str, object] | None,
|
||||
*,
|
||||
counselor_identity: str | None = None,
|
||||
client_identity: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
if not record:
|
||||
return {}
|
||||
payload = record.get("payload")
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
masked = _mask_payload_text_values(payload)
|
||||
masked = _mask_payload_text_values(
|
||||
payload,
|
||||
counselor_identity=counselor_identity,
|
||||
client_identity=client_identity,
|
||||
)
|
||||
return masked if isinstance(masked, dict) else {}
|
||||
|
||||
|
||||
def _mask_payload_text(value: object) -> str:
|
||||
return guardrail.mask_pii(str(value or "")).text_masked
|
||||
def _mask_payload_text(
|
||||
value: object,
|
||||
*,
|
||||
counselor_identity: str | None = None,
|
||||
client_identity: str | None = None,
|
||||
) -> str:
|
||||
return guardrail.mask_role_identities(
|
||||
str(value or ""),
|
||||
counselor_identity=counselor_identity,
|
||||
client_identity=client_identity,
|
||||
synthetic_generated=True,
|
||||
).text_masked
|
||||
|
||||
|
||||
def _mask_payload_text_values(value: Any) -> Any:
|
||||
def _mask_payload_text_values(
|
||||
value: Any,
|
||||
*,
|
||||
counselor_identity: str | None = None,
|
||||
client_identity: str | None = None,
|
||||
) -> Any:
|
||||
if isinstance(value, str):
|
||||
return _mask_payload_text(value)
|
||||
return _mask_payload_text(
|
||||
value,
|
||||
counselor_identity=counselor_identity,
|
||||
client_identity=client_identity,
|
||||
)
|
||||
if isinstance(value, dict):
|
||||
return {str(key): _mask_payload_text_values(child) for key, child in value.items()}
|
||||
return {
|
||||
str(key): _mask_payload_text_values(
|
||||
child,
|
||||
counselor_identity=counselor_identity,
|
||||
client_identity=client_identity,
|
||||
)
|
||||
for key, child in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [_mask_payload_text_values(child) for child in value]
|
||||
return [
|
||||
_mask_payload_text_values(
|
||||
child,
|
||||
counselor_identity=counselor_identity,
|
||||
client_identity=client_identity,
|
||||
)
|
||||
for child in value
|
||||
]
|
||||
if isinstance(value, tuple):
|
||||
return [_mask_payload_text_values(child) for child in value]
|
||||
return [
|
||||
_mask_payload_text_values(
|
||||
child,
|
||||
counselor_identity=counselor_identity,
|
||||
client_identity=client_identity,
|
||||
)
|
||||
for child in value
|
||||
]
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -1614,6 +1865,7 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
|
|||
sess = read_input.session
|
||||
visible_turns = learner_visible_turns(sess)
|
||||
hidden_turns = len(visible_turns) != len(sess.turns)
|
||||
feedback_hidden = hidden_turns or not read_input.expose_learner_feedback
|
||||
|
||||
end_ts = sess.ended_at or read_input.now_ts or datetime.now().timestamp()
|
||||
duration_seconds = max(0, int(round(end_ts - sess.created_at)))
|
||||
|
|
@ -1628,17 +1880,29 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
|
|||
|
||||
now_ts = read_input.now_ts or datetime.now().timestamp()
|
||||
evaluation_record = read_input.evaluation_record
|
||||
if evaluation_record is None and not hidden_turns:
|
||||
if evaluation_record is None and not feedback_hidden:
|
||||
evaluation_record = _missing_session_evaluation_record(
|
||||
sess,
|
||||
has_visible_turns=bool(visible_turns),
|
||||
now_ts=now_ts,
|
||||
)
|
||||
evaluation_payload = {} if hidden_turns else _evaluation_payload(evaluation_record)
|
||||
evaluation_status = (
|
||||
"" if hidden_turns else str(evaluation_record.get("status") or "") if evaluation_record else ""
|
||||
evaluation_payload = (
|
||||
{}
|
||||
if feedback_hidden
|
||||
else _evaluation_payload(
|
||||
evaluation_record,
|
||||
counselor_identity=sess.learner_label,
|
||||
client_identity=sess.persona.display_name,
|
||||
)
|
||||
)
|
||||
evaluation_ready = not hidden_turns and evaluation_status == "ready"
|
||||
evaluation_status = (
|
||||
""
|
||||
if feedback_hidden
|
||||
else str(evaluation_record.get("status") or "")
|
||||
if evaluation_record
|
||||
else ""
|
||||
)
|
||||
evaluation_ready = not feedback_hidden and evaluation_status == "ready"
|
||||
|
||||
first_turn_ts = visible_turns[0].created_at if visible_turns else sess.created_at
|
||||
turns: list[ReviewTurn] = []
|
||||
|
|
@ -1646,25 +1910,35 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
|
|||
speaker: Literal["learner", "client"] = (
|
||||
"learner" if turn.speaker == "counselor" else "client"
|
||||
)
|
||||
turn_eval = turn.evaluation if (speaker == "learner" and not hidden_turns) else None
|
||||
safe_turn_text = guardrail.mask_role_identities(
|
||||
turn.text_masked,
|
||||
counselor_identity=sess.learner_label,
|
||||
client_identity=sess.persona.display_name,
|
||||
synthetic_generated=speaker == "client",
|
||||
).text_masked
|
||||
turn_eval = turn.evaluation if (speaker == "learner" and not feedback_hidden) else None
|
||||
turns.append(
|
||||
ReviewTurn(
|
||||
id=f"t{index + 1}",
|
||||
turn_id=turn.turn_id,
|
||||
ts=_offset_label(turn.created_at - first_turn_ts),
|
||||
speaker=speaker,
|
||||
who="학습자" if speaker == "learner" else client_name,
|
||||
text=turn.text_masked,
|
||||
who="상담자" if speaker == "learner" else "내담자",
|
||||
text=safe_turn_text,
|
||||
techniques=_review_techniques_from_turn_eval(turn_eval),
|
||||
nonverbal=_review_nonverbal_events(turn) if speaker == "learner" else [],
|
||||
note=_review_note_from_turn_eval(turn_eval, turn.text_masked),
|
||||
nonverbal=(
|
||||
_review_nonverbal_events(turn)
|
||||
if speaker == "learner" and not feedback_hidden
|
||||
else []
|
||||
),
|
||||
note=_review_note_from_turn_eval(turn_eval, safe_turn_text),
|
||||
)
|
||||
)
|
||||
|
||||
# 감정 밸런스 타임라인 — 학습자 기준선 + 내담자 정서가(비공개 턴 존재 시 비산출)
|
||||
counselor_baseline: list[ReviewValencePoint] = []
|
||||
client_valence: list[ReviewValencePoint] = []
|
||||
if not hidden_turns:
|
||||
if not feedback_hidden:
|
||||
counselor_baseline = counselor_baseline_points(
|
||||
visible_turns,
|
||||
first_turn_ts=first_turn_ts,
|
||||
|
|
@ -1690,7 +1964,7 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
|
|||
session_signal = "진행 중"
|
||||
|
||||
transcript_summary = _review_summary(
|
||||
client_name=client_name,
|
||||
client_name="내담자",
|
||||
reached_phase=reached_phase,
|
||||
turns=turns,
|
||||
)
|
||||
|
|
@ -1713,9 +1987,11 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
|
|||
growth_points = _intent_deviation_points(evaluation_payload.get("intent_deviations"))
|
||||
next_line = _next_line_from_evaluation(evaluation_payload)
|
||||
|
||||
client_feedback = _latest_client_feedback(turns)
|
||||
review_degraded = bool(turns) and not evaluation_ready
|
||||
if evaluation_ready:
|
||||
client_feedback = None if feedback_hidden else _latest_client_feedback(turns)
|
||||
review_degraded = bool(turns) and not evaluation_ready and not feedback_hidden
|
||||
if not read_input.expose_learner_feedback:
|
||||
supervisor_state = "피드백 비활성"
|
||||
elif evaluation_ready:
|
||||
supervisor_state = "평가 완료"
|
||||
elif evaluation_status == "error":
|
||||
supervisor_state = "평가 실패"
|
||||
|
|
@ -1724,18 +2000,37 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
|
|||
else:
|
||||
supervisor_state = "기록 대기"
|
||||
|
||||
summary = _review_summary_from_evaluation(
|
||||
fallback=transcript_summary,
|
||||
evaluation_record=None if hidden_turns else evaluation_record,
|
||||
payload=evaluation_payload,
|
||||
summary = (
|
||||
"관리자가 이 계정의 AI 학습 피드백을 비활성화했습니다. "
|
||||
"회기 축어록과 가상 내담자 응답은 그대로 보존됩니다."
|
||||
if not read_input.expose_learner_feedback
|
||||
else _review_summary_from_evaluation(
|
||||
fallback=transcript_summary,
|
||||
evaluation_record=None if hidden_turns else evaluation_record,
|
||||
payload=evaluation_payload,
|
||||
)
|
||||
)
|
||||
if evaluation_record and not hidden_turns and not read_input.evaluation_durable:
|
||||
if evaluation_record and not feedback_hidden and not read_input.evaluation_durable:
|
||||
summary += " 현재 평가는 런타임 캐시에서 복원되었습니다."
|
||||
|
||||
saved_worksheet = saved_case_worksheet_from_payload(
|
||||
read_input.saved_worksheet_payload
|
||||
)
|
||||
generated_worksheet = case_worksheet_from_turns(turns)
|
||||
case_worksheet = (
|
||||
saved_case_worksheet_from_payload(read_input.saved_worksheet_payload)
|
||||
or generated_worksheet
|
||||
if not read_input.expose_learner_feedback:
|
||||
case_worksheet = saved_worksheet or ReviewCaseWorksheet(
|
||||
status="empty",
|
||||
generatedBy="learner input only",
|
||||
limitations=[
|
||||
"AI 자동 초안은 표시하지 않습니다. 저장한 학습자 워크시트는 그대로 보존됩니다."
|
||||
],
|
||||
)
|
||||
else:
|
||||
case_worksheet = saved_worksheet or generated_worksheet
|
||||
first_session_review = (
|
||||
None
|
||||
if feedback_hidden
|
||||
else first_session_checklist(session_no=sess.session_no, turns=turns)
|
||||
)
|
||||
|
||||
teacher_review: SessionTeacherReviewStatus | None = None
|
||||
|
|
@ -1757,6 +2052,7 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
|
|||
|
||||
return SessionReviewResponse(
|
||||
session_id=sess.session_id,
|
||||
sessionNo=sess.session_no,
|
||||
client=ReviewClient(
|
||||
name=client_name,
|
||||
initial=client_initial,
|
||||
|
|
@ -1777,6 +2073,7 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
|
|||
counselorBaseline=counselor_baseline,
|
||||
turns=turns,
|
||||
rubric=rubric,
|
||||
firstSessionChecklist=first_session_review,
|
||||
goodMoments=good_moments,
|
||||
growthPoints=growth_points,
|
||||
caseWorksheet=case_worksheet,
|
||||
|
|
@ -1786,5 +2083,6 @@ def build_session_review(read_input: SessionReviewReadInput) -> SessionReviewRes
|
|||
pdfExportUrl=None,
|
||||
degraded=review_degraded,
|
||||
reviewReady=evaluation_ready,
|
||||
learnerFeedbackEnabled=read_input.learner_feedback_enabled,
|
||||
teacherReview=teacher_review,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue