Isolate malformed LLM batch responses#265
Conversation
Signed-off-by: kigland <shuaizhicheng336@gmail.com>
rng1995
left a comment
There was a problem hiding this comment.
[Automated SkillSpector Review]
Summary: Wraps the sync run_batches loop in per-batch exception handling so a malformed structured LLM response (pydantic ValidationError) skips only its own batch instead of propagating, while plain ValueError/NotImplementedError (misconfiguration) still propagate. Adds two sync regression tests. Claims to fix #250.
Blockers
-
The fix does not cover the paths that actually crash the scan — #250 remains reproducible. Pydantic v2
ValidationErrorsubclassesValueError. The untouched asyncarun_batchesre-raises anything matchingisinstance(result, (ValueError, NotImplementedError))(src/skillspector/llm_analyzer_base.py:441), so a malformed structured response still escapes it as aValidationError. Three of the four LLM stages callarun_batches(semantic_quality_policy,semantic_developer_intent,meta_analyzer), and each node re-raises viaexcept ValueError: raise, killing the whole scan with no report — the exact symptom in #250. I verified this empirically against this PR's head: feedingarun_batchesthe sameWe{"findings":[]}payload used in the new sync test aborts the entire fan-out. Meanwhile the onlyrun_batchescaller (semantic_security_discovery) already caughtValidationErrorat node level on main and degraded gracefully. The sameexcept ValidationErrorcarve-out (placed before theValueErrorre-raise check) is needed inarun_batches, with an async regression test. -
Silent-degradation regression: dropped batches no longer surface in
llm_call_logor the report. On main, a malformed response in the sync path reachedsemantic_security_discovery'sexcept ValidationErrorhandler, which recordedllm_call_record(ok=False, error="malformed LLM response: ...")— feeding the report's_llm_degradation_notice. After this PR that handler is dead code: batches are dropped insiderun_batcheswith only a log warning, and the node reportsok=Trueeven if every batch failed to parse. For a security scanner, unannotated loss of analysis coverage is a detection gap (and #250 explicitly asked for clearly annotated partial results).run_batchescallers should be able to detect dropped batches (compare submitted vs returned, asmeta_analyzerdoes for the async path) and the node should record a degraded/partialllm_call_record.
Non-blocking
- The raw-string branch (
_message_text(self._llm.invoke(prompt))) has noValidationErrorcarve-out, so structured mode skips aValidationErrorwhile raw mode propagates it. If intentional, a comment would help; otherwise align the two branches. - The three near-identical try/except blocks in
run_batchescould be factored into a small helper so the exception policy (skip vs propagate) lives in one place — ideally shared witharun_batchesso the two paths cannot drift again (which is exactly how blocker 1 happened). run_batches's docstring was not updated;arun_batchesdocuments its failure-isolation semantics in detail — mirror that.- Tests cover only the structured-invoke leg; the
parse_responseleg and raw-string leg of the new handling are untested.
Tests: the two new sync tests are well-constructed (the ValidationError is raised from inside invoke, matching how langchain structured output fails) and pass locally along with the rest of the file (113 passed).
| response = self._structured_llm.invoke(prompt) | ||
| try: | ||
| response = self._structured_llm.invoke(prompt) | ||
| except ValidationError as exc: |
There was a problem hiding this comment.
This carve-out is correct here, but the same fix is missing in arun_batches: pydantic ValidationError subclasses ValueError, so isinstance(result, (ValueError, NotImplementedError)) at line 441 re-raises it, and all three async callers (semantic_quality_policy, semantic_developer_intent, meta_analyzer) re-raise ValueError at node level — the whole-scan crash from #250 is still reproducible through those stages (verified against this branch with the same We{"findings":[]} payload). Please add an isinstance(result, ValidationError) check before the ValueError re-raise in arun_batches, plus an async regression test.
| try: | ||
| response = self._structured_llm.invoke(prompt) | ||
| except ValidationError as exc: | ||
| logger.warning("LLM batch failed for %s: %s", batch.file_label, exc) |
There was a problem hiding this comment.
Dropping the batch with only a log warning gives callers no signal. Previously a ValidationError reached semantic_security_discovery's node-level handler, which recorded llm_call_record(ok=False, error="malformed LLM response: ...") and fed the report's degradation notice; that handler is now dead code, so a scan where every batch fails to parse still reports the LLM stage as fully OK. Consider having the node compare submitted vs returned batches (as meta_analyzer does) and record a degraded call, so partial results stay annotated in the report.
| else: | ||
| response = _message_text(self._llm.invoke(prompt)) | ||
| try: | ||
| response = _message_text(self._llm.invoke(prompt)) |
There was a problem hiding this comment.
Inconsistency: this raw-mode branch has no ValidationError carve-out, so a ValidationError raised here (or in a subclass's raw-mode pipeline) propagates via the ValueError re-raise, while the structured branch above skips it. If intentional, add a comment; otherwise align the two branches.
| ) | ||
| if self._structured_llm: | ||
| response = self._structured_llm.invoke(prompt) | ||
| try: |
There was a problem hiding this comment.
Maintainability: this is the first of three near-identical try/except blocks in the loop. Extracting the policy into a small helper (skip on ValidationError/other Exceptions, propagate ValueError/NotImplementedError) would keep it in one place and let arun_batches share it, preventing the sync/async drift that left the async path unfixed. Also, the run_batches docstring should document the new failure-isolation semantics the way arun_batches's docstring does.
| MODEL = "nvidia/openai/gpt-oss-120b" | ||
|
|
||
| @patch(MOCK_PATCH_TARGET, _mock_get_chat_model) | ||
| def test_malformed_structured_batch_does_not_abort_the_others(self) -> None: |
There was a problem hiding this comment.
Good regression test for the sync path. Please add the async counterpart: an arun_batches test where ainvoke raises this same ValidationError and the other batches survive — it currently fails on this branch because arun_batches re-raises ValidationError via the (ValueError, NotImplementedError) isinstance check. Coverage for the parse_response leg and raw-string mode of the new sync handling would also be worthwhile.
Summary
run_batchesregression coverage for a stray-text structured responseFixes #250.
Verification
uv run pytest tests/nodes/test_llm_analyzer_base.py -k "RunBatches or failed_batch or value_error"uv run pytest tests/nodes/test_llm_analyzer_base.pygit diff --check upstream/main...HEADuv run ruff check src/ tests/uv run ruff format --check src/ tests/Note:
make lintandmake formatfail in this shell because the Makefile calls bareruff, but the equivalentuv run ruff ...commands above pass.