Every scene of a multi-scene call correlates independently in upload.py's scene loop, but every scene's corr_debug was written flat onto the same shared call doc — scene 2's write silently clobbered scene 1's corr_path/corr_consensus/etc (#96), and summarizer.py read the whole call's raw transcript per linked call, mixing text from scenes the incident had nothing to do with, while ignoring transcript_corrected entirely (#114). Fix: thread a scene_index from both `for scene in scenes:` loops in upload.py down through _correlate_with_consensus -> incident_correlator.preview_correlation/correlate_call -> _build_context -> ctx["scene_index"]. incident_correlator._apply_and_log now writes, in the same Firestore call: - the existing flat corr_* fields, unchanged (last-scene-wins, the safe backward-compatible default for any reader that doesn't know about `scenes` yet) - a new nested `scenes.<scene_index>` entry with {transcript, incident_id, corr_debug}, via doc_set(..., merge=True). Firestore's DocumentReference.set(data, merge=True) recursively merges nested map fields by key (documented SDK behaviour, not assumed) — a write to scenes.1 merges alongside an existing scenes.0 instead of replacing the whole `scenes` map. scene_index defaults to 0 for every caller with no scene concept (the recorrelation sweep, the no-scenes-extracted orphan-check path), so a plain single-scene call still gets a one-entry `scenes` map equivalent to reading its flat fields today. admin.py's _call_summary exposes the new `scenes` list per call (each entry carrying the same corr_* field names as the flat fields, so the two shapes are interchangeable to the tally); the summary tally now iterates each call's scenes-if-present, else its own flat fields, so a 2-scene call with two different corr_path values counts as two data points instead of one blend. New `scene_decision_count` sits next to `linked_call_count` to make that distinction visible. summarizer.py's _scene_text_for_incident reads a linked call's `scenes` map to find the scene(s) whose corr_debug recorded a link into the specific incident being summarized, joining more than one if several scenes landed in the same incident. Falls back to transcript_corrected-or-transcript for a call doc with no `scenes` field (predates this change) — the one-liner half of #114, worth doing regardless since it stops raw-transcript summaries even for old-schema docs. Does not touch #80/#95/#102's existing ctx-threading fixes (embedding/severity/coords/LLM-prompt-transcript) — correct as-is, out of scope here. Tests: 14 new (test_per_scene_call_doc.py, test_summarizer_scene_transcript.py, additions to test_admin_debug_correlation.py) covering the merge shape, last-scene-wins flat-field backward compat, the admin tally's per-scene vs per-call counting (including old-schema fallback), and the summarizer's scene-specific text selection (including old-schema fallback). Full sandboxed suite: 364 -> 378 passed, all green. Fixes server-26#96, server-26#114 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tbknwttzou4s46PAykmtix
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b7701b6d49
commit
fae84a45c3
@@ -97,8 +97,49 @@ async def debug_correlation(
|
||||
def _strip(doc: dict) -> dict:
|
||||
return {k: v for k, v in doc.items() if k != "embedding"}
|
||||
|
||||
def _call_summary(call: dict) -> dict:
|
||||
def _scene_summary(scene_index: str, scene: dict) -> dict:
|
||||
"""
|
||||
One scene's own correlation record, from the call doc's `scenes` map
|
||||
(server-26#96). Same corr_* field names as _call_summary's flat
|
||||
fields below, deliberately — a scene entry and a scene-less call
|
||||
summary are interchangeable data points to the tally functions.
|
||||
"""
|
||||
corr_debug = scene.get("corr_debug") or {}
|
||||
return {
|
||||
"scene_index": scene_index,
|
||||
"transcript": scene.get("transcript"),
|
||||
"incident_id": scene.get("incident_id"),
|
||||
"corr_path": corr_debug.get("corr_path"),
|
||||
"corr_incident_idle_min": corr_debug.get("corr_incident_idle_min"),
|
||||
"corr_distance_km": corr_debug.get("corr_distance_km"),
|
||||
"corr_score": corr_debug.get("corr_score"),
|
||||
"corr_candidates": corr_debug.get("corr_candidates"),
|
||||
"corr_shared_units": corr_debug.get("corr_shared_units"),
|
||||
"corr_fit_signal": corr_debug.get("corr_fit_signal"),
|
||||
"corr_matched_units": corr_debug.get("corr_matched_units"),
|
||||
"corr_consensus": corr_debug.get("corr_consensus"),
|
||||
"corr_llm_reasoning": corr_debug.get("corr_llm_reasoning"),
|
||||
"corr_llm_action": corr_debug.get("corr_llm_action"),
|
||||
"corr_rules_action": corr_debug.get("corr_rules_action"),
|
||||
"corr_gate_veto": corr_debug.get("corr_gate_veto"),
|
||||
}
|
||||
|
||||
def _call_summary(call: dict) -> dict:
|
||||
# server-26#96 — per-scene records, keyed by scene index as written by
|
||||
# incident_correlator._apply_and_log. Present only on calls that went
|
||||
# through correlation after this fix landed; absent (None) on older
|
||||
# call docs, which the tally below falls back for. Sorted numerically
|
||||
# so a >=10-scene call still reads in scene order.
|
||||
scenes_map = call.get("scenes") or {}
|
||||
scenes = [
|
||||
_scene_summary(idx, s)
|
||||
for idx, s in sorted(
|
||||
scenes_map.items(),
|
||||
key=lambda kv: (0, int(kv[0])) if kv[0].isdigit() else (1, kv[0]),
|
||||
)
|
||||
] or None
|
||||
return {
|
||||
"scenes": scenes,
|
||||
"call_id": call.get("call_id"),
|
||||
"started_at": call.get("started_at"),
|
||||
"ended_at": call.get("ended_at"),
|
||||
@@ -271,6 +312,21 @@ async def debug_correlation(
|
||||
linked = [c for inc in incident_records for c in (inc.get("calls_detail") or [])]
|
||||
call_counts = [len(inc.get("call_ids") or []) for inc in incident_records]
|
||||
|
||||
def _tally_entries(call_summary: dict) -> list:
|
||||
"""
|
||||
server-26#96 — the unit correlation actually decided over is the
|
||||
scene, not the call. A call summary carrying a `scenes` list (every
|
||||
call correlated after this fix) contributes one entry per scene, each
|
||||
with its own corr_path/corr_consensus/etc, instead of the single flat
|
||||
record that used to blend every scene's last write together. A call
|
||||
summary with no `scenes` (a call doc from before this fix) falls back
|
||||
to contributing itself as one entry — identical to pre-#96 behaviour.
|
||||
"""
|
||||
scenes = call_summary.get("scenes")
|
||||
return scenes if scenes else [call_summary]
|
||||
|
||||
scene_entries = [entry for c in linked for entry in _tally_entries(c)]
|
||||
|
||||
def _span_minutes(inc: dict) -> float:
|
||||
stamps = sorted(
|
||||
s for s in ((c.get("started_at") or "") for c in (inc.get("calls_detail") or [])) if s
|
||||
@@ -302,13 +358,20 @@ async def debug_correlation(
|
||||
"ai_systems_only": ai_systems_only,
|
||||
"ai_enabled_system_ids": sorted(ai_systems),
|
||||
"linked_call_count": len(linked),
|
||||
"corr_path": _tally(c.get("corr_path") for c in linked),
|
||||
"corr_fit_signal": _tally(c.get("corr_fit_signal") for c in linked),
|
||||
"corr_consensus": _tally(c.get("corr_consensus") for c in linked),
|
||||
"corr_llm_action": _tally(c.get("corr_llm_action") for c in linked),
|
||||
# server-26#96 — tallied over scene_entries (one entry per scene of a
|
||||
# multi-scene call, from its `scenes` map; one entry per call when it
|
||||
# has none) rather than over `linked` directly, so a 2-scene call
|
||||
# with two different corr_path values counts as two data points
|
||||
# instead of one blended flat record. scene_decision_count makes that
|
||||
# distinction visible next to linked_call_count.
|
||||
"scene_decision_count": len(scene_entries),
|
||||
"corr_path": _tally(e.get("corr_path") for e in scene_entries),
|
||||
"corr_fit_signal": _tally(e.get("corr_fit_signal") for e in scene_entries),
|
||||
"corr_consensus": _tally(e.get("corr_consensus") for e in scene_entries),
|
||||
"corr_llm_action": _tally(e.get("corr_llm_action") for e in scene_entries),
|
||||
# server-26#115 — this IS the number the escape-hatch fix exists to
|
||||
# produce: why each llm=orphan/rules=new call escaped the gate.
|
||||
"corr_gate_veto": _tally(c.get("corr_gate_veto") for c in linked),
|
||||
"corr_gate_veto": _tally(e.get("corr_gate_veto") for e in scene_entries),
|
||||
# server-26#127 — shadow-mode chatter classifier. The target
|
||||
# population is non-events, which land as orphans or single-call
|
||||
# incidents, NOT as a slice of every linked call -- tally `orphans`
|
||||
|
||||
Reference in New Issue
Block a user