개선관리 요구사항과 Google 로그인을 완료

This commit is contained in:
Yun Chan 2026-08-28 16:07:09 +09:00
parent cc0a15b7c6
commit 2a39636163
112 changed files with 10166 additions and 527 deletions

View file

@ -30,6 +30,7 @@ from ..runtime_policy import require_runtime_fallback_allowed
from ..session_evaluation_input import enriched_masked_turns
from ..services import (
evaluator,
feedback_policy,
guardrail,
live_coach,
memory,
@ -67,6 +68,7 @@ from ..session_read_model import (
dashboard_growth as _dashboard_growth,
dashboard_overview as _dashboard_overview,
dashboard_persona_progress as _dashboard_persona_progress,
dashboard_training_exposure as _dashboard_training_exposure,
iso as _iso,
learner_summary as _learner_summary,
learner_visible_turns as _learner_visible_turns,
@ -112,6 +114,8 @@ class SessionStartRequest(BaseModel):
class SessionStartResponse(BaseModel):
session_id: str
case_id: str
persona_id: str
persona_version: int
session_no: int
stage: StageLabel
effective_openness: float
@ -122,6 +126,7 @@ class SessionStartResponse(BaseModel):
# 시간 기반 회기 종료 계약(회의 P1): 프론트 타이머·10분 전 알람의 기준값.
duration_limit_seconds: int = 0
warning_before_end_seconds: int = 0
learner_feedback_enabled: bool = True
class TurnRequest(BaseModel):
@ -308,6 +313,8 @@ async def _retrieve_live_coach_grounding(
source_type=source_type or None,
version=source_version or None,
citation=citation or None,
license_class=chunk.license_class,
external_llm_ok=chunk.external_llm_ok,
summary=body[:500],
)
)
@ -525,6 +532,7 @@ async def _prepare_turn_context(
card=sess.persona,
state=sess.state,
learner_text=learner_text,
learner_identity=sess.learner_label,
memory=orchestrator.TurnMemory(
recall_summary=recall.recall_summary,
pinned_facts=recall.pinned_facts,
@ -908,7 +916,11 @@ async def _generate_and_save_session_evaluation(sess: InProcSession) -> None:
return
timeout_seconds = _session_evaluation_timeout_seconds()
enriched = enriched_masked_turns(sess.masked_turns())
enriched = enriched_masked_turns(
sess.masked_turns(),
counselor_identity=sess.learner_label,
client_identity=sess.persona.display_name,
)
try:
result = await asyncio.wait_for(
@ -928,6 +940,8 @@ async def _generate_and_save_session_evaluation(sess: InProcSession) -> None:
session_id=sess.session_id,
learner_id=sess.learner_id,
result=result,
counselor_identity=sess.learner_label,
client_identity=sess.persona.display_name,
)
saved = await session_persistence.save_session_evaluation(write)
if not saved:
@ -960,6 +974,8 @@ async def _generate_and_save_session_evaluation(sess: InProcSession) -> None:
scope="session_end",
stage=_stage_label(sess.state.stage),
error=message,
counselor_identity=sess.learner_label,
client_identity=sess.persona.display_name,
)
saved = await session_persistence.save_session_evaluation(write)
if not saved:
@ -978,6 +994,8 @@ async def _generate_and_save_session_evaluation(sess: InProcSession) -> None:
scope="session_end",
stage=_stage_label(sess.state.stage),
error=exc,
counselor_identity=sess.learner_label,
client_identity=sess.persona.display_name,
)
saved = await session_persistence.save_session_evaluation(write)
if not saved:
@ -1124,6 +1142,8 @@ async def _load_learner_sessions(
async def _review_ready(sess: InProcSession, principal: Principal) -> bool:
if not feedback_policy.can_expose_principal_learner_feedback(sess, principal):
return False
turns = _learner_visible_turns(sess)
if not sess.ended or not turns:
return False
@ -1173,6 +1193,9 @@ async def _session_archive_response(
session=_learner_summary(
sess,
review_ready=await _review_ready(sess, principal),
learner_feedback_enabled=(
feedback_policy.effective_learner_feedback_enabled(sess, principal)
),
archived=archived,
archived_at=archived_at,
),
@ -1194,6 +1217,12 @@ async def list_learner_sessions(principal: CurrentPrincipal) -> LearnerSessionsR
_learner_summary(
sess,
review_ready=await _review_ready(sess, principal),
learner_feedback_enabled=(
feedback_policy.effective_learner_feedback_enabled(
sess,
principal,
)
),
archived=archive_record is not None,
archived_at=_iso(float(archived_at))
if isinstance(archived_at, (int, float))
@ -1213,10 +1242,22 @@ async def learner_dashboard(principal: CurrentPrincipal) -> LearnerDashboardResp
principal = _ensure_learner(principal)
sessions, durable = await _load_learner_sessions(
principal,
include_turn_evaluation=True,
include_turn_evaluation=principal.learner_feedback_enabled,
)
review_ready = await _review_ready_map(sessions, principal)
archives = await _archive_map(sessions, principal)
learner_feedback_enabled = {
sess.session_id: feedback_policy.effective_learner_feedback_enabled(
sess,
principal,
)
for sess in sessions
}
feedback_sessions = [
sess
for sess in sessions
if learner_feedback_enabled.get(sess.session_id, True)
]
visible_review_ready = {
session_id: ready
for session_id, ready in review_ready.items()
@ -1229,10 +1270,15 @@ async def learner_dashboard(principal: CurrentPrincipal) -> LearnerDashboardResp
visible_review_ready=visible_review_ready,
archived_sessions=len(archives),
),
growth=_dashboard_growth(sessions),
persona_progress=_dashboard_persona_progress(sessions, visible_review_ready),
growth=_dashboard_growth(feedback_sessions),
persona_progress=_dashboard_persona_progress(
sessions,
visible_review_ready,
learner_feedback_enabled,
),
training_exposure=_dashboard_training_exposure(sessions),
achievements=_dashboard_achievements(sessions, visible_review_ready),
recent_feedback=_dashboard_feedback(sessions),
recent_feedback=_dashboard_feedback(feedback_sessions),
message=(
"실제 연습 기록을 기준으로 개인 학습 흐름을 표시합니다."
if sessions
@ -1253,7 +1299,13 @@ async def get_session_detail(
principal,
allow_ended=True,
)
return _session_detail(sess, review_ready=await _review_ready(sess, principal))
return _session_detail(
sess,
review_ready=await _review_ready(sess, principal),
learner_feedback_enabled=(
feedback_policy.effective_learner_feedback_enabled(sess, principal)
),
)
@router.post("/{session_id}/archive", response_model=SessionArchiveResponse)
@ -1355,18 +1407,26 @@ async def start_session(
carry_rapport = st.rapport_credit
goal_stages = [str(stage) for stage in body.goal_stages]
sess = await session_persistence.create_session(
learner_id=principal.user_id,
card=card,
theory_mode=body.theory_mode,
state=st,
session_no=session_no,
carry_rapport=carry_rapport,
persona_id=catalog_persona.persona_id,
persona_version=catalog_persona.version,
case_id=case_context.case_id if case_context else None,
goal_stages=goal_stages,
)
learner_feedback_enabled = principal.learner_feedback_enabled
try:
sess = await session_persistence.create_session(
learner_id=principal.user_id,
card=card,
theory_mode=body.theory_mode,
state=st,
session_no=session_no,
carry_rapport=carry_rapport,
persona_id=catalog_persona.persona_id,
persona_version=catalog_persona.version,
case_id=case_context.case_id if case_context else None,
goal_stages=goal_stages,
learner_feedback_enabled=learner_feedback_enabled,
)
except session_persistence.SessionCreationPersistenceError as exc:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
detail="session_persistence_unavailable",
) from exc
degraded = catalog_persona.degraded or sess is None
if sess is None:
require_runtime_fallback_allowed("session creation")
@ -1375,13 +1435,21 @@ async def start_session(
persona=card,
theory_mode=body.theory_mode,
state=st,
persona_id=catalog_persona.persona_id,
persona_version=catalog_persona.version,
session_no=session_no,
carry_rapport=carry_rapport,
goal_stages=goal_stages,
learner_feedback_enabled=learner_feedback_enabled,
)
else:
store.put(sess)
# DB 재조회 전의 첫 턴과 runtime fallback에서도 인증된 학습자 표시명을
# 역할 기반 비식별화에 사용할 수 있게 in-process 세션에만 보존한다.
sess.learner_label = principal.display_name or principal.user_id
store.put(sess)
# 즉시 빈/carry 회상으로 응답을 막지 않는다. RAG 회상·KB 단서(임베더 로드 수 초)는
# 백그라운드 warm으로 캐시 — 회기 시작/턴 응답이 임베더 로드에 블로킹되지 않게(성능 회귀 방지).
_RECALL_CACHE[sess.session_id] = recall
@ -1390,6 +1458,8 @@ async def start_session(
return SessionStartResponse(
session_id=sess.session_id,
case_id=sess.case_id,
persona_id=catalog_persona.persona_id,
persona_version=catalog_persona.version,
session_no=sess.session_no,
stage=_stage_label(st.stage),
effective_openness=round(st.effective_openness, 4),
@ -1399,6 +1469,7 @@ async def start_session(
goal_stages=body.goal_stages,
duration_limit_seconds=settings.session_duration_minutes * 60,
warning_before_end_seconds=settings.session_warning_minutes * 60,
learner_feedback_enabled=sess.learner_feedback_enabled,
)
@ -1413,18 +1484,31 @@ async def get_session_review(
principal,
include_turn_evaluation=True,
)
(
evaluation_record,
evaluation_durable,
) = await session_persistence.load_session_evaluation(
session_id,
review_principal,
expose_learner_feedback = (
feedback_policy.can_expose_principal_learner_feedback(
sess,
review_principal,
)
)
if expose_learner_feedback:
(
evaluation_record,
evaluation_durable,
) = await session_persistence.load_session_evaluation(
session_id,
review_principal,
)
else:
evaluation_record, evaluation_durable = None, True
saved_worksheet_payload, _ = await session_persistence.load_case_worksheet(
session_id,
review_principal,
)
include_teacher_review = review_principal.role in {Role.TEACHER, Role.ADMIN}
learner_feedback_enabled = feedback_policy.effective_learner_feedback_enabled(
sess,
review_principal,
)
teacher_review_record = None
if include_teacher_review:
teacher_review_record, _ = await session_persistence.load_session_review_status(
@ -1440,6 +1524,8 @@ async def get_session_review(
saved_worksheet_payload=saved_worksheet_payload,
include_teacher_review=include_teacher_review,
teacher_review_record=teacher_review_record,
learner_feedback_enabled=learner_feedback_enabled,
expose_learner_feedback=expose_learner_feedback,
)
)
@ -1462,6 +1548,11 @@ async def create_session_share(
raise HTTPException(
status.HTTP_409_CONFLICT, detail="session must be ended before sharing"
)
if not feedback_policy.can_expose_principal_learner_feedback(sess, principal):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=feedback_policy.LEARNER_FEEDBACK_DISABLED_DETAIL,
)
review = await get_session_review(session_id, principal)
token = secrets.token_urlsafe(32)
@ -1606,7 +1697,12 @@ async def list_live_coach_history(
) -> LiveCoachHistoryResponse:
"""현재 회기에서 학습자에게 실제로 전달된 라이브 코칭 이력을 반환한다."""
principal = _ensure_learner(principal)
await _load_session_or_404(session_id, principal)
sess = await _load_session_or_404(session_id, principal)
if not feedback_policy.can_expose_principal_learner_feedback(sess, principal):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=feedback_policy.LEARNER_FEEDBACK_DISABLED_DETAIL,
)
events, durable = await session_persistence.list_live_coach_events(
session_id, principal
)
@ -1641,6 +1737,11 @@ async def live_coach_turn(
"""방금 완료된 턴에 대한 비차단 라이브 코칭을 반환한다."""
principal = _ensure_learner(principal)
sess = await _load_session_or_404(session_id, principal)
if not feedback_policy.can_expose_principal_learner_feedback(sess, principal):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=feedback_policy.LEARNER_FEEDBACK_DISABLED_DETAIL,
)
quota, _ = await session_persistence.get_live_coach_quota(session_id, principal)
if int(quota.get("remaining", 0)) <= 0:
raise HTTPException(