fix(vision): NaN/inf/bool di uno strumento non diventano più una quota (#16)

runner.run_graph esclude bool e NaN/inf dagli output numerici, con il
motivo in RunOutcome.invalid_outputs. vision_service tratta una quota
mappata con valore None/assente/non finito come "non prodotta" (422),
invece di arrivare a float(None) e sollevare TypeError.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:35:53 +02:00
parent 10261b66f0
commit 20957e517d
5 changed files with 137 additions and 4 deletions
+23 -2
View File
@@ -6,6 +6,7 @@ to the worker over HTTP and stays ignorant of how a number was produced.
from __future__ import annotations
import json
import math
from pathlib import Path
from uuid import uuid4
@@ -21,6 +22,21 @@ from src.backend.models.orm.vision import VisionReferenceImage, VisionResult
from src.backend.services import measurement_service
def _is_measurable(value: object) -> bool:
"""A worker output counts as a quote only if it is a finite, non-bool number.
None (absent, or explicitly null in a malformed response), a bool (a
subclass of int), and NaN/inf all fail this - each one would otherwise
reach `float()` and either crash with a bare TypeError or silently save a
value nobody could have measured.
"""
return (
isinstance(value, (int, float))
and not isinstance(value, bool)
and math.isfinite(value)
)
async def _call_worker(image_bytes: bytes, graph: dict) -> dict:
"""The one place that talks to the worker; the tests replace it.
@@ -159,8 +175,13 @@ async def execute_task(
outputs = report["outputs"]
# Every quote or none. A half-measured piece read as a whole one is worse
# than a piece not measured at all.
missing = [q.vision_output for q in quotes if q.vision_output not in outputs]
# than a piece not measured at all. A quote counts as produced only if its
# output is present *and* a finite, non-bool number: the in-process runner
# already drops NaN/inf itself (see src/vision/runner.py), but the worker
# is reached over HTTP - a different implementation of it, or a malformed
# response, could still hand back None or a non-finite float for a mapped
# key instead of omitting it outright.
missing = [q.vision_output for q in quotes if not _is_measurable(outputs.get(q.vision_output))]
if missing:
failed = ", ".join(f["tool_name"] for f in report["failures"])
raise HTTPException(