세션 메모리와 비언어 이벤트 저장
This commit is contained in:
parent
e8e08935ed
commit
50fa4ad432
12 changed files with 2848 additions and 1277 deletions
|
|
@ -551,6 +551,7 @@ def _turn_from_row(row, evaluation: dict[str, Any] | None = None) -> TurnRecord:
|
|||
silence_ms=_row_value(row, "silence_ms"),
|
||||
speech_rate=_row_value(row, "speech_rate"),
|
||||
barge_in=_row_value(row, "barge_in"),
|
||||
provider_events=_dict_items(_row_value(row, "provider_events")),
|
||||
evaluation=evaluation,
|
||||
visible_to=tuple(_row_value(row, "visible_to") or DEFAULT_TURN_VISIBLE_TO),
|
||||
)
|
||||
|
|
@ -1890,7 +1891,7 @@ async def load_session(
|
|||
"""
|
||||
SELECT id, seq, speaker, stage, text, text_masked, created_at,
|
||||
llm_provider, model, tokens_in, tokens_out, cost_usd,
|
||||
audio_ref, silence_ms, speech_rate, barge_in, visible_to
|
||||
audio_ref, silence_ms, speech_rate, barge_in, provider_events, visible_to
|
||||
FROM app.turns
|
||||
WHERE session_id = $1::uuid
|
||||
ORDER BY seq
|
||||
|
|
@ -1945,12 +1946,12 @@ async def append_turn(
|
|||
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, visible_to
|
||||
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::text[]
|
||||
$13, $14, $15, $16, $17::jsonb, $18::text[]
|
||||
)
|
||||
ON CONFLICT (session_id, seq) DO NOTHING
|
||||
RETURNING id
|
||||
|
|
@ -1971,6 +1972,7 @@ async def append_turn(
|
|||
turn.silence_ms,
|
||||
turn.speech_rate,
|
||||
turn.barge_in,
|
||||
turn.provider_events or [],
|
||||
list(turn.visible_to or DEFAULT_TURN_VISIBLE_TO),
|
||||
)
|
||||
if inserted_turn_id is None:
|
||||
|
|
@ -2000,13 +2002,176 @@ async def update_state(
|
|||
return False
|
||||
|
||||
|
||||
async def _insert_pinned_fact_history(
|
||||
conn: Any,
|
||||
*,
|
||||
fact_id: Any,
|
||||
case_id: str,
|
||||
old_value: Any,
|
||||
new_value: Any,
|
||||
reason: str,
|
||||
session_no: int,
|
||||
turn_id: str | None,
|
||||
) -> None:
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO app.pinned_fact_history (
|
||||
fact_id, case_id, old_value, new_value, reason, session_no, turn_id
|
||||
)
|
||||
VALUES ($1::uuid, $2::uuid, $3, $4, $5, $6, $7::uuid)
|
||||
""",
|
||||
fact_id,
|
||||
case_id,
|
||||
old_value,
|
||||
new_value,
|
||||
reason,
|
||||
session_no,
|
||||
turn_id,
|
||||
)
|
||||
|
||||
|
||||
async def _upsert_pinned_fact_candidates(conn: Any, sess: InProcSession) -> None:
|
||||
turn_rows = [
|
||||
{
|
||||
"speaker": turn.speaker,
|
||||
"text": turn.text_masked,
|
||||
"turn_id": turn.turn_id,
|
||||
}
|
||||
for turn in sess.turns_visible_to("client")
|
||||
]
|
||||
candidates = memory.extract_pinned_fact_candidates(turn_rows)
|
||||
for fact in candidates:
|
||||
if fact.status == "contradicted":
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
WITH existing AS (
|
||||
SELECT id, value
|
||||
FROM app.pinned_fact
|
||||
WHERE case_id = $1::uuid
|
||||
AND key = $2
|
||||
AND status <> 'locked'
|
||||
FOR UPDATE
|
||||
),
|
||||
updated AS (
|
||||
UPDATE app.pinned_fact
|
||||
SET value = $3,
|
||||
fact_type = $4,
|
||||
status = 'contradicted',
|
||||
source_turn = COALESCE($5::uuid, app.pinned_fact.source_turn),
|
||||
confidence = GREATEST(app.pinned_fact.confidence, $6),
|
||||
version = app.pinned_fact.version + 1,
|
||||
updated_session_no = $7,
|
||||
visible_to = $8::text[],
|
||||
updated_at = now()
|
||||
FROM existing
|
||||
WHERE app.pinned_fact.id = existing.id
|
||||
RETURNING
|
||||
app.pinned_fact.id,
|
||||
existing.value AS old_value,
|
||||
app.pinned_fact.value AS new_value
|
||||
)
|
||||
SELECT id, old_value, new_value FROM updated
|
||||
""",
|
||||
sess.case_id,
|
||||
fact.key,
|
||||
fact.value,
|
||||
fact.fact_type,
|
||||
fact.source_turn_id,
|
||||
fact.confidence,
|
||||
sess.session_no,
|
||||
["evaluator"],
|
||||
)
|
||||
if not row:
|
||||
continue
|
||||
old_value = row["old_value"]
|
||||
new_value = row["new_value"]
|
||||
if old_value == new_value:
|
||||
continue
|
||||
await _insert_pinned_fact_history(
|
||||
conn,
|
||||
fact_id=row["id"],
|
||||
case_id=sess.case_id,
|
||||
old_value=old_value,
|
||||
new_value=new_value,
|
||||
reason="contradiction",
|
||||
session_no=sess.session_no,
|
||||
turn_id=fact.source_turn_id,
|
||||
)
|
||||
continue
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
WITH existing AS (
|
||||
SELECT id, value, status
|
||||
FROM app.pinned_fact
|
||||
WHERE case_id = $1::uuid AND key = $2
|
||||
FOR UPDATE
|
||||
),
|
||||
upserted AS (
|
||||
INSERT INTO app.pinned_fact (
|
||||
case_id, key, value, fact_type, status, source_turn,
|
||||
confidence, updated_session_no, visible_to, updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1::uuid, $2, $3, $4, $5, $6::uuid,
|
||||
$7, $8, $9::text[], now()
|
||||
)
|
||||
ON CONFLICT (case_id, key) DO UPDATE SET
|
||||
value = EXCLUDED.value,
|
||||
fact_type = EXCLUDED.fact_type,
|
||||
status = EXCLUDED.status,
|
||||
source_turn = COALESCE(EXCLUDED.source_turn, app.pinned_fact.source_turn),
|
||||
confidence = GREATEST(app.pinned_fact.confidence, EXCLUDED.confidence),
|
||||
version = CASE
|
||||
WHEN app.pinned_fact.value IS DISTINCT FROM EXCLUDED.value
|
||||
THEN app.pinned_fact.version + 1
|
||||
ELSE app.pinned_fact.version
|
||||
END,
|
||||
updated_session_no = EXCLUDED.updated_session_no,
|
||||
visible_to = EXCLUDED.visible_to,
|
||||
updated_at = now()
|
||||
WHERE app.pinned_fact.status <> 'locked'
|
||||
RETURNING
|
||||
app.pinned_fact.id,
|
||||
(SELECT value FROM existing) AS old_value,
|
||||
app.pinned_fact.value AS new_value
|
||||
)
|
||||
SELECT id, old_value, new_value FROM upserted
|
||||
""",
|
||||
sess.case_id,
|
||||
fact.key,
|
||||
fact.value,
|
||||
fact.fact_type,
|
||||
fact.status,
|
||||
fact.source_turn_id,
|
||||
fact.confidence,
|
||||
sess.session_no,
|
||||
["client", "evaluator"],
|
||||
)
|
||||
if not row:
|
||||
continue
|
||||
old_value = row["old_value"]
|
||||
new_value = row["new_value"]
|
||||
if old_value is not None and old_value == new_value:
|
||||
continue
|
||||
reason = "progression" if old_value is None else "clarification"
|
||||
await _insert_pinned_fact_history(
|
||||
conn,
|
||||
fact_id=row["id"],
|
||||
case_id=sess.case_id,
|
||||
old_value=old_value,
|
||||
new_value=new_value,
|
||||
reason=reason,
|
||||
session_no=sess.session_no,
|
||||
turn_id=fact.source_turn_id,
|
||||
)
|
||||
|
||||
async def end_session(sess: InProcSession, carry: memory.CarryOver) -> bool:
|
||||
try:
|
||||
get_pool()
|
||||
digest = (
|
||||
f"회기 축어록 {len(sess.turns)}개가 저장되었습니다. 정밀 리뷰는 생성 대기 중입니다."
|
||||
if sess.turns
|
||||
else "실제 발화가 없어 요약을 생성하지 않았습니다."
|
||||
digest = memory.build_fallback_session_digest(
|
||||
session_no=sess.session_no,
|
||||
masked_turns=sess.masked_turns(visible_to="client"),
|
||||
end_state=carry.end_state,
|
||||
)
|
||||
async with acquire(role="learner", user_id=sess.learner_id) as conn:
|
||||
await conn.execute(
|
||||
|
|
@ -2039,6 +2204,51 @@ async def end_session(sess: InProcSession, carry: memory.CarryOver) -> bool:
|
|||
digest,
|
||||
list(carry.compression_job.open_threads if carry.compression_job else []),
|
||||
)
|
||||
case_row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT case_digest, rapport_trajectory, alliance_level
|
||||
FROM app.case_profile
|
||||
WHERE case_id = $1::uuid
|
||||
AND learner_id = $2::uuid
|
||||
""",
|
||||
sess.case_id,
|
||||
sess.learner_id,
|
||||
)
|
||||
if case_row is not None:
|
||||
trajectory_point = memory.rapport_trajectory_point(
|
||||
session_no=sess.session_no,
|
||||
end_state=carry.end_state,
|
||||
)
|
||||
case_digest = memory.merge_case_digest(
|
||||
existing_digest=case_row["case_digest"],
|
||||
session_no=sess.session_no,
|
||||
session_digest=digest,
|
||||
)
|
||||
rapport_trajectory = memory.merge_rapport_trajectory(
|
||||
case_row["rapport_trajectory"],
|
||||
trajectory_point,
|
||||
)
|
||||
alliance_level = memory.update_alliance_level(
|
||||
case_row["alliance_level"],
|
||||
trajectory_point.get("end_rapport"),
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE app.case_profile
|
||||
SET case_digest = $3,
|
||||
rapport_trajectory = $4::jsonb,
|
||||
alliance_level = $5,
|
||||
updated_at = now()
|
||||
WHERE case_id = $1::uuid
|
||||
AND learner_id = $2::uuid
|
||||
""",
|
||||
sess.case_id,
|
||||
sess.learner_id,
|
||||
case_digest,
|
||||
rapport_trajectory,
|
||||
alliance_level,
|
||||
)
|
||||
await _upsert_pinned_fact_candidates(conn, sess)
|
||||
return True
|
||||
except Exception:
|
||||
require_runtime_fallback_allowed("session end")
|
||||
|
|
@ -2110,7 +2320,7 @@ async def list_sessions(
|
|||
"""
|
||||
SELECT id, seq, speaker, stage, text, text_masked, created_at,
|
||||
llm_provider, model, tokens_in, tokens_out, cost_usd,
|
||||
audio_ref, silence_ms, speech_rate, barge_in, visible_to
|
||||
audio_ref, silence_ms, speech_rate, barge_in, provider_events, visible_to
|
||||
FROM app.turns
|
||||
WHERE session_id = $1::uuid
|
||||
ORDER BY seq
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue