전 저장소 리팩터링과 SSOT 정비
This commit is contained in:
parent
14ecbd4e7d
commit
3dfddcac6f
173 changed files with 19679 additions and 6952 deletions
|
|
@ -16,13 +16,13 @@ from typing import Any, Literal, Optional, cast
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
from .config import settings
|
||||
from .services import guardrail, session_metrics
|
||||
from .services import guardrail, session_metrics, state_machine
|
||||
from .stage_contract import (
|
||||
ReviewPhaseKey,
|
||||
StageLabel,
|
||||
review_phase_key,
|
||||
stage_label as _normalize_stage_label,
|
||||
stage_label_or_none,
|
||||
stage_label_or_none as stage_label_or_none,
|
||||
)
|
||||
from .store import InProcSession, TurnRecord
|
||||
|
||||
|
|
@ -106,6 +106,8 @@ class LearnerDashboardPersonaProgress(BaseModel):
|
|||
latest_stage: StageLabel | None = None
|
||||
latest_score: float | None = None
|
||||
trend: str = "insufficient"
|
||||
# P2 누적 게이지: 이 페르소나와 쌓아온 라포 누적(전 주기 임계 기준 %).
|
||||
rapport_percent: int = 0
|
||||
|
||||
|
||||
class LearnerDashboardAchievement(BaseModel):
|
||||
|
|
@ -147,6 +149,75 @@ class SessionArchiveResponse(BaseModel):
|
|||
session: LearnerSessionSummary
|
||||
|
||||
|
||||
class SessionStageProgress(BaseModel):
|
||||
"""단계별 누적 게이지(P2). 상태머신 결정론 수치의 파생값만 담는다."""
|
||||
|
||||
stage: StageLabel
|
||||
percent: int = 0
|
||||
achieved: bool = False
|
||||
is_goal: bool = False
|
||||
|
||||
|
||||
class SessionProgress(BaseModel):
|
||||
"""회기 진행 상세(P2). 내부 원값 명칭 대신 학습자-안전 파생 %만 노출한다."""
|
||||
|
||||
stages: list[SessionStageProgress] = Field(default_factory=list)
|
||||
rapport_percent: int = 0
|
||||
rapport_delta_percent: int = 0
|
||||
resistance_percent: int = 0
|
||||
openness_percent: int = 0
|
||||
|
||||
|
||||
# 전 주기(라포→정리) 기준 라포 만점: 마지막 전이 임계(개입→정리)를 100으로 본다.
|
||||
_FULL_CYCLE_RAPPORT = max(state_machine.STAGE_ADVANCE_RAPPORT.values())
|
||||
|
||||
|
||||
def _rapport_percent(credit: float) -> int:
|
||||
if _FULL_CYCLE_RAPPORT <= 0:
|
||||
return 0
|
||||
return max(0, min(100, int(round(float(credit or 0.0) / _FULL_CYCLE_RAPPORT * 100))))
|
||||
|
||||
|
||||
def build_session_progress(
|
||||
state: state_machine.SessionState,
|
||||
*,
|
||||
prev_rapport_credit: float = 0.0,
|
||||
goal_stages: list[str] | None = None,
|
||||
) -> SessionProgress:
|
||||
"""상태머신 수치 → 단계 누적 게이지/상세 수치 파생(순수함수, LLM 미경유).
|
||||
|
||||
누적성: rapport_credit 은 회기 간 ×0.7 이월되므로 다음 회기 게이지는 이월분에서 시작한다.
|
||||
"""
|
||||
goals = {stage_label(goal) for goal in (goal_stages or [])}
|
||||
order = state_machine.STAGE_ORDER
|
||||
cur_idx = order.index(state.stage)
|
||||
stages: list[SessionStageProgress] = []
|
||||
for i, st in enumerate(order):
|
||||
label = stage_label(st)
|
||||
if i < cur_idx:
|
||||
pct, achieved = 100, True
|
||||
elif i > cur_idx:
|
||||
pct, achieved = 0, False
|
||||
elif st is state_machine.Stage.CLOSE:
|
||||
pct, achieved = 100, True
|
||||
else:
|
||||
need = state_machine.STAGE_ADVANCE_RAPPORT.get(st, 1.0)
|
||||
ratio = min(1.0, float(state.rapport_credit or 0.0) / need) if need > 0 else 0.0
|
||||
pct, achieved = min(99, int(round(ratio * 99))), False
|
||||
stages.append(
|
||||
SessionStageProgress(stage=label, percent=pct, achieved=achieved, is_goal=label in goals)
|
||||
)
|
||||
rapport_pct = _rapport_percent(state.rapport_credit)
|
||||
prev_pct = _rapport_percent(prev_rapport_credit)
|
||||
return SessionProgress(
|
||||
stages=stages,
|
||||
rapport_percent=rapport_pct,
|
||||
rapport_delta_percent=max(0, rapport_pct - prev_pct),
|
||||
resistance_percent=max(0, min(100, int(round(float(state.resistance or 0.0) * 100)))),
|
||||
openness_percent=max(0, min(100, int(round(float(state.effective_openness or 0.0) * 100)))),
|
||||
)
|
||||
|
||||
|
||||
class SessionDetailTurn(BaseModel):
|
||||
turn_seq: int
|
||||
speaker: Literal["learner", "client"]
|
||||
|
|
@ -168,6 +239,12 @@ class SessionDetailResponse(BaseModel):
|
|||
ended_at: str | None = None
|
||||
turns: list[SessionDetailTurn] = Field(default_factory=list)
|
||||
review_ready: bool = False
|
||||
# 시간 기반 회기(2026-07-13 회의 P1): 새로고침 복원 시 타이머·목표 표시의 기준.
|
||||
goal_stages: list[StageLabel] = Field(default_factory=list)
|
||||
duration_limit_seconds: int = 0
|
||||
warning_before_end_seconds: int = 0
|
||||
# P2 단계 누적 게이지·상세 수치.
|
||||
progress: SessionProgress | None = None
|
||||
|
||||
|
||||
class ReviewClient(BaseModel):
|
||||
|
|
@ -537,6 +614,7 @@ def dashboard_persona_progress(
|
|||
latest_stage=stage_label(latest.state.stage),
|
||||
latest_score=growth.latest_score if growth else None,
|
||||
trend=growth.trend if growth else "insufficient",
|
||||
rapport_percent=_rapport_percent(latest.state.rapport_credit),
|
||||
)
|
||||
)
|
||||
return sorted(
|
||||
|
|
@ -643,6 +721,14 @@ def session_detail(
|
|||
for turn in turns
|
||||
],
|
||||
review_ready=review_ready,
|
||||
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,
|
||||
progress=build_session_progress(
|
||||
sess.state,
|
||||
prev_rapport_credit=sess.prev_rapport_credit,
|
||||
goal_stages=list(sess.goal_stages or []),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue