mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-28 17:26:48 +00:00
fix(llm): parse claude-cli structured_output, not the prose result field (follow-up to #2095)
This commit is contained in:
+11
-1
@@ -1509,7 +1509,17 @@ def _call_claude_cli(user_message: str, max_tokens: int = 8192, *, deep_mode: bo
|
||||
|
||||
envelope = _claude_cli_envelope(proc.stdout)
|
||||
|
||||
raw_content = envelope.get("result", "")
|
||||
# When --json-schema is in effect the CLI puts the CONSTRAINED object in the
|
||||
# `structured_output` envelope field; `result` stays the model's discretionary
|
||||
# text, which on a "reporting" turn is prose even with the flag set (verified
|
||||
# live on Claude Code 2.1.185). Prefer the structured channel and route it
|
||||
# through the same _parse_llm_json normalizer; fall back to parsing `result`
|
||||
# for older CLIs that don't emit structured_output (#2076 review).
|
||||
structured = envelope.get("structured_output")
|
||||
if isinstance(structured, dict):
|
||||
raw_content = json.dumps(structured)
|
||||
else:
|
||||
raw_content = envelope.get("result", "")
|
||||
result = _parse_llm_json(raw_content or "{}")
|
||||
usage = envelope.get("usage") or {}
|
||||
result["input_tokens"] = (
|
||||
|
||||
@@ -349,3 +349,35 @@ def test_simple_completion_resolves_cmd_shim_on_windows(monkeypatch):
|
||||
|
||||
assert out == "ok"
|
||||
assert captured["argv0"] == r"C:\npm\claude.cmd"
|
||||
|
||||
|
||||
def test_prefers_structured_output_over_prose_result(monkeypatch):
|
||||
"""#2076 review: with --json-schema the CLI puts the constrained object in
|
||||
`structured_output` while `result` may be prose (a 'reporting' turn). The
|
||||
backend must parse the structured object; parsing the prose would read as an
|
||||
empty/hollow extraction and bisect forever."""
|
||||
envelope = {
|
||||
"type": "result", "subtype": "success", "is_error": False,
|
||||
"result": "Knowledge graph extracted successfully: 2 nodes, 1 edge.", # prose only
|
||||
"structured_output": {
|
||||
"nodes": [
|
||||
{"id": "foo_module", "label": "Foo", "file_type": "document", "source_file": "foo.md"},
|
||||
{"id": "foo_greet", "label": "greet", "file_type": "code", "source_file": "foo.md"},
|
||||
],
|
||||
"edges": [
|
||||
{"source": "foo_module", "target": "foo_greet",
|
||||
"relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0},
|
||||
],
|
||||
"hyperedges": [],
|
||||
},
|
||||
"stop_reason": "end_turn",
|
||||
"usage": {"input_tokens": 6, "output_tokens": 11},
|
||||
"modelUsage": {"claude-opus-4-7[1m]": {}},
|
||||
}
|
||||
completed = MagicMock(returncode=0, stdout=json.dumps(envelope), stderr="")
|
||||
with patch("shutil.which", return_value="/fake/bin/claude"), \
|
||||
patch("subprocess.run", return_value=completed):
|
||||
result = llm._call_claude_cli("dummy", max_tokens=8192)
|
||||
assert len(result["nodes"]) == 2, "must parse structured_output, not the prose result"
|
||||
assert len(result["edges"]) == 1
|
||||
assert result["finish_reason"] == "stop"
|
||||
|
||||
Reference in New Issue
Block a user