correlator: address #102 review — 0-based segment labels, never-empty slice
drb-correlation-review on the prior commit flagged two ways the per-scene transcript could silently fall back to the whole-call text: 1. _build_transcript_block numbered transmissions "1." while the prompt says "0-based indices" — a model echoing the labels it saw returned 1-based indices, shifting every scene's slice by one. Labels are now "0." to match the documented contract (also fixes the same latent skew in _build_scene_embed_text / #80). 2. An empty join (bad / out-of-range / non-int indices) hit `transcript or call_doc.get(...)` in _build_context and fell back to the whole-call transcript — re-opening the leak exactly when indices are wrong. The slice now falls back to this call's own whole transcript *before* _build_context sees it, so it is never "". Non-int and negative indices are rejected rather than raising. Slice logic extracted to `_scene_transcript_text` with a dedicated test file (4 cases: subset, corrected-wins, no-indices fallback, bad-indices fallback). Call-doc fallback kept (sweep / no-scene path) per the review. Also restored the `-> ` spacing lost in the prior commit's kwarg edit. Full c2-core suite green: 300 passed (sandboxed venv). Still DO NOT MERGE until the measurement window closes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
ef1e3d7f9d
commit
7189ba03e4
@@ -671,7 +671,7 @@ async def correlate_call(
|
|||||||
embedding: Optional[list] = None,
|
embedding: Optional[list] = None,
|
||||||
severity: Optional[str] = None,
|
severity: Optional[str] = None,
|
||||||
transcript: Optional[str] = None,
|
transcript: Optional[str] = None,
|
||||||
) ->Optional[str]:
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Link call_id to an existing incident or create a new one.
|
Link call_id to an existing incident or create a new one.
|
||||||
Thin wrapper: builds context → runs rules decision → commits.
|
Thin wrapper: builds context → runs rules decision → commits.
|
||||||
@@ -712,7 +712,7 @@ async def preview_correlation(
|
|||||||
embedding: Optional[list] = None,
|
embedding: Optional[list] = None,
|
||||||
severity: Optional[str] = None,
|
severity: Optional[str] = None,
|
||||||
transcript: Optional[str] = None,
|
transcript: Optional[str] = None,
|
||||||
) ->dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Run the rules engine and return the decision WITHOUT committing to Firestore.
|
Run the rules engine and return the decision WITHOUT committing to Firestore.
|
||||||
|
|
||||||
|
|||||||
@@ -337,18 +337,9 @@ async def extract_scenes(
|
|||||||
)
|
)
|
||||||
embedding = await asyncio.to_thread(_sync_embed, scene_text)
|
embedding = await asyncio.to_thread(_sync_embed, scene_text)
|
||||||
|
|
||||||
# This scene's own words, unprefixed — corrected text if we have it,
|
scene_transcript = _scene_transcript_text(
|
||||||
# else the raw segments this scene owns, else (single-scene) the whole
|
transcript, segments, segment_indices, transcript_corrected
|
||||||
# transcript. The correlator's LLM tier reads this per scene instead of
|
|
||||||
# the call doc's whole-call transcript (server-26#102).
|
|
||||||
if transcript_corrected:
|
|
||||||
scene_transcript = transcript_corrected
|
|
||||||
elif segments and segment_indices:
|
|
||||||
scene_transcript = " ".join(
|
|
||||||
segments[i]["text"] for i in segment_indices if i < len(segments)
|
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
scene_transcript = transcript
|
|
||||||
|
|
||||||
processed.append({
|
processed.append({
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
@@ -585,11 +576,49 @@ def _municipality_from_tg(tg_name: Optional[str]) -> Optional[str]:
|
|||||||
def _build_transcript_block(transcript: str, segments: Optional[list[dict]]) -> str:
|
def _build_transcript_block(transcript: str, segments: Optional[list[dict]]) -> str:
|
||||||
"""Format transcript as numbered transmissions if segments are available."""
|
"""Format transcript as numbered transmissions if segments are available."""
|
||||||
if segments and len(segments) > 1:
|
if segments and len(segments) > 1:
|
||||||
lines = [f"{i+1}. [{s['start']}s] {s['text']}" for i, s in enumerate(segments)]
|
# 0-based labels, matching the prompt's "0-based indices into the
|
||||||
|
# numbered transmissions" — the model echoes these back as
|
||||||
|
# `segment_indices`, which _build_scene_embed_text and the per-scene
|
||||||
|
# `transcript` (server-26#102) then slice with directly.
|
||||||
|
lines = [f"{i}. [{s['start']}s] {s['text']}" for i, s in enumerate(segments)]
|
||||||
return f"Transmissions ({len(segments)}):\n" + "\n".join(lines)
|
return f"Transmissions ({len(segments)}):\n" + "\n".join(lines)
|
||||||
return f"Transcript:\n{transcript}"
|
return f"Transcript:\n{transcript}"
|
||||||
|
|
||||||
|
|
||||||
|
def _scene_transcript_text(
|
||||||
|
transcript: str,
|
||||||
|
segments: Optional[list[dict]],
|
||||||
|
segment_indices: Optional[list[int]],
|
||||||
|
transcript_corrected: Optional[str],
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
This scene's own words, unprefixed — the segments it owns, joined.
|
||||||
|
|
||||||
|
server-26#102: the correlator's LLM tier reads this per scene instead of
|
||||||
|
the call doc's whole-call transcript, so on a multi-scene call scene N is
|
||||||
|
no longer judged against scenes 1..N-1's text.
|
||||||
|
|
||||||
|
Never returns "". Anything that would leave the slice empty — no
|
||||||
|
`segment_indices` (a single-segment call is never numbered by
|
||||||
|
`_build_transcript_block`), or indices that are out of range / not ints —
|
||||||
|
falls back to the whole-call transcript, which for a single-scene call is
|
||||||
|
the same text and for a mis-sliced multi-scene call is at least this
|
||||||
|
call's own words. `_sync_extract`'s prompt documents 0-based indices and
|
||||||
|
`_build_transcript_block` numbers to match, so no base normalisation here.
|
||||||
|
"""
|
||||||
|
if transcript_corrected:
|
||||||
|
return transcript_corrected
|
||||||
|
if segments and segment_indices:
|
||||||
|
joined = " ".join(
|
||||||
|
segments[i]["text"]
|
||||||
|
for i in segment_indices
|
||||||
|
if isinstance(i, int) and 0 <= i < len(segments)
|
||||||
|
)
|
||||||
|
if joined:
|
||||||
|
return joined
|
||||||
|
return transcript
|
||||||
|
|
||||||
|
|
||||||
def _build_scene_embed_text(
|
def _build_scene_embed_text(
|
||||||
transcript: str,
|
transcript: str,
|
||||||
segments: Optional[list[dict]],
|
segments: Optional[list[dict]],
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
server-26#102 — a scene is correlated on its OWN transcript, not the whole call.
|
||||||
|
|
||||||
|
_scene_transcript_text slices the segments a scene owns. It must never return
|
||||||
|
"" (an empty slice would let incident_correlator._build_context fall back to
|
||||||
|
the call doc's whole-call transcript, re-opening the leak in exactly the case
|
||||||
|
— bad indices — where it matters).
|
||||||
|
"""
|
||||||
|
from app.internal.intelligence import _scene_transcript_text
|
||||||
|
|
||||||
|
SEGS = [
|
||||||
|
{"text": "structure fire, 12 Main"},
|
||||||
|
{"text": "engine 4 responding"},
|
||||||
|
{"text": "traffic stop, plate ABC"},
|
||||||
|
{"text": "one occupant"},
|
||||||
|
]
|
||||||
|
WHOLE = "structure fire, 12 Main engine 4 responding traffic stop, plate ABC one occupant"
|
||||||
|
|
||||||
|
|
||||||
|
def test_scene_owns_a_subset_of_segments():
|
||||||
|
assert _scene_transcript_text(WHOLE, SEGS, [0, 1], None) == "structure fire, 12 Main engine 4 responding"
|
||||||
|
assert _scene_transcript_text(WHOLE, SEGS, [2, 3], None) == "traffic stop, plate ABC one occupant"
|
||||||
|
|
||||||
|
|
||||||
|
def test_corrected_text_wins_when_present():
|
||||||
|
assert _scene_transcript_text(WHOLE, SEGS, [0], "cleaned up text") == "cleaned up text"
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_segment_indices_falls_back_to_whole_call():
|
||||||
|
# single-segment calls are never numbered by _build_transcript_block → null indices
|
||||||
|
assert _scene_transcript_text(WHOLE, SEGS, None, None) == WHOLE
|
||||||
|
assert _scene_transcript_text(WHOLE, None, [0, 1], None) == WHOLE
|
||||||
|
|
||||||
|
|
||||||
|
def test_out_of_range_or_nonint_indices_fall_back_never_empty():
|
||||||
|
assert _scene_transcript_text(WHOLE, SEGS, [9, 10], None) == WHOLE # all out of range
|
||||||
|
assert _scene_transcript_text(WHOLE, SEGS, ["1", "2"], None) == WHOLE # 1-based strings, rejected
|
||||||
|
assert _scene_transcript_text(WHOLE, SEGS, [-1], None) == WHOLE # negative
|
||||||
|
# partial validity: keep what's in range
|
||||||
|
assert _scene_transcript_text(WHOLE, SEGS, [3, 99], None) == "one occupant"
|
||||||
Reference in New Issue
Block a user