mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-25 08:45:09 +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."""
|
||||
|
||||
@@ -172,7 +172,7 @@ info = "Commits must follow Conventional Commits 1.0.0."
|
||||
[tool.uv]
|
||||
exclude-newer = "3 days"
|
||||
# These security fixes are newer than the global supply-chain cutoff.
|
||||
exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z" }
|
||||
exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z", gitpython = "2026-07-24T00:00:00Z" }
|
||||
|
||||
# composio-core pins rich<14 but textual requires rich>=14.
|
||||
# onnxruntime 1.24+ dropped Python 3.10 wheels; cap it so qdrant[fastembed] resolves on 3.10.
|
||||
@@ -187,7 +187,9 @@ exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T
|
||||
# uv <0.11.15 has GHSA-4gg8-gxpx-9rph (and earlier GHSA-pjjw-68hj-v9mw); force 0.11.15+.
|
||||
# python-multipart <0.0.27 has GHSA-pp6c-gr5w-3c5g (DoS via unbounded multipart headers).
|
||||
# gitpython <3.1.50 has GHSA-mv93-w799-cj2w (config_writer newline injection bypassing the 3.1.49 patch -> RCE via core.hooksPath).
|
||||
# gitpython <3.1.51 has GHSA-2f96-g7mh-g2hx, GHSA-v396-v7q4-x2qj, and GHSA-956x-8gvw-wg5v; force 3.1.51+.
|
||||
# gitpython <3.1.51 has GHSA-2f96-g7mh-g2hx, GHSA-v396-v7q4-x2qj, and GHSA-956x-8gvw-wg5v.
|
||||
# gitpython <=3.1.51 has GHSA-rwj8-pgh3-r573; fixed in 3.1.52.
|
||||
# gitpython 3.1.52 has GHSA-3rp5-jjmw-4wv2, GHSA-fjr4-x663-mwxc, GHSA-6p8h-3wgx-97gf, and GHSA-r9mr-m37c-5fr3; force 3.1.55+.
|
||||
# pyasn1 <0.6.4 has GHSA-8ppf-4f7h-5ppj and GHSA-hm4w-wwcw-mr6r; force 0.6.4+.
|
||||
# urllib3 <2.7.0 has GHSA-qccp-gfcp-xxvc (ProxyManager cross-origin redirect leaks Authorization/Cookie) and GHSA-mf9v-mfxr-j63j (streaming decompression-bomb bypass); force 2.7.0+.
|
||||
# langsmith <0.8.18 has GHSA-3644-q5cj-c5c7 (public prompt manifest deserialization, SSRF/secret disclosure)
|
||||
@@ -216,7 +218,7 @@ override-dependencies = [
|
||||
"pypdf>=6.14.2,<7",
|
||||
"uv>=0.11.15,<1",
|
||||
"python-multipart>=0.0.27,<1",
|
||||
"gitpython>=3.1.51,<4",
|
||||
"gitpython>=3.1.55,<4",
|
||||
"pyasn1>=0.6.4",
|
||||
"langsmith>=0.8.18,<1",
|
||||
"authlib>=1.6.12",
|
||||
|
||||
13
uv.lock
generated
13
uv.lock
generated
@@ -13,12 +13,13 @@ resolution-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-07-21T05:40:28.001333135Z"
|
||||
exclude-newer = "2026-07-21T18:21:00.467584933Z"
|
||||
exclude-newer-span = "P3D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
msgpack = "2026-06-20T00:00:00Z"
|
||||
langsmith = "2026-06-20T00:00:00Z"
|
||||
gitpython = "2026-07-24T00:00:00Z"
|
||||
pypdf = "2026-06-24T00:00:00Z"
|
||||
pydantic-settings = "2026-06-20T00:00:00Z"
|
||||
|
||||
@@ -36,7 +37,7 @@ overrides = [
|
||||
{ name = "authlib", specifier = ">=1.6.12" },
|
||||
{ name = "cryptography", specifier = ">=46.0.7" },
|
||||
{ name = "docling-core", extras = ["chunking"], specifier = ">=2.74.1" },
|
||||
{ name = "gitpython", specifier = ">=3.1.51,<4" },
|
||||
{ name = "gitpython", specifier = ">=3.1.55,<4" },
|
||||
{ name = "langchain-core", specifier = ">=1.3.3,<2" },
|
||||
{ name = "langchain-text-splitters", specifier = ">=1.1.2,<2" },
|
||||
{ name = "langsmith", specifier = ">=0.8.18,<1" },
|
||||
@@ -1755,7 +1756,7 @@ requires-dist = [
|
||||
{ name = "e2b-code-interpreter", marker = "extra == 'e2b'", specifier = "~=2.6.0" },
|
||||
{ name = "exa-py", marker = "extra == 'exa-py'", specifier = ">=1.8.7" },
|
||||
{ name = "firecrawl-py", marker = "extra == 'firecrawl-py'", specifier = ">=1.8.0" },
|
||||
{ name = "gitpython", marker = "extra == 'github'", specifier = ">=3.1.51,<4" },
|
||||
{ name = "gitpython", marker = "extra == 'github'", specifier = ">=3.1.55,<4" },
|
||||
{ name = "hyperbrowser", marker = "extra == 'hyperbrowser'", specifier = ">=0.18.0" },
|
||||
{ name = "langchain-apify", marker = "extra == 'apify'", specifier = ">=0.1.2,<1.0.0" },
|
||||
{ name = "linkup-sdk", marker = "extra == 'linkup-sdk'", specifier = ">=0.2.2" },
|
||||
@@ -2767,14 +2768,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gitpython"
|
||||
version = "3.1.52"
|
||||
version = "3.1.55"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "gitdb" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e5/fd/df0bafa4eb5ea2f51e1adee9f7a94c8e62c5d180e65117045dfca3439c8a/gitpython-3.1.52.tar.gz", hash = "sha256:de0a8ad86274c6e75ae8b37dd055ba68f19818c813108642263227b20775b48e", size = 223726, upload-time = "2026-07-16T03:15:59.599Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/ab/ba0d29f2fa2277ed6256b2ac09003494045355f3a10bf32f351761287870/gitpython-3.1.55.tar.gz", hash = "sha256:781e3b1624dad81b24e9524bf0297b69786a0706db2cbceec1e2b05c38e5152f", size = 225071, upload-time = "2026-07-23T02:52:43.246Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/90/04dff7c1e176bb1c3011ef1647393d368790da710d8dde1cdcfad301f45a/gitpython-3.1.52-py3-none-any.whl", hash = "sha256:79a36ee1f83523214a3f72d56cf1c4e490d577dc61af77e43dfe5862bd9da01a", size = 215366, upload-time = "2026-07-16T03:15:58.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/6a/d3b8208d2f8aac66abe8ccc1c23fa2c89464ec42cc71a601e95d05902428/gitpython-3.1.55-py3-none-any.whl", hash = "sha256:7c9ec1e69c158c081632ab35c41471e302c96db2ae42165036a5d2403378812e", size = 216590, upload-time = "2026-07-23T02:52:41.932Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user