현재 작업 전체 반영
This commit is contained in:
parent
5560638e54
commit
c0dddab594
85 changed files with 11322 additions and 539 deletions
|
|
@ -22,14 +22,14 @@ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
|||
from fastapi.responses import JSONResponse
|
||||
from starlette.websockets import WebSocketState
|
||||
|
||||
from .. import session_persistence
|
||||
from .. import session_persistence, turn_runtime
|
||||
from ..auth_sessions import get_session
|
||||
from ..config import settings
|
||||
from ..deps import Principal, Role
|
||||
from ..engine_client import EngineError, engine_client
|
||||
from ..persona_repository import get_catalog_persona
|
||||
from ..runtime_policy import require_runtime_fallback_allowed, runtime_fallback_allowed
|
||||
from ..services import evaluator, memory, orchestrator, state_machine
|
||||
from ..runtime_policy import require_runtime_fallback_allowed
|
||||
from ..services import evaluator, orchestrator, state_machine
|
||||
from ..services import voice as voice_svc
|
||||
from ..services.voice import VoicePreset, VoiceUnavailable, resolve_voice, voice_service
|
||||
from ..store import InProcSession, TurnRecord, store
|
||||
|
|
@ -292,7 +292,10 @@ async def _run_turn_and_speak(
|
|||
await _safe_send_json(websocket, {"type": "state", "state": "idle"})
|
||||
return
|
||||
|
||||
recall = memory.RecallContext()
|
||||
from . import sessions as session_routes
|
||||
|
||||
recall = await session_routes.ensure_recall_context(sess)
|
||||
kb_cues = session_routes._KB_CUES_CACHE.get(session_id) or []
|
||||
ctx = orchestrator.prepare_turn(
|
||||
session_id=session_id,
|
||||
case_id=sess.case_id,
|
||||
|
|
@ -302,6 +305,7 @@ async def _run_turn_and_speak(
|
|||
recall_summary=recall.recall_summary,
|
||||
pinned_facts=recall.pinned_facts,
|
||||
recent_turns=sess.recent_turns(visible_to="client"),
|
||||
kb_behavior_cues=kb_cues,
|
||||
theory_mode=sess.theory_mode,
|
||||
)
|
||||
assert ctx.state_after is not None
|
||||
|
|
@ -311,7 +315,11 @@ async def _run_turn_and_speak(
|
|||
result = await orchestrator.run_turn_generate(
|
||||
ctx,
|
||||
engine_client,
|
||||
eval_hook=evaluator.make_eval_hook(engine_client),
|
||||
eval_hook=evaluator.make_eval_hook(
|
||||
engine_client,
|
||||
audit_hook=session_persistence.record_llm_call_audit,
|
||||
),
|
||||
audit_hook=session_persistence.record_llm_call_audit,
|
||||
)
|
||||
except EngineError as e:
|
||||
await _safe_send_json(websocket, {"type": "error", "detail": f"engine unavailable: {e}"})
|
||||
|
|
@ -321,12 +329,15 @@ async def _run_turn_and_speak(
|
|||
reply = result.client_reply or ""
|
||||
# Persist only after the client reply has been generated. A failed AI turn
|
||||
# must not leave a learner-only transcript in review or history.
|
||||
await _append_voice_turn(
|
||||
await turn_runtime.record_completed_turn(
|
||||
sess,
|
||||
TurnRecord(
|
||||
ctx,
|
||||
result,
|
||||
context_prefix="voice session",
|
||||
counselor_turn=TurnRecord(
|
||||
turn_seq=ctx.state_after.turn_seq,
|
||||
speaker="counselor",
|
||||
stage=ctx.state_after.stage.value,
|
||||
stage=turn_runtime.stage_label(ctx.state_after.stage),
|
||||
text=learner_text,
|
||||
text_masked=ctx.learner_text_masked,
|
||||
audio_ref=audio_ref,
|
||||
|
|
@ -336,24 +347,7 @@ async def _run_turn_and_speak(
|
|||
evaluation=result.evaluation,
|
||||
),
|
||||
)
|
||||
if reply:
|
||||
# Persist the generated client reply before TTS playback.
|
||||
await _append_voice_turn(
|
||||
sess,
|
||||
TurnRecord(
|
||||
turn_seq=result.turn_seq,
|
||||
speaker="client",
|
||||
stage=result.stage,
|
||||
text=reply,
|
||||
text_masked=reply,
|
||||
llm_provider=result.llm_provider,
|
||||
model=result.model,
|
||||
tokens_in=result.tokens_in,
|
||||
tokens_out=result.tokens_out,
|
||||
cost_usd=result.cost_usd,
|
||||
),
|
||||
)
|
||||
await _update_voice_state(sess, result.state_after)
|
||||
await turn_runtime.record_safety_event(sess, ctx, result)
|
||||
|
||||
# Send the final client text before audio playback.
|
||||
await _safe_send_json(
|
||||
|
|
@ -367,6 +361,8 @@ async def _run_turn_and_speak(
|
|||
"turn_seq": result.turn_seq,
|
||||
"safety_flagged": result.safety_flagged,
|
||||
"crisis_kind": result.crisis_kind,
|
||||
"crisis_resource": result.crisis_resource,
|
||||
"conversation_stopped": result.conversation_stopped,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -403,49 +399,17 @@ async def _load_voice_session(
|
|||
session_id: str,
|
||||
principal: Principal,
|
||||
) -> tuple[InProcSession | None, str | None]:
|
||||
sess = await session_persistence.load_session(session_id, principal, allow_ended=True)
|
||||
if sess is not None:
|
||||
store.put(sess)
|
||||
elif runtime_fallback_allowed():
|
||||
sess = store.get(session_id)
|
||||
if sess is None:
|
||||
sess, err = await turn_runtime.load_owned_session(session_id, principal)
|
||||
if err == turn_runtime.SessionAccessError.NOT_FOUND:
|
||||
return None, f"unknown session {session_id}"
|
||||
if sess.learner_id != principal.user_id:
|
||||
if err == turn_runtime.SessionAccessError.FORBIDDEN:
|
||||
return None, "session does not belong to user"
|
||||
if sess.ended:
|
||||
if err == turn_runtime.SessionAccessError.ENDED:
|
||||
return None, "session already ended"
|
||||
assert sess is not None
|
||||
return sess, None
|
||||
|
||||
|
||||
async def _append_voice_turn(sess: InProcSession, turn: TurnRecord) -> None:
|
||||
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("voice session turn append")
|
||||
store.append_turn(sess.session_id, turn)
|
||||
|
||||
|
||||
async def _update_voice_state(
|
||||
sess: InProcSession,
|
||||
state: state_machine.SessionState,
|
||||
) -> None:
|
||||
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("voice session state update")
|
||||
store.update_state(sess.session_id, state)
|
||||
|
||||
|
||||
async def _principal_from_websocket(websocket: WebSocket) -> Principal | None:
|
||||
"""Restore the same server-side browser session used by REST routes."""
|
||||
raw_cookie = websocket.cookies.get(settings.cookie_name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue