fix(claude): read text blocks after leading ThinkingBlock - #2734
Conversation
Anthropic models with extended thinking return a ThinkingBlock before the TextBlock. Indexing content[0] crashed semantic extraction and community labeling. Select the first text block instead. Fixes Graphify-Labs#2697.
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
This PR adds a helper _anthropic_response_text in graphify/llm.py that scans Anthropic response content blocks and returns the first block carrying non-empty text, rather than assuming text lives at content[0]. It updates the two Anthropic call sites (_call_claude and the Claude branch of _call_llm) to use this helper, motivated by extended-thinking models emitting a leading thinking block. The PR also adds unit tests in tests/test_image_vision.py covering the new helper (single text block, leading thinking block, legacy blocks without a type, and default fallback) and an end-to-end _call_claude test simulating a thinking-model response. Note: many of the "changed symbols" listed (e.g. various rationale_*, image/bedrock helpers, temperature/tokenizer functions) don't appear in the diff, so the actual code surface touched is limited to the additions and two call-site edits shown above.
No blocking issues surfaced.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 692 functions depend on the 208 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
_call_llm()— 11 callers, 18 callees - worse:
_build_image_refs()— 15 callers, 3 callees - worse:
_call_claude()— 3 callers, 8 callees
Verification — 692 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 443 function(s) in the blast radius were not formally verified this run
Formal verification
Verification did not run: Verification did not run for \_call\_claude.
The verification could not execute (an environment/toolchain issue, not a statement about the code).
Guarantee: No guarantee, the check itself did not complete.
Note: Detail: harness produced no verdict (rc=124): timeout after 30s
Could not verify: Could not verify \_call\_llm.
The verifier did not have enough to check \_call\_llm, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
· 3 more finding(s) on lines outside this diff (see the check run).
Summary
graphify extract --backend claudeand community labeling both readresp.content[0].text. On current Claude models, the first block is often aThinkingBlock, not the answer — so extraction fails withThinkingBlock object has no attribute text.What changed
Added
_anthropic_response_text()to pick the firsttextblock instead of assuming index 0. Wired it into_call_claudeand the claude branch of_call_llm. Added regression tests mirroring the existing Bedrock reasoning-block coverage.Why
Fixes #2697. I kept hitting chunk failures on semantic extraction where every error was the ThinkingBlock attribute error, not the actual API problem.
Test plan
pytest tests/test_image_vision.py -k anthropic_response_textpytest tests/test_image_vision.py::test_call_claude_parses_thinking_model_responsepytest tests/test_llm_backends.py::test_call_llm_claude_client_built_with_timeout_and_retries