feat: per-POV summaries on close for each present witness
This commit is contained in:
@@ -156,64 +156,50 @@ def _read_recent_dialogue(
|
||||
return out
|
||||
|
||||
|
||||
async def apply_scene_close_summary(
|
||||
async def _summarize_and_apply_for_witness(
|
||||
conn: Connection,
|
||||
client: LLMClient,
|
||||
*,
|
||||
classifier_model: str,
|
||||
chat_id: str,
|
||||
scene_id: int,
|
||||
host_bot_id: str,
|
||||
timeout_s: float = 10.0,
|
||||
bot_id: str,
|
||||
you_name: str,
|
||||
dialogue: list[dict],
|
||||
timeout_s: float,
|
||||
) -> ScenePOVSummary:
|
||||
"""Drive the per-POV summary pipeline after ``scene_closed``.
|
||||
"""Run :func:`summarize_scene` for one bot witness and apply the
|
||||
three projected updates (memory pov_summary rewrite, edge summary
|
||||
overwrite, edge knowledge_facts append).
|
||||
|
||||
Steps (Phase 1, single-bot):
|
||||
1. Gather the closing scene's dialogue from the event_log.
|
||||
2. Run :func:`summarize_scene` for the host bot.
|
||||
3. Rewrite each scene-bound memory's ``pov_summary`` via
|
||||
``manual_edit`` (target_kind ``memory_pov_summary``), capturing
|
||||
the prior value for §6.4 reversibility.
|
||||
4. Update the bot->you edge summary via ``manual_edit`` with the
|
||||
new ``edge_summary`` target_kind. v1 combines prior + new by
|
||||
concatenation — the classifier's ``relationship_summary`` is
|
||||
already phrased as a continuation.
|
||||
5. Append any new knowledge_facts to the same edge via
|
||||
``edge_update``.
|
||||
|
||||
Tolerant of missing pieces: no memories -> skip step 3 silently;
|
||||
no edge row -> skip step 4; empty knowledge_facts -> skip step 5.
|
||||
The classifier's empty default flows through harmlessly.
|
||||
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).
|
||||
"""
|
||||
# Local imports to keep the module-level surface tight and avoid
|
||||
# any chance of a circular dep through chat.state.*.
|
||||
from chat.state.edges import get_edge
|
||||
from chat.state.entities import get_bot, get_you
|
||||
from chat.state.entities import get_bot
|
||||
|
||||
host_bot = get_bot(conn, host_bot_id) or {"name": host_bot_id, "persona": ""}
|
||||
you_entity = get_you(conn) or {"name": "you", "persona": ""}
|
||||
bot = get_bot(conn, bot_id) or {"name": bot_id, "persona": ""}
|
||||
|
||||
dialogue = _read_recent_dialogue(conn, chat_id)
|
||||
|
||||
edge_b2y = get_edge(conn, host_bot_id, "you")
|
||||
edge_b2y = get_edge(conn, bot_id, "you")
|
||||
prior_summary = (edge_b2y or {}).get("summary", "") or ""
|
||||
|
||||
pov = await summarize_scene(
|
||||
client,
|
||||
model=classifier_model,
|
||||
bot_name=host_bot.get("name", host_bot_id),
|
||||
bot_persona=host_bot.get("persona", "") or "",
|
||||
you_name=you_entity.get("name", "you") or "you",
|
||||
bot_name=bot.get("name", bot_id),
|
||||
bot_persona=bot.get("persona", "") or "",
|
||||
you_name=you_name,
|
||||
prior_edge_summary=prior_summary,
|
||||
dialogue=dialogue,
|
||||
timeout_s=timeout_s,
|
||||
)
|
||||
|
||||
# Update memories belonging to the closed scene for the host bot.
|
||||
# Update memories belonging to the closed scene for this witness.
|
||||
cur = conn.execute(
|
||||
"SELECT id, pov_summary FROM memories "
|
||||
"WHERE scene_id = ? AND owner_id = ?",
|
||||
(scene_id, host_bot_id),
|
||||
(scene_id, bot_id),
|
||||
)
|
||||
for memory_id, prior_pov in cur.fetchall():
|
||||
if not pov.summary:
|
||||
@@ -231,7 +217,7 @@ async def apply_scene_close_summary(
|
||||
},
|
||||
)
|
||||
|
||||
# Update the bot->you edge summary if we have an edge row and a
|
||||
# Update this bot->you edge summary if we have an edge row and a
|
||||
# non-empty relationship_summary to merge.
|
||||
if edge_b2y is not None and pov.relationship_summary:
|
||||
new_summary = (
|
||||
@@ -245,7 +231,7 @@ async def apply_scene_close_summary(
|
||||
payload={
|
||||
"target_kind": "edge_summary",
|
||||
"target_id": {
|
||||
"source_id": host_bot_id,
|
||||
"source_id": bot_id,
|
||||
"target_id": "you",
|
||||
},
|
||||
"prior_value": prior_summary,
|
||||
@@ -253,13 +239,13 @@ async def apply_scene_close_summary(
|
||||
},
|
||||
)
|
||||
|
||||
# Append knowledge_facts to the bot->you edge if present.
|
||||
# Append knowledge_facts to this bot->you edge if present.
|
||||
if pov.knowledge_facts:
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="edge_update",
|
||||
payload={
|
||||
"source_id": host_bot_id,
|
||||
"source_id": bot_id,
|
||||
"target_id": "you",
|
||||
"chat_id": chat_id,
|
||||
"knowledge_facts": list(pov.knowledge_facts),
|
||||
@@ -267,3 +253,107 @@ async def apply_scene_close_summary(
|
||||
)
|
||||
|
||||
return pov
|
||||
|
||||
|
||||
async def apply_scene_close_summary(
|
||||
conn: Connection,
|
||||
client: LLMClient,
|
||||
*,
|
||||
classifier_model: str,
|
||||
chat_id: str,
|
||||
scene_id: int,
|
||||
host_bot_id: str,
|
||||
timeout_s: float = 10.0,
|
||||
) -> ScenePOVSummary:
|
||||
"""Drive the per-POV summary pipeline after ``scene_closed``.
|
||||
|
||||
Phase 1 (single-bot) behavior — the host bot is summarized once and
|
||||
the result drives memory + edge rewrites — is preserved exactly when
|
||||
the chat has no guest. T45 extends this to fan out across each
|
||||
present bot witness when a guest is also in the room:
|
||||
|
||||
1. Gather the closing scene's dialogue from the event_log.
|
||||
2. For each present witness (host + guest if any), run
|
||||
:func:`summarize_scene` once with that witness's persona and
|
||||
their own prior ``bot -> you`` edge summary.
|
||||
3. For each witness independently:
|
||||
a. Rewrite each scene-bound memory's ``pov_summary`` via
|
||||
``manual_edit`` (target_kind ``memory_pov_summary``).
|
||||
b. Update that witness's ``bot -> you`` edge summary via
|
||||
``manual_edit`` (target_kind ``edge_summary``). v2 combines
|
||||
prior + classifier ``relationship_summary`` by simple
|
||||
concatenation.
|
||||
c. Append any ``knowledge_facts`` to the same edge via
|
||||
``edge_update``.
|
||||
4. If a ``group_node`` row exists for this chat, append a
|
||||
``group_node_updated`` event whose ``summary`` is the naive
|
||||
per-POV concat ``f"{name}: {summary}\\n\\n..."``. A true
|
||||
LLM-merged group view is deferred to Phase 2.5; ``dynamic``
|
||||
is left empty here for v2 (Phase 3 polishes it).
|
||||
|
||||
The host's :class:`ScenePOVSummary` is returned to preserve the
|
||||
Phase 1 callers' contract.
|
||||
"""
|
||||
# Local imports to keep the module-level surface tight and avoid
|
||||
# any chance of a circular dep through chat.state.*.
|
||||
from chat.state.entities import get_bot, get_you
|
||||
from chat.state.group_node import get_group_node
|
||||
from chat.state.world import get_chat
|
||||
|
||||
you_entity = get_you(conn) or {"name": "you", "persona": ""}
|
||||
you_name = you_entity.get("name", "you") or "you"
|
||||
|
||||
chat = get_chat(conn, chat_id) or {}
|
||||
guest_bot_id = chat.get("guest_bot_id")
|
||||
|
||||
dialogue = _read_recent_dialogue(conn, chat_id)
|
||||
|
||||
host_pov = await _summarize_and_apply_for_witness(
|
||||
conn,
|
||||
client,
|
||||
classifier_model=classifier_model,
|
||||
chat_id=chat_id,
|
||||
scene_id=scene_id,
|
||||
bot_id=host_bot_id,
|
||||
you_name=you_name,
|
||||
dialogue=dialogue,
|
||||
timeout_s=timeout_s,
|
||||
)
|
||||
|
||||
guest_pov: ScenePOVSummary | None = None
|
||||
if guest_bot_id is not None:
|
||||
guest_pov = await _summarize_and_apply_for_witness(
|
||||
conn,
|
||||
client,
|
||||
classifier_model=classifier_model,
|
||||
chat_id=chat_id,
|
||||
scene_id=scene_id,
|
||||
bot_id=guest_bot_id,
|
||||
you_name=you_name,
|
||||
dialogue=dialogue,
|
||||
timeout_s=timeout_s,
|
||||
)
|
||||
|
||||
# Group node update: naive per-POV concat for v2. Only fires when
|
||||
# both POVs ran (i.e. the guest is present) and a group_node row
|
||||
# exists for this chat.
|
||||
if guest_pov is not None and get_group_node(conn, chat_id) is not None:
|
||||
host_bot = get_bot(conn, host_bot_id) or {"name": host_bot_id}
|
||||
guest_bot = get_bot(conn, guest_bot_id) or {"name": guest_bot_id}
|
||||
host_name = host_bot.get("name", host_bot_id) or host_bot_id
|
||||
guest_name = guest_bot.get("name", guest_bot_id) or guest_bot_id
|
||||
group_summary = (
|
||||
f"{host_name}: {host_pov.summary}\n\n"
|
||||
f"{guest_name}: {guest_pov.summary}"
|
||||
)
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="group_node_updated",
|
||||
payload={
|
||||
"chat_id": chat_id,
|
||||
"summary": group_summary,
|
||||
"dynamic": "",
|
||||
},
|
||||
)
|
||||
|
||||
return host_pov
|
||||
|
||||
@@ -258,3 +258,425 @@ async def test_apply_scene_close_summary_updates_memories_and_edge(tmp_path):
|
||||
|
||||
# Knowledge fact appended via edge_update.
|
||||
assert any("deadline" in fact for fact in edge["knowledge"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T45: per-POV summaries on close for each present witness.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _bot_payload(bot_id: str, name: str, persona: str = "thoughtful") -> dict:
|
||||
return {
|
||||
"id": bot_id,
|
||||
"name": name,
|
||||
"persona": persona,
|
||||
"voice_samples": [],
|
||||
"traits": [],
|
||||
"backstory": "",
|
||||
"initial_relationship_to_you": "",
|
||||
"kickoff_prose": "",
|
||||
}
|
||||
|
||||
|
||||
def _seed_single_bot_scene(conn) -> None:
|
||||
"""Seed the canonical Phase 1 single-bot scene used by the regression test."""
|
||||
append_event(conn, kind="bot_authored", payload=_bot_payload("bot_a", "BotA"))
|
||||
append_event(
|
||||
conn,
|
||||
kind="you_authored",
|
||||
payload={"name": "Me", "pronouns": "they/them", "persona": "engineer"},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="chat_created",
|
||||
payload={
|
||||
"id": "chat_bot_a",
|
||||
"host_bot_id": "bot_a",
|
||||
"initial_time": "2026-04-26T20:00:00+00:00",
|
||||
"narrative_anchor": "Day 1",
|
||||
"weather": "",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="container_created",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"name": "office",
|
||||
"type": "workplace",
|
||||
"properties": {},
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="scene_opened",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"container_id": 1,
|
||||
"started_at": "2026-04-26T20:00:00+00:00",
|
||||
"participants": ["you", "bot_a"],
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="edge_update",
|
||||
payload={
|
||||
"source_id": "bot_a",
|
||||
"target_id": "you",
|
||||
"chat_id": "chat_bot_a",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="memory_written",
|
||||
payload={
|
||||
"owner_id": "bot_a",
|
||||
"chat_id": "chat_bot_a",
|
||||
"scene_id": 1,
|
||||
"pov_summary": "Original raw narrative (host)",
|
||||
"witness_you": 1,
|
||||
"witness_host": 1,
|
||||
"witness_guest": 0,
|
||||
"significance": 1,
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="user_turn",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"prose": "Quick chat about the deadline",
|
||||
"segments": [],
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="assistant_turn",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"speaker_id": "bot_a",
|
||||
"text": "It's going to be okay.",
|
||||
"truncated": False,
|
||||
"user_turn_id": 1,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _seed_two_bot_scene(conn, *, with_group_node: bool = False) -> None:
|
||||
"""Seed a host+guest scene with bot_a (host) and bot_b (guest), plus a
|
||||
memory row per bot owner so each per-POV update has something to rewrite,
|
||||
and seeded directed edges from each bot to ``you`` so each edge_summary
|
||||
update has a row to operate on. Optionally seeds the group_node row too.
|
||||
"""
|
||||
append_event(conn, kind="bot_authored", payload=_bot_payload("bot_a", "BotA"))
|
||||
append_event(conn, kind="bot_authored", payload=_bot_payload("bot_b", "BotB"))
|
||||
append_event(
|
||||
conn,
|
||||
kind="you_authored",
|
||||
payload={"name": "Me", "pronouns": "they/them", "persona": "engineer"},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="chat_created",
|
||||
payload={
|
||||
"id": "chat_bot_a",
|
||||
"host_bot_id": "bot_a",
|
||||
"guest_bot_id": "bot_b",
|
||||
"initial_time": "2026-04-26T20:00:00+00:00",
|
||||
"narrative_anchor": "Day 1",
|
||||
"weather": "",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="container_created",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"name": "office",
|
||||
"type": "workplace",
|
||||
"properties": {},
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="scene_opened",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"container_id": 1,
|
||||
"started_at": "2026-04-26T20:00:00+00:00",
|
||||
"participants": ["you", "bot_a", "bot_b"],
|
||||
},
|
||||
)
|
||||
# Seed edges in both bot -> you directions so the edge_summary updates
|
||||
# have rows to target.
|
||||
append_event(
|
||||
conn,
|
||||
kind="edge_update",
|
||||
payload={
|
||||
"source_id": "bot_a",
|
||||
"target_id": "you",
|
||||
"chat_id": "chat_bot_a",
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="edge_update",
|
||||
payload={
|
||||
"source_id": "bot_b",
|
||||
"target_id": "you",
|
||||
"chat_id": "chat_bot_a",
|
||||
},
|
||||
)
|
||||
# One memory per witness, scene 1.
|
||||
append_event(
|
||||
conn,
|
||||
kind="memory_written",
|
||||
payload={
|
||||
"owner_id": "bot_a",
|
||||
"chat_id": "chat_bot_a",
|
||||
"scene_id": 1,
|
||||
"pov_summary": "Original raw narrative (host)",
|
||||
"witness_you": 1,
|
||||
"witness_host": 1,
|
||||
"witness_guest": 1,
|
||||
"significance": 1,
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="memory_written",
|
||||
payload={
|
||||
"owner_id": "bot_b",
|
||||
"chat_id": "chat_bot_a",
|
||||
"scene_id": 1,
|
||||
"pov_summary": "Original raw narrative (guest)",
|
||||
"witness_you": 1,
|
||||
"witness_host": 1,
|
||||
"witness_guest": 1,
|
||||
"significance": 1,
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="user_turn",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"prose": "Three of us in the office.",
|
||||
"segments": [],
|
||||
},
|
||||
)
|
||||
append_event(
|
||||
conn,
|
||||
kind="assistant_turn",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"speaker_id": "bot_a",
|
||||
"text": "Glad you're both here.",
|
||||
"truncated": False,
|
||||
"user_turn_id": 1,
|
||||
},
|
||||
)
|
||||
if with_group_node:
|
||||
append_event(
|
||||
conn,
|
||||
kind="group_node_initialized",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"members": ["you", "bot_a", "bot_b"],
|
||||
"summary": "",
|
||||
"dynamic": "",
|
||||
"threads": [],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_with_no_guest_matches_phase1(tmp_path):
|
||||
"""Regression: when guest_bot_id is None, the close summary path runs
|
||||
summarize_scene exactly once and rewrites the host's memory + host->you
|
||||
edge in place — same as Phase 1 behavior."""
|
||||
db = tmp_path / "t.db"
|
||||
apply_migrations(db)
|
||||
canned = json.dumps(
|
||||
{
|
||||
"summary": "BotA helped you talk through the deadline anxiety.",
|
||||
"knowledge_facts": ["Deadline next Friday."],
|
||||
"relationship_summary": "BotA leaned in supportively.",
|
||||
}
|
||||
)
|
||||
with open_db(db) as conn:
|
||||
_seed_single_bot_scene(conn)
|
||||
project(conn)
|
||||
|
||||
# canned has 2 entries to detect any over-call; the assertion below
|
||||
# confirms only one was consumed.
|
||||
client = MockLLMClient(canned=[canned, canned])
|
||||
await apply_scene_close_summary(
|
||||
conn,
|
||||
client,
|
||||
classifier_model="x",
|
||||
chat_id="chat_bot_a",
|
||||
scene_id=1,
|
||||
host_bot_id="bot_a",
|
||||
)
|
||||
|
||||
# Exactly one classifier call -> exactly one canned entry consumed,
|
||||
# leaving the second untouched.
|
||||
assert len(client._canned) == 1
|
||||
|
||||
# Host memory rewritten with the per-POV summary content.
|
||||
new_pov = conn.execute(
|
||||
"SELECT pov_summary FROM memories "
|
||||
"WHERE owner_id = 'bot_a' AND scene_id = 1"
|
||||
).fetchone()[0]
|
||||
assert "BotA helped" in new_pov
|
||||
|
||||
# host->you edge summary rewritten with the relationship_summary.
|
||||
from chat.state.edges import get_edge
|
||||
|
||||
edge = get_edge(conn, "bot_a", "you")
|
||||
assert "supportively" in edge["summary"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_with_guest_calls_summarize_twice(tmp_path):
|
||||
"""When a guest is present, summarize_scene runs once per witness
|
||||
(host + guest) and each bot's memory rewrite uses its own POV summary."""
|
||||
db = tmp_path / "t.db"
|
||||
apply_migrations(db)
|
||||
host_canned = json.dumps(
|
||||
{
|
||||
"summary": "BotA noticed BotB warming up to you.",
|
||||
"knowledge_facts": ["You sketched on the whiteboard."],
|
||||
"relationship_summary": "BotA felt steady around you.",
|
||||
}
|
||||
)
|
||||
guest_canned = json.dumps(
|
||||
{
|
||||
"summary": "BotB found the office quieter than expected.",
|
||||
"knowledge_facts": ["You prefer black coffee."],
|
||||
"relationship_summary": "BotB warmed up to you a little.",
|
||||
}
|
||||
)
|
||||
with open_db(db) as conn:
|
||||
_seed_two_bot_scene(conn)
|
||||
project(conn)
|
||||
|
||||
client = MockLLMClient(canned=[host_canned, guest_canned])
|
||||
await apply_scene_close_summary(
|
||||
conn,
|
||||
client,
|
||||
classifier_model="x",
|
||||
chat_id="chat_bot_a",
|
||||
scene_id=1,
|
||||
host_bot_id="bot_a",
|
||||
)
|
||||
|
||||
# Both canned entries consumed -> classifier ran twice.
|
||||
assert client._canned == []
|
||||
|
||||
# Host memory carries the host's per-POV summary; guest memory
|
||||
# carries the guest's.
|
||||
host_pov = conn.execute(
|
||||
"SELECT pov_summary FROM memories "
|
||||
"WHERE owner_id = 'bot_a' AND scene_id = 1"
|
||||
).fetchone()[0]
|
||||
guest_pov = conn.execute(
|
||||
"SELECT pov_summary FROM memories "
|
||||
"WHERE owner_id = 'bot_b' AND scene_id = 1"
|
||||
).fetchone()[0]
|
||||
assert "BotA noticed" in host_pov
|
||||
assert "BotB found" in guest_pov
|
||||
assert host_pov != guest_pov
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_with_guest_updates_both_edges(tmp_path):
|
||||
"""Both bot->you edges receive their own relationship_summary on close."""
|
||||
db = tmp_path / "t.db"
|
||||
apply_migrations(db)
|
||||
host_canned = json.dumps(
|
||||
{
|
||||
"summary": "BotA noticed BotB warming up.",
|
||||
"knowledge_facts": [],
|
||||
"relationship_summary": "BotA felt steady around you.",
|
||||
}
|
||||
)
|
||||
guest_canned = json.dumps(
|
||||
{
|
||||
"summary": "BotB warmed to the office.",
|
||||
"knowledge_facts": [],
|
||||
"relationship_summary": "BotB warmed up to you a little.",
|
||||
}
|
||||
)
|
||||
with open_db(db) as conn:
|
||||
_seed_two_bot_scene(conn)
|
||||
project(conn)
|
||||
|
||||
client = MockLLMClient(canned=[host_canned, guest_canned])
|
||||
await apply_scene_close_summary(
|
||||
conn,
|
||||
client,
|
||||
classifier_model="x",
|
||||
chat_id="chat_bot_a",
|
||||
scene_id=1,
|
||||
host_bot_id="bot_a",
|
||||
)
|
||||
|
||||
from chat.state.edges import get_edge
|
||||
|
||||
edge_h2y = get_edge(conn, "bot_a", "you")
|
||||
edge_g2y = get_edge(conn, "bot_b", "you")
|
||||
assert "steady" in edge_h2y["summary"]
|
||||
assert "warmed up" in edge_g2y["summary"]
|
||||
# Per-POV; the two edges did not collapse onto the same text.
|
||||
assert edge_h2y["summary"] != edge_g2y["summary"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_with_group_node_updates_group_summary(tmp_path):
|
||||
"""When a group_node row exists, scene close emits group_node_updated
|
||||
with a non-empty summary that mentions both bots' names (v2 naive
|
||||
concat of per-POV summaries)."""
|
||||
db = tmp_path / "t.db"
|
||||
apply_migrations(db)
|
||||
import chat.state.group_node # noqa: F401 -- register handlers
|
||||
|
||||
host_canned = json.dumps(
|
||||
{
|
||||
"summary": "BotA appreciated the calm.",
|
||||
"knowledge_facts": [],
|
||||
"relationship_summary": "BotA felt steady.",
|
||||
}
|
||||
)
|
||||
guest_canned = json.dumps(
|
||||
{
|
||||
"summary": "BotB found the room friendly.",
|
||||
"knowledge_facts": [],
|
||||
"relationship_summary": "BotB warmed up.",
|
||||
}
|
||||
)
|
||||
with open_db(db) as conn:
|
||||
_seed_two_bot_scene(conn, with_group_node=True)
|
||||
project(conn)
|
||||
|
||||
client = MockLLMClient(canned=[host_canned, guest_canned])
|
||||
await apply_scene_close_summary(
|
||||
conn,
|
||||
client,
|
||||
classifier_model="x",
|
||||
chat_id="chat_bot_a",
|
||||
scene_id=1,
|
||||
host_bot_id="bot_a",
|
||||
)
|
||||
|
||||
from chat.state.group_node import get_group_node
|
||||
|
||||
gn = get_group_node(conn, "chat_bot_a")
|
||||
assert gn is not None
|
||||
assert gn["summary"] # non-empty
|
||||
# Naive concat surfaces both bot names in the group summary.
|
||||
assert "BotA" in gn["summary"]
|
||||
assert "BotB" in gn["summary"]
|
||||
# Phase 2 v2 keeps dynamic empty (Phase 3 polishes).
|
||||
assert gn["dynamic"] == ""
|
||||
|
||||
Reference in New Issue
Block a user