mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-26 01:05:10 +00:00
fix: detect Anthropic preview tool-use blocks (#6629)
* fix: detect anthropic preview tool-use blocks * fix: preserve typed Anthropic tool-use blocks * fix: bump gitpython for vulnerability scan * docs: clarify gitpython advisory ranges --------- Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com>
This commit is contained in:
@@ -107,7 +107,7 @@ stagehand = [
|
||||
"stagehand>=0.4.1",
|
||||
]
|
||||
github = [
|
||||
"gitpython>=3.1.51,<4",
|
||||
"gitpython>=3.1.55,<4",
|
||||
"PyGithub==1.59.1",
|
||||
]
|
||||
rag = [
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Final, Literal, TypeGuard, cast
|
||||
from typing import Any, Final, Literal, Protocol, TypeGuard, TypedDict, cast
|
||||
|
||||
from pydantic import BaseModel, PrivateAttr, model_validator
|
||||
|
||||
@@ -124,6 +124,68 @@ def _contains_file_id_reference(messages: list[dict[str, Any]]) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class _ToolUseDictBlock(TypedDict):
|
||||
type: Literal["tool_use"]
|
||||
id: str
|
||||
name: str
|
||||
input: dict[str, Any]
|
||||
|
||||
|
||||
class _ToolUseObjectBlock(Protocol):
|
||||
type: Literal["tool_use"]
|
||||
id: str
|
||||
name: str
|
||||
input: dict[str, object]
|
||||
|
||||
|
||||
_AnthropicToolUseBlock = (
|
||||
ToolUseBlock | BetaToolUseBlock | _ToolUseDictBlock | _ToolUseObjectBlock
|
||||
)
|
||||
|
||||
|
||||
def _is_tool_use_block(block: Any) -> TypeGuard[_AnthropicToolUseBlock]:
|
||||
"""Return true for complete 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 API
|
||||
contract used by execution, streaming, and follow-up paths is ``type``,
|
||||
``id``, ``name``, and ``input``, so validate that full shape before treating
|
||||
a block as tool-use.
|
||||
"""
|
||||
if isinstance(block, dict):
|
||||
return (
|
||||
block.get("type") == "tool_use"
|
||||
and isinstance(block.get("id"), str)
|
||||
and isinstance(block.get("name"), str)
|
||||
and isinstance(block.get("input"), dict)
|
||||
)
|
||||
|
||||
return (
|
||||
getattr(block, "type", None) == "tool_use"
|
||||
and isinstance(getattr(block, "id", None), str)
|
||||
and isinstance(getattr(block, "name", None), str)
|
||||
and isinstance(getattr(block, "input", None), dict)
|
||||
)
|
||||
|
||||
|
||||
def _tool_use_blocks(blocks: list[Any]) -> list[_AnthropicToolUseBlock]:
|
||||
return [block for block in blocks if _is_tool_use_block(block)]
|
||||
|
||||
|
||||
def _tool_use_id(block: _AnthropicToolUseBlock) -> str:
|
||||
return block["id"] if isinstance(block, dict) else block.id
|
||||
|
||||
|
||||
def _tool_use_name(block: _AnthropicToolUseBlock) -> str:
|
||||
return block["name"] if isinstance(block, dict) else block.name
|
||||
|
||||
|
||||
def _tool_use_input(block: _AnthropicToolUseBlock) -> dict[str, Any]:
|
||||
return (
|
||||
block["input"] if isinstance(block, dict) else cast(dict[str, Any], block.input)
|
||||
)
|
||||
|
||||
|
||||
class AnthropicThinkingConfig(BaseModel):
|
||||
type: Literal["enabled", "disabled"]
|
||||
budget_tokens: int | None = None
|
||||
@@ -589,20 +651,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 +1027,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_name(block) == "structured_output"
|
||||
):
|
||||
structured_data = response_model.model_validate(block.input)
|
||||
structured_data = response_model.model_validate(
|
||||
_tool_use_input(block)
|
||||
)
|
||||
self._emit_call_completed_event(
|
||||
response=structured_data.model_dump_json(),
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
@@ -962,11 +1047,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 +1174,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_id(block)
|
||||
tool_name = _tool_use_name(block)
|
||||
current_tool_calls[block_index] = {
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"id": tool_use_id,
|
||||
"name": tool_name,
|
||||
"arguments": "",
|
||||
"index": block_index,
|
||||
}
|
||||
@@ -1106,9 +1189,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 +1260,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_name(block) == "structured_output"
|
||||
):
|
||||
structured_data = response_model.model_validate(block.input)
|
||||
structured_data = response_model.model_validate(
|
||||
_tool_use_input(block)
|
||||
)
|
||||
self._emit_call_completed_event(
|
||||
response=structured_data.model_dump_json(),
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
@@ -1194,11 +1279,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 +1310,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
def _execute_tools_and_collect_results(
|
||||
self,
|
||||
tool_uses: list[ToolUseBlock | BetaToolUseBlock],
|
||||
tool_uses: list[_AnthropicToolUseBlock],
|
||||
available_functions: dict[str, Any],
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
@@ -1248,12 +1329,12 @@ 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_name(tool_use)
|
||||
function_args = _tool_use_input(tool_use)
|
||||
|
||||
result = self._handle_tool_execution(
|
||||
function_name=function_name,
|
||||
function_args=cast(dict[str, Any], function_args),
|
||||
function_args=function_args,
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
@@ -1261,7 +1342,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
tool_result = {
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tool_use.id,
|
||||
"tool_use_id": _tool_use_id(tool_use),
|
||||
"content": str(result)
|
||||
if result is not None
|
||||
else "Tool execution completed",
|
||||
@@ -1272,7 +1353,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
def _execute_first_tool(
|
||||
self,
|
||||
tool_uses: list[ToolUseBlock | BetaToolUseBlock],
|
||||
tool_uses: list[_AnthropicToolUseBlock],
|
||||
available_functions: dict[str, Any],
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
@@ -1293,8 +1374,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_name(tool_use)
|
||||
function_args = _tool_use_input(tool_use)
|
||||
|
||||
return self._handle_tool_execution(
|
||||
function_name=function_name,
|
||||
@@ -1308,7 +1389,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
def _handle_tool_use_conversation(
|
||||
self,
|
||||
initial_response: Message | BetaMessage,
|
||||
tool_uses: list[ToolUseBlock | BetaToolUseBlock],
|
||||
tool_uses: list[_AnthropicToolUseBlock],
|
||||
params: dict[str, Any],
|
||||
available_functions: dict[str, Any],
|
||||
from_task: Any | None = None,
|
||||
@@ -1335,13 +1416,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_id(block),
|
||||
"name": _tool_use_name(block),
|
||||
"input": _tool_use_input(block),
|
||||
}
|
||||
)
|
||||
elif hasattr(block, "text"):
|
||||
@@ -1494,10 +1575,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_name(block) == "structured_output"
|
||||
):
|
||||
structured_data = response_model.model_validate(block.input)
|
||||
structured_data = response_model.model_validate(
|
||||
_tool_use_input(block)
|
||||
)
|
||||
self._emit_call_completed_event(
|
||||
response=structured_data.model_dump_json(),
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
@@ -1510,13 +1593,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 +1708,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_id(block)
|
||||
tool_name = _tool_use_name(block)
|
||||
current_tool_calls[block_index] = {
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"id": tool_use_id,
|
||||
"name": tool_name,
|
||||
"arguments": "",
|
||||
"index": block_index,
|
||||
}
|
||||
@@ -1642,9 +1723,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 +1784,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_name(block) == "structured_output"
|
||||
):
|
||||
structured_data = response_model.model_validate(block.input)
|
||||
structured_data = response_model.model_validate(
|
||||
_tool_use_input(block)
|
||||
)
|
||||
self._emit_call_completed_event(
|
||||
response=structured_data.model_dump_json(),
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
@@ -1720,11 +1803,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 +1833,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
async def _ahandle_tool_use_conversation(
|
||||
self,
|
||||
initial_response: Message | BetaMessage,
|
||||
tool_uses: list[ToolUseBlock | BetaToolUseBlock],
|
||||
tool_uses: list[_AnthropicToolUseBlock],
|
||||
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,248 @@ _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
|
||||
|
||||
|
||||
def test_anthropic_dict_tool_use_blocks_require_id():
|
||||
"""Incomplete dict-shaped tool_use blocks are not valid tool calls."""
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
|
||||
llm = AnthropicCompletion(model="claude-fable-5")
|
||||
mock_response = _dict_tool_use_response()
|
||||
del mock_response.content[0]["id"]
|
||||
|
||||
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 == ""
|
||||
|
||||
|
||||
def test_anthropic_object_tool_use_blocks_require_id():
|
||||
"""Incomplete object-shaped tool_use blocks are not valid tool calls."""
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
|
||||
llm = AnthropicCompletion(model="claude-fable-5")
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = [
|
||||
types.SimpleNamespace(
|
||||
type="tool_use",
|
||||
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"
|
||||
|
||||
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 == ""
|
||||
|
||||
|
||||
@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