Merge branch 'langchain-ai:master' into support-bind_tools-for-fake-chat-model

This commit is contained in:
amosctlee 2025-07-29 14:30:33 +08:00 committed by GitHub
commit 3c84183e28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 96 additions and 1 deletions

View File

@ -108,6 +108,39 @@ def merge_lists(left: Optional[list], *others: Optional[list]) -> Optional[list]
else e
)
merged[to_merge[0]] = merge_dicts(merged[to_merge[0]], new_e)
# Special handling for tool call chunks: if this chunk appears to be
# a continuation of a prior chunk (has None name/id) and no matching
# index was found, try to merge with the most recent tool call chunk
# that has a name/id.
# Fixes issues with models that send inconsistent indices.
# See #31511 for more.
elif (
e.get("type") == "tool_call_chunk"
and e.get("name") is None
and e.get("id") is None
and merged
):
# Find the most recent tool call chunk with a valid name or id
for i in reversed(range(len(merged))):
if (
isinstance(merged[i], dict)
and merged[i].get("type") == "tool_call_chunk"
and (
merged[i].get("name") is not None
or merged[i].get("id") is not None
)
):
# Merge with this chunk
new_e = (
{k: v for k, v in e.items() if k != "type"}
if "type" in e
else e
)
merged[i] = merge_dicts(merged[i], new_e)
break
else:
# No suitable chunk found, append as new
merged.append(e)
else:
merged.append(e)
else:

View File

@ -313,7 +313,7 @@ def test_cache_with_generation_objects() -> None:
but ChatResult expects ChatGeneration objects, causing validation errors.
See #22389 for more info.
"""
cache = InMemoryCache()

View File

@ -1197,3 +1197,65 @@ def test_convert_to_openai_image_block() -> None:
}
result = convert_to_openai_image_block(input_block)
assert result == expected
def test_tool_call_streaming_different_indices() -> None:
"""Test that tool call chunks with different indices but logically part of the same
tool call are merged correctly. This addresses issues with models like Qwen3 that
send inconsistent indices during streaming.
See #31511.
""" # noqa: D205
# Create chunks that simulate Qwen3 behavior:
# First chunk has index=1, subsequent chunks have index=0 with name=None, id=None
chunk1 = AIMessageChunk(
content="",
tool_call_chunks=[
create_tool_call_chunk(
name="search_function",
args='{"query": "langchain',
id="call_123",
index=1, # Initial index
)
],
)
chunk2 = AIMessageChunk(
content="",
tool_call_chunks=[
create_tool_call_chunk(
name=None, # Continuation chunk
args=' tutorial"}',
id=None, # Continuation chunk
index=0, # Different index
)
],
)
# Merge chunks as happens during streaming
merged_chunk: AIMessageChunk = chunk1 + chunk2 # type: ignore[assignment]
# Should result in a single merged tool call chunk
assert len(merged_chunk.tool_call_chunks) == 1
assert merged_chunk.tool_call_chunks[0]["name"] == "search_function"
assert merged_chunk.tool_call_chunks[0]["args"] == '{"query": "langchain tutorial"}'
assert merged_chunk.tool_call_chunks[0]["id"] == "call_123"
# Should result in a single valid tool call
assert len(merged_chunk.tool_calls) == 1
assert len(merged_chunk.invalid_tool_calls) == 0
# Verify the final tool call is correct
tool_call = merged_chunk.tool_calls[0]
assert tool_call["name"] == "search_function"
assert tool_call["args"] == {"query": "langchain tutorial"}
assert tool_call["id"] == "call_123"
# Test with message_chunk_to_message
message: AIMessage = message_chunk_to_message(merged_chunk) # type: ignore[assignment]
assert len(message.tool_calls) == 1
assert len(message.invalid_tool_calls) == 0
assert message.tool_calls[0]["name"] == "search_function"
assert message.tool_calls[0]["args"] == {"query": "langchain tutorial"}