fix: defensive coercion for LLM confidence and cluster fields in hypothesizer
- Add _coerce_float() module-level helper: catches TypeError/ValueError from
non-numeric LLM output (e.g. 'high', 'N/A') and returns a caller-supplied
default instead of raising.
- Replace float(item.get('confidence', 0.5)) with
_coerce_float(item.get('confidence'), 0.5) in _parse_response.
- Guard supporting_cluster_ids: tuple(item.get(...) or []) so a JSON null
from the LLM does not cause TypeError('NoneType is not iterable').
- runbook_refs is hardcoded as () and not sourced from LLM output; no change
needed there.
- Add test_non_numeric_confidence_uses_default (Test 10) to cover the 'high'
string case: asserts no exception and confidence == 0.5.
- 341 tests passing (+1).
Closes: #29
This commit is contained in:
parent
6a4fc40a7d
commit
5a4ee3f912
2 changed files with 45 additions and 2 deletions
|
|
@ -30,6 +30,14 @@ _SYSTEM_PROMPT = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_float(val: object, default: float) -> float:
|
||||||
|
"""Safely coerce LLM output to float, returning default on failure."""
|
||||||
|
try:
|
||||||
|
return float(val) # type: ignore[arg-type]
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _validate_severity(s: str) -> SeverityLabel:
|
def _validate_severity(s: str) -> SeverityLabel:
|
||||||
"""Map a raw severity string to a valid SeverityLabel, defaulting to ERROR."""
|
"""Map a raw severity string to a valid SeverityLabel, defaulting to ERROR."""
|
||||||
upper = s.upper()
|
upper = s.upper()
|
||||||
|
|
@ -198,8 +206,8 @@ class RootCauseHypothesizer:
|
||||||
hypothesis_id=str(uuid4()),
|
hypothesis_id=str(uuid4()),
|
||||||
title=str(item.get("title", "Unknown"))[:80],
|
title=str(item.get("title", "Unknown"))[:80],
|
||||||
description=str(item.get("description", "")),
|
description=str(item.get("description", "")),
|
||||||
confidence=float(item.get("confidence", 0.5)),
|
confidence=_coerce_float(item.get("confidence"), 0.5),
|
||||||
supporting_cluster_ids=tuple(item.get("supporting_clusters", [])),
|
supporting_cluster_ids=tuple(item.get("supporting_clusters") or []),
|
||||||
runbook_refs=(),
|
runbook_refs=(),
|
||||||
severity=severity,
|
severity=severity,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -449,3 +449,38 @@ def test_confidence_string_float_coercion():
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
assert isinstance(results[0].confidence, float)
|
assert isinstance(results[0].confidence, float)
|
||||||
assert results[0].confidence == pytest.approx(0.8)
|
assert results[0].confidence == pytest.approx(0.8)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 10: Non-numeric confidence string falls back to default 0.5
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_numeric_confidence_uses_default():
|
||||||
|
"""LLM returning 'high' for confidence should not raise and defaults to 0.5."""
|
||||||
|
cluster = _make_cluster()
|
||||||
|
classified = _make_classified(clusters=(cluster,))
|
||||||
|
ctx = _make_ctx()
|
||||||
|
hypothesizer = RootCauseHypothesizer()
|
||||||
|
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
"title": "t",
|
||||||
|
"description": "d",
|
||||||
|
"confidence": "high",
|
||||||
|
"severity": "ERROR",
|
||||||
|
"supporting_clusters": [],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
mock_resp = _llm_json_response(items)
|
||||||
|
|
||||||
|
with patch("httpx.post", return_value=mock_resp):
|
||||||
|
results = hypothesizer.hypothesize(
|
||||||
|
classified, ctx, query="test",
|
||||||
|
llm_url="http://localhost:11434",
|
||||||
|
llm_model="llama3",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(results) == 1
|
||||||
|
assert isinstance(results[0].confidence, float)
|
||||||
|
assert results[0].confidence == pytest.approx(0.5)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue