개선관리 요구사항과 Google 로그인을 완료
This commit is contained in:
parent
cc0a15b7c6
commit
2a39636163
112 changed files with 10166 additions and 527 deletions
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue