관리자 감정 관측 기록과 조회 API 추가
This commit is contained in:
parent
d22cd9883d
commit
acb0d26338
16 changed files with 1445 additions and 18 deletions
|
|
@ -14,6 +14,7 @@ from typing import Any, Awaitable, Callable, Iterable, Literal
|
|||
from .db import acquire, get_pool
|
||||
from .deps import Principal
|
||||
from .config import settings
|
||||
from .contracts.client_affect import ClientAffectTraceV1
|
||||
from .persona_repository import (
|
||||
SEED_VERSION,
|
||||
card_from_row,
|
||||
|
|
@ -44,6 +45,10 @@ class SessionCreationPersistenceError(RuntimeError):
|
|||
"""fail-closed 환경에서 영속 세션 생성이 실패했다."""
|
||||
|
||||
|
||||
class ClientAffectTracePersistenceError(RuntimeError):
|
||||
"""내담자 턴·감정 trace·상태를 같은 트랜잭션으로 기록하지 못했다."""
|
||||
|
||||
|
||||
class ActiveSessionExistsError(RuntimeError):
|
||||
"""같은 learner-persona 전체에 미종료 회기가 이미 존재한다."""
|
||||
|
||||
|
|
@ -2892,6 +2897,100 @@ async def append_turn(
|
|||
return False
|
||||
|
||||
|
||||
async def append_client_turn_with_affect_trace(
|
||||
*,
|
||||
session_id: str,
|
||||
learner_id: str,
|
||||
turn: TurnRecord,
|
||||
state: state_machine.SessionState,
|
||||
trace: ClientAffectTraceV1,
|
||||
) -> bool:
|
||||
"""성공 내담자 턴·Jev trace·전이 상태를 하나의 DB 트랜잭션에 기록한다."""
|
||||
inserted_turn_id: str | None = None
|
||||
try:
|
||||
get_pool()
|
||||
async with acquire(role="learner", user_id=learner_id) as conn:
|
||||
async with conn.transaction():
|
||||
locked = await conn.fetchval(
|
||||
"""
|
||||
SELECT id
|
||||
FROM app.sessions
|
||||
WHERE id = $1::uuid
|
||||
AND learner_id = $2::uuid
|
||||
FOR UPDATE
|
||||
""",
|
||||
session_id,
|
||||
learner_id,
|
||||
)
|
||||
if locked is None:
|
||||
raise ClientAffectTracePersistenceError("owned session not found")
|
||||
seq = int(
|
||||
await conn.fetchval(
|
||||
"SELECT COALESCE(MAX(seq), 0) + 1 FROM app.turns WHERE session_id = $1::uuid",
|
||||
session_id,
|
||||
)
|
||||
or 1
|
||||
)
|
||||
inserted = await conn.fetchval(
|
||||
"""
|
||||
INSERT INTO app.turns (
|
||||
session_id, seq, speaker, stage, text, text_masked, actor_kind,
|
||||
llm_provider, model, tokens_in, tokens_out, cost_usd,
|
||||
audio_ref, silence_ms, speech_rate, barge_in, provider_events, visible_to
|
||||
)
|
||||
VALUES (
|
||||
$1::uuid, $2, $3, $4, $5, $6, $7,
|
||||
$8, $9, $10, $11, $12,
|
||||
$13, $14, $15, $16, $17::jsonb, $18::text[]
|
||||
)
|
||||
ON CONFLICT (session_id, seq) DO NOTHING
|
||||
RETURNING id
|
||||
""",
|
||||
session_id,
|
||||
seq,
|
||||
turn.speaker,
|
||||
turn.stage,
|
||||
turn.text_masked,
|
||||
turn.text_masked,
|
||||
"client_ai",
|
||||
turn.llm_provider,
|
||||
turn.model,
|
||||
turn.tokens_in,
|
||||
turn.tokens_out,
|
||||
turn.cost_usd,
|
||||
turn.audio_ref,
|
||||
turn.silence_ms,
|
||||
turn.speech_rate,
|
||||
turn.barge_in,
|
||||
turn.provider_events or [],
|
||||
list(turn.visible_to or DEFAULT_TURN_VISIBLE_TO),
|
||||
)
|
||||
if inserted is None:
|
||||
raise ClientAffectTracePersistenceError("client turn insert conflict")
|
||||
inserted_turn_id = str(inserted)
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO app.client_affect_trace (turn_id, session_id, trace)
|
||||
VALUES ($1::uuid, $2::uuid, $3::jsonb)
|
||||
""",
|
||||
inserted_turn_id,
|
||||
session_id,
|
||||
trace.model_dump(mode="json"),
|
||||
)
|
||||
await _upsert_state(conn, session_id, state)
|
||||
except ClientAffectTracePersistenceError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
"client affect trace persistence failed: session_id=%s", session_id
|
||||
)
|
||||
raise ClientAffectTracePersistenceError(
|
||||
"client affect trace persistence failed"
|
||||
) from exc
|
||||
turn.turn_id = inserted_turn_id
|
||||
return True
|
||||
|
||||
|
||||
async def update_state(
|
||||
*,
|
||||
session_id: str,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue