263 lines
10 KiB
Python
263 lines
10 KiB
Python
"""Session object ledger for PMFlow AI turn runtime.
|
|
|
|
The ledger is a customer-safe working memory of objects the assistant actually
|
|
showed, focused, or acted on. It is not a business write surface and it does not
|
|
infer intent from prose.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
LEDGER_PACKET_VERSION = "pmflow_ai_object_ledger.v1"
|
|
|
|
|
|
def build_object_ledger_from_payload(
|
|
*,
|
|
payload: dict[str, Any],
|
|
previous_ledger: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
previous = previous_ledger if isinstance(previous_ledger, dict) else {}
|
|
shown = _extract_objects_from_payload(payload)
|
|
previous_recent = [
|
|
item for item in list(previous.get("recent_objects") or []) if isinstance(item, dict)
|
|
]
|
|
recent = _dedupe_objects([*shown, *previous_recent])[:12]
|
|
current_focus = _focus_from_payload(payload, shown=shown)
|
|
if current_focus is None and len(shown) == 1:
|
|
current_focus = shown[0]
|
|
if current_focus is None:
|
|
prior_focus = previous.get("current_focus_object")
|
|
if isinstance(prior_focus, dict) and _object_ref(prior_focus):
|
|
current_focus = _normalize_visible_object(prior_focus)
|
|
active_topic = _active_topic_from_payload(payload=payload, current_focus=current_focus, previous=previous)
|
|
return {
|
|
"packet_version": LEDGER_PACKET_VERSION,
|
|
"visibility": "customer",
|
|
"current_focus_object": current_focus,
|
|
"last_shown_objects": shown[:12],
|
|
"recent_objects": recent,
|
|
"active_topic": active_topic,
|
|
}
|
|
|
|
|
|
def object_ledger_from_context_capsules(value: Any) -> dict[str, Any]:
|
|
rows = value if isinstance(value, list) else []
|
|
for item in rows:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
if str(item.get("packet_version") or "") == LEDGER_PACKET_VERSION:
|
|
return dict(item)
|
|
if str(item.get("capsule_type") or "") == "pmflow_ai_object_ledger":
|
|
return dict(item)
|
|
return {}
|
|
|
|
|
|
def merge_object_ledger_into_context_packet(
|
|
*,
|
|
context_packet: dict[str, Any] | None,
|
|
object_ledger: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
packet = dict(context_packet or {})
|
|
if not object_ledger:
|
|
return packet
|
|
current = object_ledger.get("current_focus_object")
|
|
shown = [item for item in list(object_ledger.get("last_shown_objects") or []) if isinstance(item, dict)]
|
|
recent = [item for item in list(object_ledger.get("recent_objects") or []) if isinstance(item, dict)]
|
|
if isinstance(current, dict) and _object_ref(current):
|
|
packet["current_focus_object"] = _normalize_visible_object(current)
|
|
if shown:
|
|
packet["last_shown_objects"] = [_normalize_visible_object(item) for item in shown if _object_ref(item)]
|
|
packet["visible_objects"] = _dedupe_objects([
|
|
*list(packet.get("visible_objects") or []),
|
|
*packet["last_shown_objects"],
|
|
])
|
|
elif recent and not packet.get("last_shown_objects"):
|
|
packet["last_shown_objects"] = [_normalize_visible_object(item) for item in recent[:6] if _object_ref(item)]
|
|
packet.setdefault("packet_version", "customer_interaction_context.v1")
|
|
packet.setdefault("visibility", "customer")
|
|
return packet
|
|
|
|
|
|
def upsert_object_ledger_capsule(
|
|
*,
|
|
capsules: Any,
|
|
object_ledger: dict[str, Any],
|
|
) -> list[dict[str, Any]]:
|
|
existing = [dict(item) for item in list(capsules or []) if isinstance(item, dict)]
|
|
out = [
|
|
item
|
|
for item in existing
|
|
if str(item.get("packet_version") or "") != LEDGER_PACKET_VERSION
|
|
and str(item.get("capsule_type") or "") != "pmflow_ai_object_ledger"
|
|
]
|
|
if object_ledger:
|
|
row = dict(object_ledger)
|
|
row["capsule_type"] = "pmflow_ai_object_ledger"
|
|
out.insert(0, row)
|
|
return out[:12]
|
|
|
|
|
|
def ledger_focus_as_turn_object(object_ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
ledger = object_ledger if isinstance(object_ledger, dict) else {}
|
|
current = ledger.get("current_focus_object")
|
|
if not isinstance(current, dict) or not _object_ref(current):
|
|
return {}
|
|
obj = _normalize_visible_object(current)
|
|
return {
|
|
"object_type": obj.get("object_type") or "unknown",
|
|
"object_ref": obj.get("object_ref") or "",
|
|
"title": obj.get("title") or "",
|
|
"binding_strength": "session_object_ledger",
|
|
"role": "current_focus",
|
|
}
|
|
|
|
|
|
def _extract_objects_from_payload(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
out: list[dict[str, Any]] = []
|
|
turn_state = payload.get("pmflow_ai_turn_state") if isinstance(payload.get("pmflow_ai_turn_state"), dict) else {}
|
|
draft = turn_state.get("customer_packet_draft") if isinstance(turn_state.get("customer_packet_draft"), dict) else {}
|
|
for item in list(draft.get("items") or []):
|
|
if isinstance(item, dict):
|
|
out.append(_normalize_visible_object(item))
|
|
for observation in list(turn_state.get("observations") or []):
|
|
if not isinstance(observation, dict):
|
|
continue
|
|
for item in list(observation.get("items") or []):
|
|
if isinstance(item, dict):
|
|
out.append(_normalize_visible_object(item))
|
|
answer_canvas = payload.get("answer_canvas") if isinstance(payload.get("answer_canvas"), dict) else {}
|
|
for item in list(answer_canvas.get("object_refs") or []):
|
|
if isinstance(item, dict):
|
|
out.append(_normalize_visible_object(item))
|
|
context = (
|
|
payload.get("customer_interaction_context_packet")
|
|
if isinstance(payload.get("customer_interaction_context_packet"), dict)
|
|
else {}
|
|
)
|
|
for key in ("visible_objects", "last_shown_objects"):
|
|
for item in list(context.get(key) or []):
|
|
if isinstance(item, dict):
|
|
out.append(_normalize_visible_object(item))
|
|
return _dedupe_objects([item for item in out if _object_ref(item) and str(item.get("title") or "").strip()])
|
|
|
|
|
|
def _focus_from_payload(payload: dict[str, Any], *, shown: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
context = (
|
|
payload.get("customer_interaction_context_packet")
|
|
if isinstance(payload.get("customer_interaction_context_packet"), dict)
|
|
else {}
|
|
)
|
|
focus = context.get("current_focus_object")
|
|
if isinstance(focus, dict) and _object_ref(focus):
|
|
return _normalize_visible_object(focus)
|
|
turn_state = payload.get("pmflow_ai_turn_state") if isinstance(payload.get("pmflow_ai_turn_state"), dict) else {}
|
|
object_state = turn_state.get("object_state") if isinstance(turn_state.get("object_state"), dict) else {}
|
|
primary = object_state.get("primary_object") if isinstance(object_state.get("primary_object"), dict) else {}
|
|
binding = str(primary.get("binding_strength") or "").strip().lower()
|
|
if _object_ref(primary) and binding not in {"", "none", "context", "visible_context"}:
|
|
return _normalize_visible_object(primary)
|
|
if len(shown) == 1:
|
|
return shown[0]
|
|
return None
|
|
|
|
|
|
def _active_topic_from_payload(
|
|
*,
|
|
payload: dict[str, Any],
|
|
current_focus: dict[str, Any] | None,
|
|
previous: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
prior = previous.get("active_topic") if isinstance(previous.get("active_topic"), dict) else {}
|
|
if current_focus and _object_ref(current_focus):
|
|
answer_type = ""
|
|
packet = payload.get("customer_evidence_packet") if isinstance(payload.get("customer_evidence_packet"), dict) else {}
|
|
answer_type = str(packet.get("answer_type") or payload.get("answer_mode") or "").strip()
|
|
return {
|
|
"topic_type": answer_type[:80] or str(prior.get("topic_type") or "object_followup"),
|
|
"primary_object_ref": current_focus.get("object_ref"),
|
|
"primary_object_title": current_focus.get("title"),
|
|
}
|
|
return dict(prior)
|
|
|
|
|
|
def _normalize_visible_object(value: dict[str, Any]) -> dict[str, Any]:
|
|
object_type = _text(value.get("object_type") or value.get("type"), limit=64) or "object"
|
|
object_ref = _object_ref(value)
|
|
object_id = _object_id(value, object_ref=object_ref)
|
|
title = _text(value.get("title") or value.get("project_title") or value.get("name"), limit=160)
|
|
status = _text(value.get("status"), limit=80)
|
|
href = _text(value.get("href"), limit=240)
|
|
summary = _text(value.get("display_summary") or value.get("summary"), limit=800)
|
|
out: dict[str, Any] = {
|
|
"object_type": object_type,
|
|
"object_ref": object_ref,
|
|
"title": title,
|
|
}
|
|
if object_id is not None:
|
|
out["object_id"] = object_id
|
|
if status:
|
|
out["status"] = status
|
|
if href:
|
|
out["href"] = href
|
|
if summary:
|
|
out["display_summary"] = summary
|
|
return out
|
|
|
|
|
|
def _dedupe_objects(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
out: list[dict[str, Any]] = []
|
|
seen: set[str] = set()
|
|
for raw in rows:
|
|
if not isinstance(raw, dict):
|
|
continue
|
|
item = _normalize_visible_object(raw)
|
|
ref = _object_ref(item)
|
|
if not ref or ref in seen or not str(item.get("title") or "").strip():
|
|
continue
|
|
seen.add(ref)
|
|
out.append(item)
|
|
return out
|
|
|
|
|
|
def _object_ref(value: dict[str, Any]) -> str:
|
|
ref = _text(value.get("object_ref") or value.get("objectRef"), limit=120)
|
|
if ref:
|
|
return ref
|
|
object_type = _text(value.get("object_type") or value.get("type"), limit=64)
|
|
object_id = value.get("object_id") or value.get("objectId") or value.get("id") or value.get("project_id")
|
|
if object_type and object_id is not None:
|
|
return f"{object_type}:{object_id}"
|
|
return ""
|
|
|
|
|
|
def _object_id(value: dict[str, Any], *, object_ref: str) -> int | None:
|
|
for key in ("object_id", "objectId", "id", "project_id"):
|
|
try:
|
|
if value.get(key) is not None:
|
|
return int(value.get(key))
|
|
except Exception:
|
|
continue
|
|
if ":" in object_ref:
|
|
tail = object_ref.rsplit(":", 1)[-1]
|
|
try:
|
|
return int(tail)
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
|
|
def _text(value: Any, *, limit: int) -> str:
|
|
return " ".join(str(value or "").strip().split())[:limit]
|
|
|
|
|
|
__all__ = [
|
|
"LEDGER_PACKET_VERSION",
|
|
"build_object_ledger_from_payload",
|
|
"ledger_focus_as_turn_object",
|
|
"merge_object_ledger_into_context_packet",
|
|
"object_ledger_from_context_capsules",
|
|
"upsert_object_ledger_capsule",
|
|
]
|