-
Notifications
You must be signed in to change notification settings - Fork 6.5k
add unified tool call block #19947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add unified tool call block #19947
Changes from 10 commits
a7f0e85
db697dd
c2bfa65
83d5983
24a1dc7
faa9994
218ffc8
a780c41
4798bcb
58b6b9a
4b594cb
898e725
bd95703
4c6bc93
8c598f4
a0f2bef
1a25ca8
1c2f12e
7c0e4b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
import json | ||
from typing import ( | ||
TYPE_CHECKING, | ||
Any, | ||
|
@@ -12,6 +11,7 @@ | |
Set, | ||
Tuple, | ||
Union, | ||
cast, | ||
) | ||
|
||
from llama_index.core.base.llms.types import ( | ||
|
@@ -22,6 +22,7 @@ | |
LLMMetadata, | ||
MessageRole, | ||
ContentBlock, | ||
ToolCallBlock, | ||
) | ||
from llama_index.core.base.llms.types import TextBlock as LITextBlock | ||
from llama_index.core.base.llms.types import CitationBlock as LICitationBlock | ||
|
@@ -42,6 +43,7 @@ | |
force_single_tool_call, | ||
is_function_calling_model, | ||
messages_to_anthropic_messages, | ||
_anthropic_tool_call_to_tool_call_block, | ||
) | ||
|
||
import anthropic | ||
|
@@ -344,8 +346,7 @@ def _completion_response_from_chat_response( | |
|
||
def _get_blocks_and_tool_calls_and_thinking( | ||
self, response: Any | ||
) -> Tuple[List[ContentBlock], List[Dict[str, Any]], List[Dict[str, Any]]]: | ||
tool_calls = [] | ||
) -> Tuple[List[ContentBlock], List[Dict[str, Any]]]: | ||
blocks: List[ContentBlock] = [] | ||
citations: List[TextCitation] = [] | ||
tracked_citations: Set[str] = set() | ||
|
@@ -385,9 +386,15 @@ def _get_blocks_and_tool_calls_and_thinking( | |
) | ||
) | ||
elif isinstance(content_block, ToolUseBlock): | ||
tool_calls.append(content_block.model_dump()) | ||
blocks.append( | ||
ToolCallBlock( | ||
tool_call_id=content_block.id, | ||
tool_name=content_block.name, | ||
tool_kwargs=content_block.input, | ||
) | ||
) | ||
|
||
return blocks, tool_calls, [x.model_dump() for x in citations] | ||
return blocks, [x.model_dump() for x in citations] | ||
|
||
@llm_chat_callback() | ||
def chat( | ||
|
@@ -405,17 +412,12 @@ def chat( | |
**all_kwargs, | ||
) | ||
|
||
blocks, tool_calls, citations = self._get_blocks_and_tool_calls_and_thinking( | ||
response | ||
) | ||
blocks, citations = self._get_blocks_and_tool_calls_and_thinking(response) | ||
|
||
return AnthropicChatResponse( | ||
message=ChatMessage( | ||
role=MessageRole.ASSISTANT, | ||
blocks=blocks, | ||
additional_kwargs={ | ||
"tool_calls": tool_calls, | ||
}, | ||
), | ||
citations=citations, | ||
raw=dict(response), | ||
|
@@ -526,7 +528,12 @@ def gen() -> Generator[AnthropicChatResponse, None, None]: | |
yield AnthropicChatResponse( | ||
message=ChatMessage( | ||
role=role, | ||
blocks=content, | ||
blocks=[ | ||
*content, | ||
*_anthropic_tool_call_to_tool_call_block( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this assumes that tool calls always come after content (I think this is true? just flagging) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that was my assumption: first there is the "explanation" of the tool call and then we have the tool call itself There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to be safe, it might be best to just match the order that the content came in, rather than assuming 👍🏻 |
||
cur_tool_calls | ||
), | ||
], | ||
additional_kwargs={ | ||
"tool_calls": [ | ||
t.model_dump() for t in tool_calls_to_send | ||
|
@@ -577,17 +584,12 @@ async def achat( | |
**all_kwargs, | ||
) | ||
|
||
blocks, tool_calls, citations = self._get_blocks_and_tool_calls_and_thinking( | ||
response | ||
) | ||
blocks, citations = self._get_blocks_and_tool_calls_and_thinking(response) | ||
|
||
return AnthropicChatResponse( | ||
message=ChatMessage( | ||
role=MessageRole.ASSISTANT, | ||
blocks=blocks, | ||
additional_kwargs={ | ||
"tool_calls": tool_calls, | ||
}, | ||
), | ||
citations=citations, | ||
raw=dict(response), | ||
|
@@ -698,11 +700,12 @@ async def gen() -> ChatResponseAsyncGen: | |
yield AnthropicChatResponse( | ||
message=ChatMessage( | ||
role=role, | ||
blocks=content, | ||
additional_kwargs={ | ||
"tool_calls": [t.dict() for t in tool_calls_to_send], | ||
"thinking": thinking.model_dump() if thinking else None, | ||
}, | ||
blocks=[ | ||
*content, | ||
*_anthropic_tool_call_to_tool_call_block( | ||
cur_tool_calls | ||
), | ||
], | ||
), | ||
citations=cur_citations, | ||
delta=content_delta, | ||
|
@@ -811,7 +814,11 @@ def get_tool_calls_from_response( | |
**kwargs: Any, | ||
) -> List[ToolSelection]: | ||
"""Predict and call the tool.""" | ||
tool_calls = response.message.additional_kwargs.get("tool_calls", []) | ||
tool_calls = [ | ||
block | ||
for block in response.message.blocks | ||
if isinstance(block, ToolCallBlock) | ||
] | ||
|
||
if len(tool_calls) < 1: | ||
if error_on_no_tool_call: | ||
|
@@ -823,25 +830,13 @@ def get_tool_calls_from_response( | |
|
||
tool_selections = [] | ||
for tool_call in tool_calls: | ||
if ( | ||
"input" not in tool_call | ||
or "id" not in tool_call | ||
or "name" not in tool_call | ||
): | ||
raise ValueError("Invalid tool call.") | ||
if tool_call["type"] != "tool_use": | ||
raise ValueError("Invalid tool type. Unsupported by Anthropic") | ||
argument_dict = ( | ||
json.loads(tool_call["input"]) | ||
if isinstance(tool_call["input"], str) | ||
else tool_call["input"] | ||
) | ||
argument_dict = tool_call.tool_kwargs | ||
|
||
tool_selections.append( | ||
ToolSelection( | ||
tool_id=tool_call["id"], | ||
tool_name=tool_call["name"], | ||
tool_kwargs=argument_dict, | ||
tool_id=tool_call.tool_call_id or "", | ||
tool_name=tool_call.tool_name, | ||
tool_kwargs=cast(Dict[str, Any], argument_dict), | ||
) | ||
) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
CitableBlock, | ||
CitationBlock, | ||
ThinkingBlock, | ||
ToolCallBlock, | ||
ContentBlock, | ||
) | ||
|
||
|
@@ -26,6 +27,7 @@ | |
ImageBlockParam, | ||
CacheControlEphemeralParam, | ||
Base64PDFSourceParam, | ||
ToolUseBlock, | ||
) | ||
from anthropic.types import ContentBlockParam as AnthropicContentBlock | ||
from anthropic.types.beta import ( | ||
|
@@ -193,6 +195,19 @@ def _to_anthropic_document_block(block: DocumentBlock) -> DocumentBlockParam: | |
) | ||
|
||
|
||
def _anthropic_tool_call_to_tool_call_block(tool_calls: list[ToolUseBlock]): | ||
blocks = [] | ||
for tool_call in tool_calls: | ||
blocks.append( | ||
ToolCallBlock( | ||
tool_call_id=tool_call.id, | ||
tool_kwargs=tool_call.input, | ||
tool_name=tool_call.name, | ||
) | ||
) | ||
return blocks | ||
|
||
|
||
def blocks_to_anthropic_blocks( | ||
blocks: Sequence[ContentBlock], kwargs: dict[str, Any] | ||
) -> List[AnthropicContentBlock]: | ||
|
@@ -270,24 +285,18 @@ def blocks_to_anthropic_blocks( | |
elif isinstance(block, CitationBlock): | ||
# No need to pass these back to Anthropic | ||
continue | ||
elif isinstance(block, ToolCallBlock): | ||
anthropic_blocks.append( | ||
ToolUseBlockParam( | ||
id=block.tool_call_id or "", | ||
input=block.tool_kwargs, | ||
name=block.tool_name, | ||
type="tool_use", | ||
) | ||
) | ||
else: | ||
raise ValueError(f"Unsupported block type: {type(block)}") | ||
|
||
tool_calls = kwargs.get("tool_calls", []) | ||
for tool_call in tool_calls: | ||
assert "id" in tool_call | ||
assert "input" in tool_call | ||
assert "name" in tool_call | ||
|
||
anthropic_blocks.append( | ||
ToolUseBlockParam( | ||
id=tool_call["id"], | ||
input=tool_call["input"], | ||
name=tool_call["name"], | ||
type="tool_use", | ||
) | ||
) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to add back this handling (in each LLM), just to ensure that old chat histories don't suddenly stop working. No harm in keeping this here |
||
return anthropic_blocks | ||
|
||
|
||
|
@@ -346,6 +355,12 @@ def messages_to_anthropic_messages( | |
|
||
|
||
def force_single_tool_call(response: ChatResponse) -> None: | ||
tool_calls = response.message.additional_kwargs.get("tool_calls", []) | ||
tool_calls = [ | ||
block for block in response.message.blocks if isinstance(block, ToolCallBlock) | ||
] | ||
if len(tool_calls) > 1: | ||
response.message.additional_kwargs["tool_calls"] = [tool_calls[0]] | ||
response.message.blocks = [ | ||
block | ||
for block in response.message.blocks | ||
if not isinstance(block, ToolCallBlock) | ||
] + [tool_calls[0]] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ dev = [ | |
|
||
[project] | ||
name = "llama-index-llms-anthropic" | ||
version = "0.9.1" | ||
version = "0.10.0" | ||
logan-markewich marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
description = "llama-index llms anthropic integration" | ||
authors = [{name = "Your Name", email = "[email protected]"}] | ||
requires-python = ">=3.9,<4.0" | ||
|
Uh oh!
There was an error while loading. Please reload this page.