mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-25 08:45:09 +00:00
fix: detect anthropic preview tool-use blocks
This commit is contained in:
@@ -124,6 +124,33 @@ def _contains_file_id_reference(messages: list[dict[str, Any]]) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _is_tool_use_block(block: Any) -> bool:
|
||||
"""Return true for Anthropic tool-use blocks across SDK shapes.
|
||||
|
||||
New/preview Anthropic models can return content blocks whose concrete SDK
|
||||
class is not one of the imported ``ToolUseBlock`` aliases. The stable part
|
||||
of the API shape is the block type plus ``name`` and ``input`` fields, so use
|
||||
duck typing instead of relying only on SDK classes.
|
||||
"""
|
||||
if isinstance(block, (ToolUseBlock, BetaToolUseBlock)):
|
||||
return True
|
||||
if isinstance(block, dict):
|
||||
return block.get("type") == "tool_use" and "name" in block and "input" in block
|
||||
return (
|
||||
getattr(block, "type", None) == "tool_use"
|
||||
and hasattr(block, "name")
|
||||
and hasattr(block, "input")
|
||||
)
|
||||
|
||||
|
||||
def _tool_use_blocks(blocks: list[Any]) -> list[Any]:
|
||||
return [block for block in blocks if _is_tool_use_block(block)]
|
||||
|
||||
|
||||
def _tool_use_attr(block: Any, attr: str) -> Any:
|
||||
return block.get(attr) if isinstance(block, dict) else getattr(block, attr)
|
||||
|
||||
|
||||
class AnthropicThinkingConfig(BaseModel):
|
||||
type: Literal["enabled", "disabled"]
|
||||
budget_tokens: int | None = None
|
||||
@@ -589,20 +616,41 @@ class AnthropicCompletion(BaseLLM):
|
||||
Returns:
|
||||
Dictionary with thinking block data including signature, or None if not a thinking block
|
||||
"""
|
||||
if content_block.type == "thinking":
|
||||
block_type = (
|
||||
content_block.get("type")
|
||||
if isinstance(content_block, dict)
|
||||
else getattr(content_block, "type", None)
|
||||
)
|
||||
if block_type == "thinking":
|
||||
thinking = (
|
||||
content_block.get("thinking")
|
||||
if isinstance(content_block, dict)
|
||||
else content_block.thinking
|
||||
)
|
||||
thinking_block = {
|
||||
"type": "thinking",
|
||||
"thinking": content_block.thinking,
|
||||
"thinking": thinking,
|
||||
}
|
||||
if hasattr(content_block, "signature"):
|
||||
thinking_block["signature"] = content_block.signature
|
||||
signature = (
|
||||
content_block.get("signature")
|
||||
if isinstance(content_block, dict)
|
||||
else getattr(content_block, "signature", None)
|
||||
)
|
||||
if signature:
|
||||
thinking_block["signature"] = signature
|
||||
return thinking_block
|
||||
if content_block.type == "redacted_thinking":
|
||||
if block_type == "redacted_thinking":
|
||||
redacted_block = {"type": "redacted_thinking"}
|
||||
if hasattr(content_block, "thinking"):
|
||||
redacted_block["thinking"] = content_block.thinking
|
||||
if hasattr(content_block, "signature"):
|
||||
redacted_block["signature"] = content_block.signature
|
||||
if isinstance(content_block, dict):
|
||||
if "thinking" in content_block:
|
||||
redacted_block["thinking"] = content_block["thinking"]
|
||||
if "signature" in content_block:
|
||||
redacted_block["signature"] = content_block["signature"]
|
||||
else:
|
||||
if hasattr(content_block, "thinking"):
|
||||
redacted_block["thinking"] = content_block.thinking
|
||||
if hasattr(content_block, "signature"):
|
||||
redacted_block["signature"] = content_block.signature
|
||||
return redacted_block
|
||||
return None
|
||||
|
||||
@@ -944,10 +992,12 @@ class AnthropicCompletion(BaseLLM):
|
||||
else:
|
||||
for block in response.content:
|
||||
if (
|
||||
isinstance(block, (ToolUseBlock, BetaToolUseBlock))
|
||||
and block.name == "structured_output"
|
||||
_is_tool_use_block(block)
|
||||
and _tool_use_attr(block, "name") == "structured_output"
|
||||
):
|
||||
structured_data = response_model.model_validate(block.input)
|
||||
structured_data = response_model.model_validate(
|
||||
_tool_use_attr(block, "input")
|
||||
)
|
||||
self._emit_call_completed_event(
|
||||
response=structured_data.model_dump_json(),
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
@@ -962,11 +1012,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
# Check if Claude wants to use tools
|
||||
if response.content:
|
||||
tool_uses = [
|
||||
block
|
||||
for block in response.content
|
||||
if isinstance(block, (ToolUseBlock, BetaToolUseBlock))
|
||||
]
|
||||
tool_uses = _tool_use_blocks(list(response.content))
|
||||
|
||||
if tool_uses:
|
||||
# Without available_functions, return tool calls so the executor can
|
||||
@@ -1093,11 +1139,13 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
if event.type == "content_block_start":
|
||||
block = event.content_block
|
||||
if block.type == "tool_use":
|
||||
if _is_tool_use_block(block):
|
||||
block_index = event.index
|
||||
tool_use_id = _tool_use_attr(block, "id")
|
||||
tool_name = _tool_use_attr(block, "name")
|
||||
current_tool_calls[block_index] = {
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"id": tool_use_id,
|
||||
"name": tool_name,
|
||||
"arguments": "",
|
||||
"index": block_index,
|
||||
}
|
||||
@@ -1106,9 +1154,9 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": block.id,
|
||||
"id": tool_use_id,
|
||||
"function": {
|
||||
"name": block.name,
|
||||
"name": tool_name,
|
||||
"arguments": "",
|
||||
},
|
||||
"type": "function",
|
||||
@@ -1177,10 +1225,12 @@ class AnthropicCompletion(BaseLLM):
|
||||
return structured_data
|
||||
for block in final_message.content:
|
||||
if (
|
||||
isinstance(block, ToolUseBlock)
|
||||
and block.name == "structured_output"
|
||||
_is_tool_use_block(block)
|
||||
and _tool_use_attr(block, "name") == "structured_output"
|
||||
):
|
||||
structured_data = response_model.model_validate(block.input)
|
||||
structured_data = response_model.model_validate(
|
||||
_tool_use_attr(block, "input")
|
||||
)
|
||||
self._emit_call_completed_event(
|
||||
response=structured_data.model_dump_json(),
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
@@ -1194,11 +1244,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
return structured_data
|
||||
|
||||
if final_message.content:
|
||||
tool_uses = [
|
||||
block
|
||||
for block in final_message.content
|
||||
if isinstance(block, (ToolUseBlock, BetaToolUseBlock))
|
||||
]
|
||||
tool_uses = _tool_use_blocks(list(final_message.content))
|
||||
|
||||
if tool_uses:
|
||||
if not available_functions:
|
||||
@@ -1229,7 +1275,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
def _execute_tools_and_collect_results(
|
||||
self,
|
||||
tool_uses: list[ToolUseBlock | BetaToolUseBlock],
|
||||
tool_uses: list[Any],
|
||||
available_functions: dict[str, Any],
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
@@ -1248,8 +1294,8 @@ class AnthropicCompletion(BaseLLM):
|
||||
tool_results = []
|
||||
|
||||
for tool_use in tool_uses:
|
||||
function_name = tool_use.name
|
||||
function_args = tool_use.input
|
||||
function_name = _tool_use_attr(tool_use, "name")
|
||||
function_args = _tool_use_attr(tool_use, "input")
|
||||
|
||||
result = self._handle_tool_execution(
|
||||
function_name=function_name,
|
||||
@@ -1261,7 +1307,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
tool_result = {
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tool_use.id,
|
||||
"tool_use_id": _tool_use_attr(tool_use, "id"),
|
||||
"content": str(result)
|
||||
if result is not None
|
||||
else "Tool execution completed",
|
||||
@@ -1272,7 +1318,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
def _execute_first_tool(
|
||||
self,
|
||||
tool_uses: list[ToolUseBlock | BetaToolUseBlock],
|
||||
tool_uses: list[Any],
|
||||
available_functions: dict[str, Any],
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
@@ -1293,8 +1339,8 @@ class AnthropicCompletion(BaseLLM):
|
||||
The result of the first tool execution, or None if execution failed
|
||||
"""
|
||||
tool_use = tool_uses[0]
|
||||
function_name = tool_use.name
|
||||
function_args = cast(dict[str, Any], tool_use.input)
|
||||
function_name = _tool_use_attr(tool_use, "name")
|
||||
function_args = cast(dict[str, Any], _tool_use_attr(tool_use, "input"))
|
||||
|
||||
return self._handle_tool_execution(
|
||||
function_name=function_name,
|
||||
@@ -1308,7 +1354,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
def _handle_tool_use_conversation(
|
||||
self,
|
||||
initial_response: Message | BetaMessage,
|
||||
tool_uses: list[ToolUseBlock | BetaToolUseBlock],
|
||||
tool_uses: list[Any],
|
||||
params: dict[str, Any],
|
||||
available_functions: dict[str, Any],
|
||||
from_task: Any | None = None,
|
||||
@@ -1335,13 +1381,13 @@ class AnthropicCompletion(BaseLLM):
|
||||
thinking_block = self._extract_thinking_block(block)
|
||||
if thinking_block:
|
||||
assistant_content.append(thinking_block)
|
||||
elif block.type == "tool_use":
|
||||
elif _is_tool_use_block(block):
|
||||
assistant_content.append(
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"input": block.input,
|
||||
"id": _tool_use_attr(block, "id"),
|
||||
"name": _tool_use_attr(block, "name"),
|
||||
"input": _tool_use_attr(block, "input"),
|
||||
}
|
||||
)
|
||||
elif hasattr(block, "text"):
|
||||
@@ -1494,10 +1540,12 @@ class AnthropicCompletion(BaseLLM):
|
||||
else:
|
||||
for block in response.content:
|
||||
if (
|
||||
isinstance(block, ToolUseBlock)
|
||||
and block.name == "structured_output"
|
||||
_is_tool_use_block(block)
|
||||
and _tool_use_attr(block, "name") == "structured_output"
|
||||
):
|
||||
structured_data = response_model.model_validate(block.input)
|
||||
structured_data = response_model.model_validate(
|
||||
_tool_use_attr(block, "input")
|
||||
)
|
||||
self._emit_call_completed_event(
|
||||
response=structured_data.model_dump_json(),
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
@@ -1510,13 +1558,9 @@ class AnthropicCompletion(BaseLLM):
|
||||
)
|
||||
return structured_data
|
||||
|
||||
# Handle both ToolUseBlock (regular API) and BetaToolUseBlock (beta API features)
|
||||
# Handle Anthropic tool-use blocks across stable, beta, and preview SDK shapes.
|
||||
if response.content:
|
||||
tool_uses = [
|
||||
block
|
||||
for block in response.content
|
||||
if isinstance(block, (ToolUseBlock, BetaToolUseBlock))
|
||||
]
|
||||
tool_uses = _tool_use_blocks(list(response.content))
|
||||
|
||||
if tool_uses:
|
||||
if not available_functions:
|
||||
@@ -1629,11 +1673,13 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
if event.type == "content_block_start":
|
||||
block = event.content_block
|
||||
if block.type == "tool_use":
|
||||
if _is_tool_use_block(block):
|
||||
block_index = event.index
|
||||
tool_use_id = _tool_use_attr(block, "id")
|
||||
tool_name = _tool_use_attr(block, "name")
|
||||
current_tool_calls[block_index] = {
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"id": tool_use_id,
|
||||
"name": tool_name,
|
||||
"arguments": "",
|
||||
"index": block_index,
|
||||
}
|
||||
@@ -1642,9 +1688,9 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": block.id,
|
||||
"id": tool_use_id,
|
||||
"function": {
|
||||
"name": block.name,
|
||||
"name": tool_name,
|
||||
"arguments": "",
|
||||
},
|
||||
"type": "function",
|
||||
@@ -1703,10 +1749,12 @@ class AnthropicCompletion(BaseLLM):
|
||||
return structured_data
|
||||
for block in final_message.content:
|
||||
if (
|
||||
isinstance(block, ToolUseBlock)
|
||||
and block.name == "structured_output"
|
||||
_is_tool_use_block(block)
|
||||
and _tool_use_attr(block, "name") == "structured_output"
|
||||
):
|
||||
structured_data = response_model.model_validate(block.input)
|
||||
structured_data = response_model.model_validate(
|
||||
_tool_use_attr(block, "input")
|
||||
)
|
||||
self._emit_call_completed_event(
|
||||
response=structured_data.model_dump_json(),
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
@@ -1720,11 +1768,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
return structured_data
|
||||
|
||||
if final_message.content:
|
||||
tool_uses = [
|
||||
block
|
||||
for block in final_message.content
|
||||
if isinstance(block, (ToolUseBlock, BetaToolUseBlock))
|
||||
]
|
||||
tool_uses = _tool_use_blocks(list(final_message.content))
|
||||
|
||||
if tool_uses:
|
||||
if not available_functions:
|
||||
@@ -1754,7 +1798,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
async def _ahandle_tool_use_conversation(
|
||||
self,
|
||||
initial_response: Message | BetaMessage,
|
||||
tool_uses: list[ToolUseBlock | BetaToolUseBlock],
|
||||
tool_uses: list[Any],
|
||||
params: dict[str, Any],
|
||||
available_functions: dict[str, Any],
|
||||
from_task: Any | None = None,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
import pytest
|
||||
|
||||
from crewai.llm import LLM
|
||||
@@ -1358,6 +1358,205 @@ _MANY_TOOLS = [
|
||||
]
|
||||
|
||||
|
||||
def _dict_tool_use_response():
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_123",
|
||||
"name": "search_web",
|
||||
"input": {"query": "CrewAI"},
|
||||
}
|
||||
]
|
||||
mock_response.usage = MagicMock(input_tokens=10, output_tokens=2)
|
||||
mock_response.stop_reason = "tool_use"
|
||||
mock_response.id = "msg_123"
|
||||
return mock_response
|
||||
|
||||
|
||||
class _SyncAnthropicStream:
|
||||
def __init__(self, events, final_message):
|
||||
self.events = events
|
||||
self.final_message = final_message
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.events)
|
||||
|
||||
def get_final_message(self):
|
||||
return self.final_message
|
||||
|
||||
|
||||
class _AsyncAnthropicStream:
|
||||
def __init__(self, events, final_message):
|
||||
self.events = list(events)
|
||||
self.final_message = final_message
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args):
|
||||
return False
|
||||
|
||||
def __aiter__(self):
|
||||
self._iter = iter(self.events)
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
try:
|
||||
return next(self._iter)
|
||||
except StopIteration:
|
||||
raise StopAsyncIteration
|
||||
|
||||
async def get_final_message(self):
|
||||
return self.final_message
|
||||
|
||||
|
||||
def _dict_tool_use_stream_events():
|
||||
return [
|
||||
types.SimpleNamespace(
|
||||
type="content_block_start",
|
||||
index=0,
|
||||
content_block={
|
||||
"type": "tool_use",
|
||||
"id": "toolu_123",
|
||||
"name": "search_web",
|
||||
"input": {},
|
||||
},
|
||||
),
|
||||
types.SimpleNamespace(
|
||||
type="content_block_delta",
|
||||
index=0,
|
||||
delta=types.SimpleNamespace(
|
||||
type="input_json_delta",
|
||||
partial_json='{"query":"CrewAI"}',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_anthropic_tool_use_dict_blocks_are_returned_as_tool_calls():
|
||||
"""Preview Anthropic models may return dict-shaped tool_use blocks."""
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
|
||||
llm = AnthropicCompletion(model="claude-fable-5")
|
||||
mock_response = _dict_tool_use_response()
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.messages.create.return_value = mock_response
|
||||
llm._client = mock_client
|
||||
|
||||
result = llm.call("Search for CrewAI", tools=_MANY_TOOLS)
|
||||
|
||||
assert result == mock_response.content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_anthropic_acall_returns_dict_tool_use_blocks_as_tool_calls():
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
|
||||
llm = AnthropicCompletion(model="claude-fable-5")
|
||||
mock_response = _dict_tool_use_response()
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.messages.create = AsyncMock(return_value=mock_response)
|
||||
llm._async_client = mock_client
|
||||
|
||||
result = await llm.acall("Search for CrewAI", tools=_MANY_TOOLS)
|
||||
|
||||
assert result == mock_response.content
|
||||
|
||||
|
||||
def test_anthropic_streaming_returns_dict_tool_use_blocks_as_tool_calls():
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
|
||||
llm = AnthropicCompletion(model="claude-fable-5", stream=True)
|
||||
mock_response = _dict_tool_use_response()
|
||||
mock_client = MagicMock()
|
||||
mock_client.messages.stream.return_value = _SyncAnthropicStream(
|
||||
_dict_tool_use_stream_events(), mock_response
|
||||
)
|
||||
llm._client = mock_client
|
||||
llm._emit_stream_chunk_event = MagicMock()
|
||||
|
||||
result = llm.call("Search for CrewAI", tools=_MANY_TOOLS)
|
||||
|
||||
assert result == mock_response.content
|
||||
assert any(
|
||||
call.kwargs.get("tool_call", {}).get("id") == "toolu_123"
|
||||
for call in llm._emit_stream_chunk_event.call_args_list
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_anthropic_async_streaming_returns_dict_tool_use_blocks_as_tool_calls():
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
|
||||
llm = AnthropicCompletion(model="claude-fable-5", stream=True)
|
||||
mock_response = _dict_tool_use_response()
|
||||
mock_client = MagicMock()
|
||||
mock_client.messages.stream.return_value = _AsyncAnthropicStream(
|
||||
_dict_tool_use_stream_events(), mock_response
|
||||
)
|
||||
llm._async_client = mock_client
|
||||
llm._emit_stream_chunk_event = MagicMock()
|
||||
|
||||
result = await llm.acall("Search for CrewAI", tools=_MANY_TOOLS)
|
||||
|
||||
assert result == mock_response.content
|
||||
assert any(
|
||||
call.kwargs.get("tool_call", {}).get("id") == "toolu_123"
|
||||
for call in llm._emit_stream_chunk_event.call_args_list
|
||||
)
|
||||
|
||||
|
||||
def test_anthropic_dict_tool_use_blocks_execute_available_function():
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
|
||||
llm = AnthropicCompletion(model="claude-fable-5")
|
||||
mock_response = _dict_tool_use_response()
|
||||
mock_client = MagicMock()
|
||||
mock_client.messages.create.return_value = mock_response
|
||||
llm._client = mock_client
|
||||
|
||||
result = llm.call(
|
||||
"Search for CrewAI",
|
||||
tools=_MANY_TOOLS,
|
||||
available_functions={"search_web": lambda query: f"found {query}"},
|
||||
)
|
||||
|
||||
assert result == "found CrewAI"
|
||||
|
||||
|
||||
def test_anthropic_dict_tool_use_blocks_work_in_follow_up_conversation():
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
|
||||
llm = AnthropicCompletion(model="claude-fable-5")
|
||||
initial_response = _dict_tool_use_response()
|
||||
final_response = MagicMock()
|
||||
final_response.content = [types.SimpleNamespace(text="Final answer")]
|
||||
final_response.usage = MagicMock(input_tokens=4, output_tokens=3)
|
||||
final_response.stop_reason = "end_turn"
|
||||
final_response.id = "msg_final"
|
||||
mock_client = MagicMock()
|
||||
mock_client.messages.create.return_value = final_response
|
||||
llm._client = mock_client
|
||||
|
||||
result = llm._handle_tool_use_conversation(
|
||||
initial_response,
|
||||
initial_response.content,
|
||||
params={"messages": []},
|
||||
available_functions={"search_web": lambda query: f"found {query}"},
|
||||
)
|
||||
|
||||
assert result == "Final answer"
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_tool_search_discovers_and_calls_tool():
|
||||
"""Tool search should discover the right tool and return a tool_use block."""
|
||||
|
||||
Reference in New Issue
Block a user