feat: 운영 안정성과 세션 음성 경험 개선

This commit is contained in:
Yun Chan 2026-07-31 00:13:08 +09:00
parent facc4ad2d9
commit c788343467
95 changed files with 8431 additions and 1785 deletions

View file

@ -71,12 +71,13 @@ from ..session_read_model import (
session_share_payload as _session_share_payload,
stage_label as _stage_label,
)
from ..store import InProcSession, store
from ..store import InProcSession, TurnRecord, store
router = APIRouter(prefix="/sessions", tags=["sessions"])
logger = logging.getLogger(__name__)
_SESSION_EVALUATION_IN_FLIGHT: set[str] = set()
_SESSION_EVALUATION_RECOVERY_TASK: asyncio.Task[int] | None = None
_STREAM_TURN_EVALUATION_TASKS: set[asyncio.Task[None]] = set()
TheoryMode = Literal["humanistic", "cbt", "integrative"]
EndStateValue = str | int | float | bool | None | dict[str, float]
@ -670,9 +671,11 @@ async def _end_persisted_session(sess: InProcSession, carry: memory.CarryOver) -
if _should_schedule_session_digest_worker(carry):
asyncio.create_task(_run_session_digest_worker_for_session(sess.session_id))
asyncio.create_task(_write_episodic_embeddings(sess))
asyncio.create_task(engine_client.close_session(sess.session_id))
return
require_runtime_fallback_allowed("session end")
store.end(sess.session_id)
asyncio.create_task(engine_client.close_session(sess.session_id))
def _should_schedule_session_digest_worker(carry: memory.CarryOver) -> bool:
@ -770,6 +773,84 @@ async def _evaluate_stream_turn(
return orchestrator.turn_evaluation_error_payload(ctx, exc)
async def _evaluate_and_persist_stream_turn(
*,
sess: InProcSession,
ctx: orchestrator.TurnContext,
final_reply: str,
result: orchestrator.TurnResult,
learner_turn: TurnRecord,
) -> None:
"""응답 완료 뒤 fast-loop 평가를 저장해 다음 발화의 임계 경로에서 분리한다."""
evaluation = await _evaluate_stream_turn(ctx, final_reply)
if evaluation is None:
return
if learner_turn.turn_id is not None:
saved = await session_persistence.replace_turn_evaluation(
turn_id=learner_turn.turn_id,
evaluation=evaluation,
)
if not saved:
logger.warning(
"turn fast-loop evaluation was not saved: session_id=%s turn_id=%s",
ctx.session_id,
learner_turn.turn_id,
)
return
learner_turn.evaluation = evaluation
cached = store.get(ctx.session_id)
if cached is not None:
for turn in cached.turns:
if learner_turn.turn_id and turn.turn_id == learner_turn.turn_id:
turn.evaluation = evaluation
break
if (
learner_turn.turn_id is None
and turn.speaker == "counselor"
and turn.turn_seq == learner_turn.turn_seq
):
turn.evaluation = evaluation
break
result.evaluation = evaluation
await turn_runtime.maybe_recharge_live_coach_credit(sess, ctx, result)
def _observe_stream_turn_evaluation_task(task: asyncio.Task[None]) -> None:
_STREAM_TURN_EVALUATION_TASKS.discard(task)
try:
task.result()
except asyncio.CancelledError:
logger.info("turn fast-loop evaluation background task cancelled")
except Exception:
logger.exception("turn fast-loop evaluation background task crashed")
def _schedule_stream_turn_evaluation(
*,
sess: InProcSession,
ctx: orchestrator.TurnContext,
final_reply: str,
result: orchestrator.TurnResult,
learner_turn: TurnRecord,
) -> asyncio.Task[None]:
task = asyncio.create_task(
_evaluate_and_persist_stream_turn(
sess=sess,
ctx=ctx,
final_reply=final_reply,
result=result,
learner_turn=learner_turn,
),
name=f"turn-evaluation:{ctx.session_id}:{result.turn_seq}",
)
_STREAM_TURN_EVALUATION_TASKS.add(task)
task.add_done_callback(_observe_stream_turn_evaluation_task)
return task
def _stream_result_from_done(
ctx: orchestrator.TurnContext,
final_reply: str,
@ -1649,15 +1730,22 @@ async def stream_turn(
).model_dump(),
}
if not finalized_turn:
evaluation = await _evaluate_stream_turn(ctx, final_reply)
result = _stream_result_from_done(
ctx, final_reply, data, evaluation
ctx, final_reply, data, None
)
await turn_runtime.finalize_completed_turn(
learner_turn = await turn_runtime.finalize_completed_turn(
sess,
ctx,
result,
context_prefix="session",
recharge_live_coach=False,
)
_schedule_stream_turn_evaluation(
sess=sess,
ctx=ctx,
final_reply=final_reply,
result=result,
learner_turn=learner_turn,
)
finalized_turn = True
yield {