Skip to content

Commit d873506

Browse files
author
Latence AI
committed
fix(trace): serialize rollup turns to handler schema
Map trace response objects to RollupTurnInput so SDK rollup calls match the gateway contract.
1 parent deae1ec commit d873506

2 files changed

Lines changed: 50 additions & 4 deletions

File tree

src/latence/resources/trace.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,55 @@ def _normalize_session_state(
9494
def _normalize_turns(
9595
turns: list[TraceResponse | dict[str, Any]],
9696
) -> list[dict[str, Any]]:
97-
"""Coerce a list of scoring responses / dicts to the rollup turn shape."""
97+
"""Coerce a list of scoring responses / dicts to the rollup turn shape.
98+
99+
The rollup handler expects ``RollupTurnInput`` with fields
100+
``scores``, ``risk_band``, ``file_attribution``, ``session_signals``,
101+
``recommendation``, ``timestamp`` — NOT the flat ``TraceResponse``
102+
fields like ``score`` / ``band`` / ``success``.
103+
"""
98104
out: list[dict[str, Any]] = []
99105
for turn in turns:
100106
if isinstance(turn, TraceResponse):
101-
out.append(turn.model_dump(exclude_none=True))
107+
d = turn.model_dump(exclude_none=True)
108+
out.append(_trace_response_to_rollup_turn(d))
109+
elif isinstance(turn, dict):
110+
if "scores" in turn or "risk_band" in turn:
111+
out.append(turn)
112+
else:
113+
out.append(_trace_response_to_rollup_turn(turn))
102114
else:
103115
out.append(turn)
104116
return out
105117

106118

119+
def _trace_response_to_rollup_turn(d: dict[str, Any]) -> dict[str, Any]:
120+
"""Map flat TraceResponse dict to the RollupTurnInput shape."""
121+
turn: dict[str, Any] = {}
122+
123+
scores: dict[str, float | None] = {}
124+
if "score" in d and d["score"] is not None:
125+
metric = d.get("primary_metric", "groundedness_v2")
126+
scores[metric] = d["score"]
127+
if "context_coverage_ratio" in d:
128+
scores["context_coverage_ratio"] = d["context_coverage_ratio"]
129+
if "context_usage_ratio" in d:
130+
scores["context_usage_ratio"] = d["context_usage_ratio"]
131+
if "nli_aggregate" in d:
132+
scores["nli_aggregate"] = d["nli_aggregate"]
133+
if scores:
134+
turn["scores"] = scores
135+
136+
if "band" in d:
137+
turn["risk_band"] = d["band"]
138+
139+
for passthrough in ("file_attribution", "session_signals", "recommendation", "timestamp"):
140+
if passthrough in d and d[passthrough] is not None:
141+
turn[passthrough] = d[passthrough]
142+
143+
return turn
144+
145+
107146
def _require_premise_lane(
108147
*,
109148
raw_context: str | None,

tests/test_trace.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -554,8 +554,15 @@ def test_rollup_accepts_response_objects(self):
554554
assert isinstance(body["turns"], list)
555555
assert len(body["turns"]) == 2
556556
assert all(isinstance(t, dict) for t in body["turns"])
557-
# Code-lane-specific fields propagate.
558-
assert body["turns"][0].get("code_lane") is not None
557+
# Response objects are mapped to the handler's RollupTurnInput shape,
558+
# not passed through with fields the gateway rejects.
559+
assert body["turns"][0].get("scores") is not None
560+
assert body["turns"][0].get("risk_band") == turn1.band
561+
assert body["turns"][0].get("session_signals") is not None
562+
assert "code_lane" not in body["turns"][0]
563+
assert body["turns"][1].get("scores") is not None
564+
assert body["turns"][1].get("risk_band") == turn2.band
565+
assert "success" not in body["turns"][1]
559566
assert body["session_id"] == "sess_abc"
560567

561568
def test_rollup_default_heatmap_not_sent(self):

0 commit comments

Comments
 (0)