Repository navigation
fix(types): parse null and {} as empty dspy.ToolCalls - #10613
isaacbmiller wants to merge 1 commit into
Conversation
Models in JSON mode (e.g. DeepSeek-V4-Flash with JSONAdapter) often write
"tool_calls": null or {} to mean "no tool calls". ToolCalls accepted [] but
rejected both, so ReActV2 ended its loop with parse_error and the forced
submit raised AdapterParseError.
Treat None and {} like [] in ToolCalls.validate_input. Optional fields
(ToolCalls | None) still validate None as None.
Amp-Thread-ID: https://ampcode.com/threads/T-01a126ab-8118-77c7-a811-67f49e5f20cd
Co-authored-by: Isaac Miller <isaac@cmpnd.ai>
|
|
AI-assisted offline review of The existing ReActV2 and Tool suites pass: 91 tests on head, 89 on base. An additional 61 checks importing the actual package all pass on head; base has 47 passes and 14 null/empty-object failures. Ruff passes on the three changed files. Actual JSONAdapter/DummyLM sync and async flows accept an empty turn followed by submit, while repeated empty replies still require submit. Controls cover 12 malformed ToolCalls shapes, Optional None and native detection/hooks/schema handling. Both revisions already normalize non-dict args such as reviewed and tested locally. |
Fixes #10612.
Summary
dspy.ToolCallsnow parsesnulland{}as an emptyToolCalls, the same as[]. JSON-mode models (seen withDeepSeek-V4-Flash+JSONAdapteron τ-bench retail) use both to mean "no tool calls". ReActV2 then rejected the turn asparse_error, and a forced submit with the same reply raisedAdapterParseErrorfromforward.The fix goes in
ToolCallsparsing rather than ReActV2's signature. Declaringtool_calls: dspy.ToolCalls | Nonewould also acceptnull. But adapters detect the native tool-call field withfield.annotation == ToolCalls, so an Optional field silently turns off native function calling for every LM that supports it. Parsing empty values in the type fixes everyToolCallsoutput field on the text path and leaves the native path unchanged.ToolCalls | Nonefields still getNoneforNone.With this change,
null/{}behave exactly like[]: the loop ends withempty_tool_calls, and the forced submit runs. If the model still never callssubmit, ReActV2 raises "no submit call" as before. Text mode can't force a tool choice, which is a separate question.Verification
test_react_v2_json_null_or_empty_tool_calls_mean_no_tool_calls(parametrized overNoneand{}) drives ReActV2 throughJSONAdapterwith aDummyLM. Both cases fail on main withAdapterParseErrorand pass with the fix.test_toolcalls_vague_matchto coverNone,{}, and[]; it fails on main.pytest tests/predict/test_react_v2.py tests/adapters/test_tool.py: 91 passed.pytest tests/adapters tests/predict tests/signatures: 1014 passed, 148 skipped.ruff checkpasses on the changed files, and CI'sruff check --fix-only --exit-non-zero-on-fixis clean. pre-commit hooks pass.DeepSeek-V4-Flash: a forced-submit reply of"tool_calls": nullnow ends as "after empty_tool_calls: no submit call" instead ofAdapterParseError.AI assistance
Investigated, reproduced, and implemented with Amp (Claude) in a maintainer-directed session. Prompt: "Open one issue and one fix PR for: ReActV2 fails when the model returns
tool_calls: null(or{}) with DeepSeek-V4-Flash + JSONAdapter. Reproduce it, confirm the root cause in code, include a DummyLM repro, decide whether the fix belongs in ReActV2's signature or in howToolCallsparses empty values, and explain the choice."