feat: event-completion promotion service (T56)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""Event-completion promotion (T56).
|
||||
|
||||
When an event reaches ``status='completed'``, read its ``props_json``
|
||||
and emit promotion events into the appropriate state stores.
|
||||
Synchronous, no LLM. Skips when the event status is not ``completed``
|
||||
(cancelled / expired terminate the event without promoting).
|
||||
|
||||
Props recognized:
|
||||
|
||||
- ``acquired_objects: list[str]`` — emits a ``manual_edit`` with
|
||||
``target_kind="memory_pov_summary"`` per object on the host's memory
|
||||
row, recording the acquisition. Phase 3 is a stub: it requires both
|
||||
``host_bot_id`` and ``host_memory_id`` (an existing memories.id) to
|
||||
be present in props; missing either skips that object cleanly.
|
||||
Phase 4 will introduce a real inventory schema.
|
||||
|
||||
- ``knowledge_facts: list[{owner_id, target_id, fact}]`` — emits an
|
||||
``edge_update`` event on the directed ``owner_id -> target_id`` edge
|
||||
with the fact appended to ``knowledge_facts``. The ``edge_update``
|
||||
projector accepts ``knowledge_facts`` as a list and extends the
|
||||
edge's stored ``knowledge_json``.
|
||||
|
||||
- ``relationship_change: {summary, source_id, target_id}`` — emits a
|
||||
``manual_edit`` with ``target_kind="edge_summary"`` overwriting the
|
||||
edge's ``summary`` field on the directed pair.
|
||||
|
||||
Anything else stays in the closed event record (the projector kept
|
||||
the row; no further promotion).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlite3 import Connection
|
||||
|
||||
from chat.eventlog.log import append_and_apply
|
||||
from chat.state.events import get_event
|
||||
|
||||
|
||||
def promote_completed_event(
|
||||
conn: Connection,
|
||||
*,
|
||||
event_id: str,
|
||||
chat_id: str,
|
||||
chat_clock_at: str | None,
|
||||
) -> dict:
|
||||
"""Read the completed event's props and emit promotion events.
|
||||
|
||||
Returns a dict of counts keyed by promoted artifact:
|
||||
``{"acquired_objects", "knowledge_facts", "relationship_change"}``.
|
||||
Skips silently if the event row is missing or its status is not
|
||||
``completed`` — cancelled / expired events terminate without any
|
||||
promotion.
|
||||
"""
|
||||
counts = {
|
||||
"acquired_objects": 0,
|
||||
"knowledge_facts": 0,
|
||||
"relationship_change": 0,
|
||||
}
|
||||
|
||||
event = get_event(conn, event_id)
|
||||
if event is None or event["status"] != "completed":
|
||||
return counts
|
||||
|
||||
props = event.get("props") or {}
|
||||
|
||||
# acquired_objects: each becomes a memory_pov_summary edit (Phase 3
|
||||
# stub). The manual_edit projector requires a valid memory rowid as
|
||||
# ``target_id`` (it does ``int(target_id)``), so skip cleanly when
|
||||
# neither a host_bot_id nor a host_memory_id is supplied.
|
||||
host_bot_id = props.get("host_bot_id")
|
||||
host_memory_id = props.get("host_memory_id")
|
||||
for obj in props.get("acquired_objects", []) or []:
|
||||
if host_bot_id is None or host_memory_id is None:
|
||||
continue
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="manual_edit",
|
||||
payload={
|
||||
"target_kind": "memory_pov_summary",
|
||||
"target_id": host_memory_id,
|
||||
"owner_id": host_bot_id,
|
||||
"chat_id": chat_id,
|
||||
"prior_value": "",
|
||||
"new_value": f"Acquired: {obj}",
|
||||
"source": "event_promotion",
|
||||
"event_id": event_id,
|
||||
"chat_clock_at": chat_clock_at,
|
||||
},
|
||||
)
|
||||
counts["acquired_objects"] += 1
|
||||
|
||||
# knowledge_facts: each becomes an edge_update appending the fact.
|
||||
for fact_entry in props.get("knowledge_facts", []) or []:
|
||||
owner_id = fact_entry.get("owner_id")
|
||||
target_id = fact_entry.get("target_id")
|
||||
fact = fact_entry.get("fact", "")
|
||||
if not owner_id or not target_id or not fact:
|
||||
continue
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="edge_update",
|
||||
payload={
|
||||
"source_id": owner_id,
|
||||
"target_id": target_id,
|
||||
"chat_id": chat_id,
|
||||
"affinity_delta": 0,
|
||||
"trust_delta": 0,
|
||||
"knowledge_facts": [fact],
|
||||
"last_interaction_at": chat_clock_at,
|
||||
"last_interaction_chat_id": chat_id,
|
||||
"source": "event_promotion",
|
||||
"event_id": event_id,
|
||||
},
|
||||
)
|
||||
counts["knowledge_facts"] += 1
|
||||
|
||||
# relationship_change: edge_summary manual_edit on the directed pair.
|
||||
# The manual_edit projector for ``edge_summary`` keys on a
|
||||
# ``target_id`` dict ``{source_id, target_id}`` (see
|
||||
# chat/state/manual_edit.py); we shape the payload to match.
|
||||
rc = props.get("relationship_change") or {}
|
||||
if rc:
|
||||
source_id = rc.get("source_id")
|
||||
rc_target_id = rc.get("target_id")
|
||||
summary = rc.get("summary", "")
|
||||
if source_id and rc_target_id and summary:
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="manual_edit",
|
||||
payload={
|
||||
"target_kind": "edge_summary",
|
||||
"target_id": {
|
||||
"source_id": source_id,
|
||||
"target_id": rc_target_id,
|
||||
},
|
||||
"chat_id": chat_id,
|
||||
"prior_value": "",
|
||||
"new_value": summary,
|
||||
"source": "event_promotion",
|
||||
"event_id": event_id,
|
||||
"chat_clock_at": chat_clock_at,
|
||||
},
|
||||
)
|
||||
counts["relationship_change"] += 1
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
__all__ = ["promote_completed_event"]
|
||||
Reference in New Issue
Block a user