merge: T58 scene compression + thread emission on close
This commit is contained in:
@@ -29,6 +29,8 @@ keeps moving.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from sqlite3 import Connection
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -167,6 +169,7 @@ async def _summarize_and_apply_for_witness(
|
||||
you_name: str,
|
||||
dialogue: list[dict],
|
||||
timeout_s: float,
|
||||
key_quotes_suffix: str = "",
|
||||
) -> ScenePOVSummary:
|
||||
"""Run :func:`summarize_scene` for one bot witness and apply the
|
||||
three projected updates (memory pov_summary rewrite, edge summary
|
||||
@@ -175,6 +178,10 @@ async def _summarize_and_apply_for_witness(
|
||||
Tolerant of missing pieces in the same way Phase 1 was: no memory
|
||||
row -> skip the rewrite; no edge row -> skip the edge_summary write
|
||||
(the empty-default classifier output simply yields no rewrites).
|
||||
|
||||
``key_quotes_suffix`` is appended verbatim to the per-POV summary
|
||||
text before the rewrite lands (T58.1) — empty string is the no-op
|
||||
default for low-significance scenes.
|
||||
"""
|
||||
from chat.state.edges import get_edge
|
||||
from chat.state.entities import get_bot
|
||||
@@ -206,6 +213,7 @@ async def _summarize_and_apply_for_witness(
|
||||
# Empty default -> skip the memory rewrite; the seeded
|
||||
# per-turn pov_summary stays in place.
|
||||
continue
|
||||
new_value = pov.summary + key_quotes_suffix
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="manual_edit",
|
||||
@@ -213,7 +221,7 @@ async def _summarize_and_apply_for_witness(
|
||||
"target_kind": "memory_pov_summary",
|
||||
"target_id": int(memory_id),
|
||||
"prior_value": prior_pov,
|
||||
"new_value": pov.summary,
|
||||
"new_value": new_value,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -255,6 +263,40 @@ async def _summarize_and_apply_for_witness(
|
||||
return pov
|
||||
|
||||
|
||||
def _build_key_quotes_suffix(conn: Connection, scene_id: int) -> str:
|
||||
"""If the scene's max-turn-significance is >= 2, build the
|
||||
"Key quotes:" suffix from the top-3 highest-significance memory rows
|
||||
(per requirements §11.1). Otherwise return the empty string so the
|
||||
per-POV summaries collapse fully (low-significance scenes lose all
|
||||
raw text in favor of the classifier rewrite).
|
||||
|
||||
Quote source is each memory's current ``pov_summary`` — the raw
|
||||
per-turn narrative seeded by T21, since this helper is called BEFORE
|
||||
the per-POV rewrite. Texts are truncated to 200 chars to bound
|
||||
memory row growth across many witnesses.
|
||||
"""
|
||||
row = conn.execute(
|
||||
"SELECT MAX(significance) FROM memories WHERE scene_id = ?",
|
||||
(scene_id,),
|
||||
).fetchone()
|
||||
max_sig = (row[0] if row else None) or 0
|
||||
if max_sig < 2:
|
||||
return ""
|
||||
cur = conn.execute(
|
||||
"SELECT pov_summary FROM memories WHERE scene_id = ? "
|
||||
"ORDER BY significance DESC, id ASC LIMIT 3",
|
||||
(scene_id,),
|
||||
)
|
||||
quotes = [
|
||||
(r[0] or "")[:200]
|
||||
for r in cur.fetchall()
|
||||
]
|
||||
if not quotes:
|
||||
return ""
|
||||
lines = "\n".join(f'- "{q}"' for q in quotes)
|
||||
return f"\n\nKey quotes:\n{lines}"
|
||||
|
||||
|
||||
async def apply_scene_close_summary(
|
||||
conn: Connection,
|
||||
client: LLMClient,
|
||||
@@ -296,8 +338,10 @@ async def apply_scene_close_summary(
|
||||
"""
|
||||
# Local imports to keep the module-level surface tight and avoid
|
||||
# any chance of a circular dep through chat.state.*.
|
||||
from chat.services.thread_detection import detect_threads
|
||||
from chat.state.entities import get_bot, get_you
|
||||
from chat.state.group_node import get_group_node
|
||||
from chat.state.threads import list_open_threads
|
||||
from chat.state.world import get_chat
|
||||
|
||||
you_entity = get_you(conn) or {"name": "you", "persona": ""}
|
||||
@@ -308,6 +352,11 @@ async def apply_scene_close_summary(
|
||||
|
||||
dialogue = _read_recent_dialogue(conn, chat_id)
|
||||
|
||||
# T58.1: build the "Key quotes:" suffix BEFORE the per-POV rewrites
|
||||
# land — quote source is the raw seeded pov_summary text on each
|
||||
# memory row, which the rewrite about to fire would clobber.
|
||||
key_quotes_suffix = _build_key_quotes_suffix(conn, scene_id)
|
||||
|
||||
host_pov = await _summarize_and_apply_for_witness(
|
||||
conn,
|
||||
client,
|
||||
@@ -318,6 +367,7 @@ async def apply_scene_close_summary(
|
||||
you_name=you_name,
|
||||
dialogue=dialogue,
|
||||
timeout_s=timeout_s,
|
||||
key_quotes_suffix=key_quotes_suffix,
|
||||
)
|
||||
|
||||
guest_pov: ScenePOVSummary | None = None
|
||||
@@ -332,6 +382,7 @@ async def apply_scene_close_summary(
|
||||
you_name=you_name,
|
||||
dialogue=dialogue,
|
||||
timeout_s=timeout_s,
|
||||
key_quotes_suffix=key_quotes_suffix,
|
||||
)
|
||||
|
||||
# Group node update: T70 runs a third classifier call to merge the
|
||||
@@ -364,6 +415,56 @@ async def apply_scene_close_summary(
|
||||
},
|
||||
)
|
||||
|
||||
# T58.2: thread detection on close. Reuses the dialogue we already
|
||||
# gathered for per-POV summarization — same {speaker, text} shape
|
||||
# detect_threads expects. Failure-tolerant: classify() returns the
|
||||
# empty default on retry-exhaustion, and the broad except below
|
||||
# protects the close pipeline from any other classifier/mock flap.
|
||||
try:
|
||||
thread_result = await detect_threads(
|
||||
client,
|
||||
classifier_model=classifier_model,
|
||||
scene_transcript=dialogue,
|
||||
open_threads=list_open_threads(conn, chat_id),
|
||||
timeout_s=timeout_s,
|
||||
)
|
||||
except Exception:
|
||||
from chat.services.thread_detection import ThreadDetectionResult
|
||||
|
||||
thread_result = ThreadDetectionResult()
|
||||
for cand in thread_result.candidates:
|
||||
if cand.action == "open":
|
||||
new_thread_id = f"thr_{uuid.uuid4().hex[:12]}"
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="thread_opened",
|
||||
payload={
|
||||
"thread_id": new_thread_id,
|
||||
"chat_id": chat_id,
|
||||
"title": cand.title,
|
||||
"summary": cand.summary,
|
||||
},
|
||||
)
|
||||
elif cand.action == "update" and cand.existing_thread_id:
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="thread_updated",
|
||||
payload={
|
||||
"thread_id": cand.existing_thread_id,
|
||||
"summary": cand.summary,
|
||||
"last_referenced_scene_id": scene_id,
|
||||
},
|
||||
)
|
||||
elif cand.action == "close" and cand.existing_thread_id:
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="thread_closed",
|
||||
payload={
|
||||
"thread_id": cand.existing_thread_id,
|
||||
"closed_at": datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
return host_pov
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user