현재 작업 전체 반영
This commit is contained in:
parent
5560638e54
commit
c0dddab594
85 changed files with 11322 additions and 539 deletions
193
apps/api/app/turn_runtime.py
Normal file
193
apps/api/app/turn_runtime.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
"""REST/WS 공용 턴 런타임 헬퍼.
|
||||
|
||||
세션 로드, 오너십 검증, 완료 턴 영속화, 상태 갱신은 REST 세션 라우트와
|
||||
음성 WebSocket 라우트가 같은 규칙을 공유해야 한다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from . import db, session_persistence
|
||||
from .deps import Principal
|
||||
from .runtime_policy import require_runtime_fallback_allowed, runtime_fallback_allowed
|
||||
from .services import orchestrator, state_machine
|
||||
from .store import InProcSession, TurnRecord, store
|
||||
|
||||
_STAGE_LABELS = {
|
||||
"RAPPORT": "라포",
|
||||
"EXPLORE": "탐색",
|
||||
"INTERVENE": "개입",
|
||||
"CLOSE": "정리",
|
||||
}
|
||||
|
||||
|
||||
def stage_label(stage: object) -> str:
|
||||
"""Stage enum과 문자열 값을 같은 한글 라벨로 정규화한다."""
|
||||
name = getattr(stage, "name", "")
|
||||
return _STAGE_LABELS.get(name, str(getattr(stage, "value", stage)))
|
||||
|
||||
|
||||
class SessionAccessError(str, Enum):
|
||||
NOT_FOUND = "not_found"
|
||||
FORBIDDEN = "forbidden"
|
||||
ENDED = "ended"
|
||||
|
||||
|
||||
async def load_owned_session(
|
||||
session_id: str,
|
||||
principal: Principal,
|
||||
*,
|
||||
allow_ended: bool = False,
|
||||
include_turn_evaluation: bool = False,
|
||||
) -> tuple[InProcSession | None, Optional[SessionAccessError]]:
|
||||
"""DB 우선으로 학습자 소유 세션을 로드하고 접근 오류를 코드로 반환한다."""
|
||||
sess = await session_persistence.load_session(
|
||||
session_id,
|
||||
principal,
|
||||
allow_ended=True,
|
||||
include_turn_evaluation=include_turn_evaluation,
|
||||
)
|
||||
if sess is not None:
|
||||
store.put(sess)
|
||||
elif runtime_fallback_allowed():
|
||||
sess = store.get(session_id)
|
||||
if sess is None:
|
||||
return None, SessionAccessError.NOT_FOUND
|
||||
if sess.learner_id != principal.user_id:
|
||||
return None, SessionAccessError.FORBIDDEN
|
||||
if sess.ended and not allow_ended:
|
||||
return None, SessionAccessError.ENDED
|
||||
return sess, None
|
||||
|
||||
|
||||
async def append_completed_turn(
|
||||
sess: InProcSession,
|
||||
turn: TurnRecord,
|
||||
*,
|
||||
context: str,
|
||||
) -> None:
|
||||
"""완료된 턴을 DB와 in-process 미러에 기록한다."""
|
||||
if await session_persistence.append_turn(
|
||||
session_id=sess.session_id,
|
||||
learner_id=sess.learner_id,
|
||||
turn=turn,
|
||||
):
|
||||
sess.turns.append(turn)
|
||||
store.put(sess)
|
||||
return
|
||||
require_runtime_fallback_allowed(context)
|
||||
store.append_turn(sess.session_id, turn)
|
||||
|
||||
|
||||
async def update_session_state(
|
||||
sess: InProcSession,
|
||||
state: state_machine.SessionState,
|
||||
*,
|
||||
context: str,
|
||||
) -> None:
|
||||
"""working state를 DB와 in-process 미러에 반영한다."""
|
||||
if await session_persistence.update_state(
|
||||
session_id=sess.session_id,
|
||||
learner_id=sess.learner_id,
|
||||
state=state,
|
||||
):
|
||||
sess.state = state
|
||||
store.put(sess)
|
||||
return
|
||||
require_runtime_fallback_allowed(context)
|
||||
store.update_state(sess.session_id, state)
|
||||
|
||||
|
||||
async def record_completed_turn(
|
||||
sess: InProcSession,
|
||||
ctx: orchestrator.TurnContext,
|
||||
result: orchestrator.TurnResult,
|
||||
*,
|
||||
context_prefix: str,
|
||||
counselor_turn: TurnRecord | None = None,
|
||||
) -> None:
|
||||
"""상담자 발화와 내담자 응답을 한 번에 기록하고 상태를 갱신한다."""
|
||||
assert ctx.state_after is not None
|
||||
learner_turn = counselor_turn or TurnRecord(
|
||||
turn_seq=ctx.state_after.turn_seq,
|
||||
speaker="counselor",
|
||||
stage=stage_label(ctx.state_after.stage),
|
||||
text=ctx.learner_text_raw,
|
||||
text_masked=ctx.learner_text_masked,
|
||||
evaluation=result.evaluation,
|
||||
)
|
||||
await append_completed_turn(
|
||||
sess,
|
||||
learner_turn,
|
||||
context=f"{context_prefix} turn append",
|
||||
)
|
||||
if result.client_reply:
|
||||
await append_completed_turn(
|
||||
sess,
|
||||
TurnRecord(
|
||||
turn_seq=result.turn_seq,
|
||||
speaker="client",
|
||||
stage=stage_label(result.state_after.stage),
|
||||
text=result.client_reply,
|
||||
text_masked=result.client_reply,
|
||||
llm_provider=result.llm_provider,
|
||||
model=result.model,
|
||||
tokens_in=result.tokens_in,
|
||||
tokens_out=result.tokens_out,
|
||||
cost_usd=result.cost_usd,
|
||||
),
|
||||
context=f"{context_prefix} turn append",
|
||||
)
|
||||
await update_session_state(
|
||||
sess,
|
||||
result.state_after,
|
||||
context=f"{context_prefix} state update",
|
||||
)
|
||||
|
||||
|
||||
async def record_safety_event(
|
||||
sess: InProcSession,
|
||||
ctx: orchestrator.TurnContext,
|
||||
result: orchestrator.TurnResult,
|
||||
) -> None:
|
||||
"""위기 escalate 시 app.safety_events에 교수자 확인용 알림 레코드를 남긴다."""
|
||||
crisis = getattr(ctx, "crisis", None)
|
||||
if crisis is None or not getattr(crisis, "escalate", False):
|
||||
return
|
||||
kind = getattr(crisis.kind, "value", None) or str(getattr(crisis, "kind", "crisis"))
|
||||
try:
|
||||
async with db.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO app.safety_events
|
||||
(session_id, trigger_type, ko_risk_level, escalated, detail)
|
||||
VALUES ($1::uuid, $2, $3, TRUE, $4::jsonb)
|
||||
""",
|
||||
sess.session_id,
|
||||
kind,
|
||||
int(getattr(crisis, "risk_level", 0) or 0),
|
||||
json.dumps({
|
||||
"matched": list(getattr(crisis, "matched", []) or []),
|
||||
"stage": getattr(result, "stage", None),
|
||||
"turn_seq": getattr(result, "turn_seq", None),
|
||||
"conversation_stopped": getattr(result, "conversation_stopped", False),
|
||||
"crisis_resource": getattr(result, "crisis_resource", None),
|
||||
"alert_status": "teacher_dashboard",
|
||||
}),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SessionAccessError",
|
||||
"append_completed_turn",
|
||||
"load_owned_session",
|
||||
"record_safety_event",
|
||||
"record_completed_turn",
|
||||
"stage_label",
|
||||
"update_session_state",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue