From c528e8bdeec223fddb896c9cdd0ad18fad1d95a6 Mon Sep 17 00:00:00 2001 From: alex-clawd Date: Fri, 24 Jul 2026 11:32:38 -0700 Subject: [PATCH] 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> --- lib/crewai-tools/pyproject.toml | 2 +- .../llms/providers/anthropic/completion.py | 213 ++++++++++----- .../tests/llms/anthropic/test_anthropic.py | 244 +++++++++++++++++- pyproject.toml | 8 +- uv.lock | 13 +- 5 files changed, 402 insertions(+), 78 deletions(-) diff --git a/lib/crewai-tools/pyproject.toml b/lib/crewai-tools/pyproject.toml index 8ec8bc75b..e621a04bf 100644 --- a/lib/crewai-tools/pyproject.toml +++ b/lib/crewai-tools/pyproject.toml @@ -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 = [ diff --git a/lib/crewai/src/crewai/llms/providers/anthropic/completion.py b/lib/crewai/src/crewai/llms/providers/anthropic/completion.py index b012aa61d..7deecbf60 100644 --- a/lib/crewai/src/crewai/llms/providers/anthropic/completion.py +++ b/lib/crewai/src/crewai/llms/providers/anthropic/completion.py @@ -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, diff --git a/lib/crewai/tests/llms/anthropic/test_anthropic.py b/lib/crewai/tests/llms/anthropic/test_anthropic.py index 1269b5bd3..fd21d3b8a 100644 --- a/lib/crewai/tests/llms/anthropic/test_anthropic.py +++ b/lib/crewai/tests/llms/anthropic/test_anthropic.py @@ -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.""" diff --git a/pyproject.toml b/pyproject.toml index 70352aff0..b6a2a8046 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/uv.lock b/uv.lock index 995818bfc..241397d56 100644 --- a/uv.lock +++ b/uv.lock @@ -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]]