Compare commits

..

1 Commits

Author SHA1 Message Date
Cursor Agent
eeb8120784 chore: verify benchmark path bug false positive 2026-03-01 09:34:29 +00:00
13 changed files with 71 additions and 1015 deletions

View File

@@ -21,7 +21,7 @@ dependencies = [
"opentelemetry-exporter-otlp-proto-http~=1.34.0",
# Data Handling
"chromadb~=1.1.0",
"tokenizers>=0.21,<1",
"tokenizers~=0.20.3",
"openpyxl~=3.1.5",
# Authentication and Security
"python-dotenv~=1.1.1",
@@ -88,7 +88,7 @@ bedrock = [
"boto3~=1.40.45",
]
google-genai = [
"google-genai~=1.65.0",
"google-genai~=1.49.0",
]
azure-ai-inference = [
"azure-ai-inference~=1.0.0b9",

View File

@@ -4,7 +4,6 @@ import urllib.request
import warnings
from crewai.agent.core import Agent
from crewai.agents.loop_detector import LoopDetector
from crewai.crew import Crew
from crewai.crews.crew_output import CrewOutput
from crewai.flow.flow import Flow
@@ -100,7 +99,6 @@ __all__ = [
"Flow",
"Knowledge",
"LLMGuardrail",
"LoopDetector",
"Memory",
"Process",
"Task",

View File

@@ -22,7 +22,6 @@ from typing_extensions import Self
from crewai.agent.internal.meta import AgentMeta
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
from crewai.agents.cache.cache_handler import CacheHandler
from crewai.agents.loop_detector import LoopDetector
from crewai.agents.tools_handler import ToolsHandler
from crewai.knowledge.knowledge import Knowledge
from crewai.knowledge.knowledge_config import KnowledgeConfig
@@ -214,15 +213,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
"If not set, falls back to crew memory."
),
)
loop_detector: LoopDetector | None = Field(
default=None,
description=(
"Optional loop detection middleware. When set, monitors agent tool calls "
"for repetitive patterns and intervenes to break loops. "
"Pass a LoopDetector instance to configure window_size, "
"repetition_threshold, and on_loop behavior."
),
)
@model_validator(mode="before")
@classmethod

View File

@@ -17,7 +17,6 @@ from pydantic import BaseModel, GetCoreSchemaHandler, ValidationError
from pydantic_core import CoreSchema, core_schema
from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
from crewai.agents.loop_detector import LoopDetector
from crewai.agents.parser import (
AgentAction,
AgentFinish,
@@ -157,11 +156,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
self.messages: list[LLMMessage] = []
self.iterations = 0
self.log_error_after = 3
self.loop_detector: LoopDetector | None = (
agent.loop_detector if agent and hasattr(agent, "loop_detector") else None
)
if self.loop_detector:
self.loop_detector.reset()
self.before_llm_call_hooks: list[Callable[..., Any]] = []
self.after_llm_call_hooks: list[Callable[..., Any]] = []
self.before_llm_call_hooks.extend(get_before_llm_call_hooks())
@@ -416,11 +410,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
)
}
# Capture tool info before _handle_agent_action may
# convert the answer to AgentFinish (result_as_answer).
_tool_name = formatted_answer.tool
_tool_input = formatted_answer.tool_input
tool_result = execute_tool_and_check_finality(
agent_action=formatted_answer,
fingerprint_context=fingerprint_context,
@@ -438,14 +427,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
formatted_answer, tool_result
)
# Check for repetitive loop patterns
loop_result = self._record_and_check_loop(
_tool_name, _tool_input
)
if loop_result is not None:
formatted_answer = loop_result
break
self._invoke_step_callback(formatted_answer) # type: ignore[arg-type]
self._append_message(formatted_answer.text) # type: ignore[union-attr]
@@ -802,14 +783,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
if tool_finish:
return tool_finish
# Check for loop after each parallel tool result
loop_result = self._record_and_check_loop(
execution_result["func_name"],
execution_result.get("tool_args", ""),
)
if loop_result is not None:
return loop_result
reasoning_prompt = self._i18n.slice("post_tool_reasoning")
reasoning_message: LLMMessage = {
"role": "user",
@@ -834,11 +807,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
if tool_finish:
return tool_finish
# Check for loop after sequential tool execution
loop_result = self._record_and_check_loop(func_name, func_args)
if loop_result is not None:
return loop_result
reasoning_prompt = self._i18n.slice("post_tool_reasoning")
reasoning_message = {
"role": "user",
@@ -1100,7 +1068,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
"result": result,
"from_cache": from_cache,
"original_tool": original_tool,
"tool_args": func_args,
}
def _append_tool_result_and_check_finality(
@@ -1279,11 +1246,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
)
}
# Capture tool info before _handle_agent_action may
# convert the answer to AgentFinish (result_as_answer).
_tool_name = formatted_answer.tool
_tool_input = formatted_answer.tool_input
tool_result = await aexecute_tool_and_check_finality(
agent_action=formatted_answer,
fingerprint_context=fingerprint_context,
@@ -1301,14 +1263,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
formatted_answer, tool_result
)
# Check for repetitive loop patterns
loop_result = self._record_and_check_loop(
_tool_name, _tool_input
)
if loop_result is not None:
formatted_answer = loop_result
break
await self._ainvoke_step_callback(formatted_answer) # type: ignore[arg-type]
self._append_message(formatted_answer.text) # type: ignore[union-attr]
@@ -1508,94 +1462,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
self._show_logs(formatted_answer)
return formatted_answer
def _record_and_check_loop(
self, tool_name: str, tool_args: str | dict[str, Any]
) -> AgentFinish | None:
"""Record a tool call and check for repetitive loop patterns.
If a loop is detected, takes the configured action:
- ``inject_reflection``: injects a reflection prompt into messages.
- ``stop``: forces the agent to produce a final answer.
- callable: calls the user's callback and injects the returned message.
Args:
tool_name: Name of the tool that was called.
tool_args: Arguments passed to the tool.
Returns:
``AgentFinish`` if the loop action is ``stop``,
``None`` otherwise (including when reflection is injected).
"""
if not self.loop_detector:
return None
self.loop_detector.record_tool_call(tool_name, tool_args)
if not self.loop_detector.is_loop_detected():
return None
repeated_tool = self.loop_detector.get_repeated_tool_info() or tool_name
action_taken = (
self.loop_detector.on_loop
if isinstance(self.loop_detector.on_loop, str)
else "callback"
)
# Emit loop detected event
from crewai.events.types.loop_events import LoopDetectedEvent
crewai_event_bus.emit(
self.agent,
LoopDetectedEvent(
agent_role=self.agent.role if self.agent else "unknown",
agent_id=str(self.agent.id) if self.agent else None,
task_id=str(self.task.id) if self.task else None,
repeated_tool=repeated_tool,
action_taken=action_taken,
iteration=self.iterations,
agent=self.agent,
),
)
if self.agent and self.agent.verbose:
self._printer.print(
content=f"Loop detected: {repeated_tool} called repeatedly. Action: {action_taken}",
color="bold_yellow",
)
if self.loop_detector.on_loop == "stop":
return handle_max_iterations_exceeded(
None,
printer=self._printer,
i18n=self._i18n,
messages=self.messages,
llm=self.llm,
callbacks=self.callbacks,
verbose=self.agent.verbose if self.agent else False,
)
# inject_reflection or callable
if callable(self.loop_detector.on_loop) and not isinstance(
self.loop_detector.on_loop, str
):
message_text = self.loop_detector.get_loop_message()
else:
# Use i18n reflection prompt
count = self.loop_detector.repetition_threshold
message_text = self._i18n.slice("loop_detected").format(
repeated_tool=repeated_tool, count=count
)
loop_message: LLMMessage = {
"role": "user",
"content": message_text,
}
self.messages.append(loop_message)
# Reset detector after intervention to give the agent a fresh window
self.loop_detector.reset()
return None
def _handle_agent_action(
self, formatted_answer: AgentAction, tool_result: ToolResult
) -> AgentAction | AgentFinish:

View File

@@ -1,210 +0,0 @@
"""Loop detection for agent execution.
Detects repetitive behavioral patterns in agent tool calls and provides
configurable intervention strategies to break out of loops.
Example usage:
from crewai import Agent
from crewai.agents.loop_detector import LoopDetector
# Using default settings (window_size=5, repetition_threshold=3, inject_reflection)
agent = Agent(
role="Researcher",
goal="Find novel insights",
backstory="An experienced researcher",
loop_detector=LoopDetector(),
)
# Custom configuration
agent = Agent(
role="Researcher",
goal="Find novel insights",
backstory="An experienced researcher",
loop_detector=LoopDetector(
window_size=10,
repetition_threshold=4,
on_loop="stop",
),
)
# With a custom callback
def my_callback(detector: LoopDetector) -> str:
return "You are repeating yourself. Try a completely different approach."
agent = Agent(
role="Researcher",
goal="Find novel insights",
backstory="An experienced researcher",
loop_detector=LoopDetector(
on_loop=my_callback,
),
)
"""
from __future__ import annotations
from collections import deque
from collections.abc import Callable
import json
import logging
from typing import Any, Literal
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
class ToolCallRecord(BaseModel):
"""Record of a single tool call for loop detection."""
tool_name: str
tool_args: str # Normalized JSON string of arguments
def __eq__(self, other: object) -> bool:
if not isinstance(other, ToolCallRecord):
return NotImplemented
return self.tool_name == other.tool_name and self.tool_args == other.tool_args
def __hash__(self) -> int:
return hash((self.tool_name, self.tool_args))
class LoopDetector(BaseModel):
"""Detects repetitive behavioral patterns in agent tool calls.
Monitors a sliding window of recent tool calls and flags when
the same tool is called repeatedly with the same arguments,
indicating the agent is stuck in a loop.
Args:
window_size: Number of recent tool calls to track in the
sliding window. Defaults to 5.
repetition_threshold: How many identical tool calls within
the window trigger loop detection. Defaults to 3.
on_loop: Action to take when a loop is detected.
- ``"inject_reflection"`` (default): Inject a meta-prompt
asking the agent to try a different approach.
- ``"stop"``: Force the agent to produce a final answer.
- A callable ``(LoopDetector) -> str``: Custom callback
that returns a message to inject into the conversation.
Example::
from crewai import Agent
from crewai.agents.loop_detector import LoopDetector
agent = Agent(
role="Researcher",
goal="Find novel insights about AI",
backstory="Senior researcher",
loop_detector=LoopDetector(
window_size=5,
repetition_threshold=3,
on_loop="inject_reflection",
),
)
"""
window_size: int = Field(
default=5,
ge=2,
description="Number of recent tool calls to track in the sliding window.",
)
repetition_threshold: int = Field(
default=3,
ge=2,
description="How many identical tool calls within the window trigger loop detection.",
)
on_loop: Literal["inject_reflection", "stop"] | Callable[..., str] = Field(
default="inject_reflection",
description=(
"Action when loop is detected: 'inject_reflection' to inject a reflection prompt, "
"'stop' to force final answer, or a callable(LoopDetector) -> str for custom handling."
),
)
_history: deque[ToolCallRecord] = deque()
def model_post_init(self, __context: Any) -> None:
"""Initialize the internal deque with the configured window size."""
object.__setattr__(self, "_history", deque(maxlen=self.window_size))
@staticmethod
def _normalize_args(args: str | dict[str, Any]) -> str:
"""Normalize tool arguments to a canonical JSON string for comparison.
Args:
args: Tool arguments as a string or dict.
Returns:
A normalized JSON string with sorted keys.
"""
if isinstance(args, str):
try:
parsed = json.loads(args)
except (json.JSONDecodeError, TypeError):
return args.strip()
return json.dumps(parsed, sort_keys=True, default=str)
return json.dumps(args, sort_keys=True, default=str)
def record_tool_call(self, tool_name: str, tool_args: str | dict[str, Any]) -> None:
"""Record a tool call in the sliding window.
Args:
tool_name: Name of the tool that was called.
tool_args: Arguments passed to the tool.
"""
record = ToolCallRecord(
tool_name=tool_name,
tool_args=self._normalize_args(tool_args),
)
self._history.append(record)
def is_loop_detected(self) -> bool:
"""Check if a repetitive loop pattern is detected.
Returns:
True if any single tool call (same name + same args)
appears at least ``repetition_threshold`` times within
the current window.
"""
if len(self._history) < self.repetition_threshold:
return False
counts: dict[ToolCallRecord, int] = {}
for record in self._history:
counts[record] = counts.get(record, 0) + 1
if counts[record] >= self.repetition_threshold:
return True
return False
def get_loop_message(self) -> str:
"""Get the intervention message based on the configured ``on_loop`` action.
Returns:
A string message to inject into the agent conversation.
"""
if callable(self.on_loop) and not isinstance(self.on_loop, str):
return self.on_loop(self)
return ""
def get_repeated_tool_info(self) -> str | None:
"""Get information about the repeated tool call, if any.
Returns:
A string describing the repeated tool and args, or None.
"""
if len(self._history) < self.repetition_threshold:
return None
counts: dict[ToolCallRecord, int] = {}
for record in self._history:
counts[record] = counts.get(record, 0) + 1
if counts[record] >= self.repetition_threshold:
return f"{record.tool_name}({record.tool_args})"
return None
def reset(self) -> None:
"""Clear the tool call history."""
self._history.clear()

View File

@@ -22,15 +22,14 @@ class PlusAPI:
EPHEMERAL_TRACING_RESOURCE = "/crewai_plus/api/v1/tracing/ephemeral"
INTEGRATIONS_RESOURCE = "/crewai_plus/api/v1/integrations"
def __init__(self, api_key: str | None = None) -> None:
def __init__(self, api_key: str) -> None:
self.api_key = api_key
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"User-Agent": f"CrewAI-CLI/{get_crewai_version()}",
"X-Crewai-Version": get_crewai_version(),
}
if api_key:
self.headers["Authorization"] = f"Bearer {api_key}"
settings = Settings()
if settings.org_uuid:
self.headers["X-Crewai-Organization-Id"] = settings.org_uuid

View File

@@ -67,7 +67,7 @@ class TraceBatchManager:
api_key=get_auth_token(),
)
except AuthError:
self.plus_api = PlusAPI()
self.plus_api = PlusAPI(api_key="")
self.ephemeral_trace_url = None
def initialize_batch(

View File

@@ -1,48 +0,0 @@
"""Events related to agent loop detection."""
from __future__ import annotations
from typing import Any
from pydantic import ConfigDict, model_validator
from crewai.events.base_events import BaseEvent
class LoopDetectedEvent(BaseEvent):
"""Event emitted when a repetitive loop pattern is detected in agent behavior.
Attributes:
agent_role: Role of the agent that is looping.
repeated_tool: Description of the repeated tool call (name + args).
action_taken: The action taken to break the loop.
iteration: Current iteration count when the loop was detected.
"""
agent_role: str
agent_id: str | None = None
task_id: str | None = None
repeated_tool: str | None = None
action_taken: str
iteration: int
agent: Any | None = None
type: str = "loop_detected"
model_config = ConfigDict(arbitrary_types_allowed=True)
@model_validator(mode="after")
def set_fingerprint_data(self) -> LoopDetectedEvent:
"""Set fingerprint data from the agent if available."""
if (
self.agent
and hasattr(self.agent, "fingerprint")
and self.agent.fingerprint
):
self.source_fingerprint = self.agent.fingerprint.uuid_str
self.source_type = "agent"
if (
hasattr(self.agent.fingerprint, "metadata")
and self.agent.fingerprint.metadata
):
self.fingerprint_metadata = self.agent.fingerprint.metadata
return self

View File

@@ -173,12 +173,6 @@ class Telemetry:
self._original_handlers: dict[int, Any] = {}
if threading.current_thread() is not threading.main_thread():
logger.debug(
"Skipping signal handler registration: not running in main thread"
)
return
self._register_signal_handler(signal.SIGTERM, SigTermEvent, shutdown=True)
self._register_signal_handler(signal.SIGINT, SigIntEvent, shutdown=True)
if hasattr(signal, "SIGHUP"):

View File

@@ -15,7 +15,6 @@
"native_tools": "",
"native_task": "\nCurrent Task: {input}",
"post_tool_reasoning": "Analyze the tool result. If requirements are met, provide the Final Answer. Otherwise, call the next tool. Deliver only the answer without meta-commentary.",
"loop_detected": "WARNING: You appear to be repeating the same action ({repeated_tool}). You have called it {count} times with the same arguments. This is not making progress. You MUST try a completely different approach — use a different tool, different arguments, or provide your Final Answer now.",
"format": "Decide if you need a tool or can provide the final answer. Use one at a time.\nTo use a tool, use:\nThought: [reasoning]\nAction: [name from {tool_names}]\nAction Input: [JSON object]\n\nTo provide the final answer, use:\nThought: [reasoning]\nFinal Answer: [complete response]",
"final_answer_format": "If you don't need to use any more tools, you must give your best complete final answer, make sure it satisfies the expected criteria, use the EXACT format below:\n\n```\nThought: I now can give a great answer\nFinal Answer: my best complete final answer to the task.\n\n```",
"format_without_tools": "\nSorry, I didn't use the right format. I MUST either use a tool (among the available ones), OR give my best final answer.\nHere is the expected format I must follow:\n\n```\nQuestion: the input question you must answer\nThought: you should always think about what to do\nAction: the action to take, should be one of [{tool_names}]\nAction Input: the input to the action\nObservation: the result of the action\n```\n This Thought/Action/Action Input/Result process can repeat N times. Once I know the final answer, I must return the following format:\n\n```\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described\n\n```",

View File

@@ -1,523 +0,0 @@
"""Unit tests for LoopDetector and loop detection integration.
Tests cover:
- LoopDetector class: tracking, detection, message generation, configuration
- ToolCallRecord equality and hashing
- Edge cases: empty history, threshold boundaries, window overflow
- Integration with CrewAgentExecutor._record_and_check_loop
- LoopDetectedEvent creation
"""
from __future__ import annotations
from unittest.mock import MagicMock, Mock, patch
import pytest
from crewai.agents.loop_detector import LoopDetector, ToolCallRecord
from crewai.events.types.loop_events import LoopDetectedEvent
# ---------------------------------------------------------------------------
# ToolCallRecord
# ---------------------------------------------------------------------------
class TestToolCallRecord:
"""Tests for ToolCallRecord model."""
def test_equality_same_name_and_args(self):
a = ToolCallRecord(tool_name="search", tool_args='{"q": "hello"}')
b = ToolCallRecord(tool_name="search", tool_args='{"q": "hello"}')
assert a == b
def test_inequality_different_name(self):
a = ToolCallRecord(tool_name="search", tool_args='{"q": "hello"}')
b = ToolCallRecord(tool_name="fetch", tool_args='{"q": "hello"}')
assert a != b
def test_inequality_different_args(self):
a = ToolCallRecord(tool_name="search", tool_args='{"q": "hello"}')
b = ToolCallRecord(tool_name="search", tool_args='{"q": "world"}')
assert a != b
def test_hash_same_records(self):
a = ToolCallRecord(tool_name="search", tool_args='{"q": "hello"}')
b = ToolCallRecord(tool_name="search", tool_args='{"q": "hello"}')
assert hash(a) == hash(b)
def test_hash_different_records(self):
a = ToolCallRecord(tool_name="search", tool_args='{"q": "hello"}')
b = ToolCallRecord(tool_name="search", tool_args='{"q": "world"}')
# Different records should (almost certainly) have different hashes
assert hash(a) != hash(b)
def test_equality_not_implemented_for_other_types(self):
a = ToolCallRecord(tool_name="search", tool_args="{}")
assert a != "not a record"
assert a.__eq__("not a record") is NotImplemented
# ---------------------------------------------------------------------------
# LoopDetector — Initialization
# ---------------------------------------------------------------------------
class TestLoopDetectorInit:
"""Tests for LoopDetector initialization and defaults."""
def test_default_values(self):
ld = LoopDetector()
assert ld.window_size == 5
assert ld.repetition_threshold == 3
assert ld.on_loop == "inject_reflection"
def test_custom_values(self):
ld = LoopDetector(window_size=10, repetition_threshold=4, on_loop="stop")
assert ld.window_size == 10
assert ld.repetition_threshold == 4
assert ld.on_loop == "stop"
def test_callable_on_loop(self):
def my_cb(detector: LoopDetector) -> str:
return "custom"
ld = LoopDetector(on_loop=my_cb)
assert callable(ld.on_loop)
def test_window_size_minimum(self):
with pytest.raises(Exception):
LoopDetector(window_size=1)
def test_repetition_threshold_minimum(self):
with pytest.raises(Exception):
LoopDetector(repetition_threshold=1)
# ---------------------------------------------------------------------------
# LoopDetector — Argument Normalization
# ---------------------------------------------------------------------------
class TestNormalizeArgs:
"""Tests for LoopDetector._normalize_args static method."""
def test_dict_sorted_keys(self):
result = LoopDetector._normalize_args({"b": 2, "a": 1})
assert result == '{"a": 1, "b": 2}'
def test_json_string_normalized(self):
result = LoopDetector._normalize_args('{"b": 2, "a": 1}')
assert result == '{"a": 1, "b": 2}'
def test_plain_string_stripped(self):
result = LoopDetector._normalize_args(" hello world ")
assert result == "hello world"
def test_invalid_json_string(self):
result = LoopDetector._normalize_args("not json")
assert result == "not json"
def test_empty_dict(self):
result = LoopDetector._normalize_args({})
assert result == "{}"
def test_empty_string(self):
result = LoopDetector._normalize_args("")
assert result == ""
# ---------------------------------------------------------------------------
# LoopDetector — Recording and Detection
# ---------------------------------------------------------------------------
class TestLoopDetection:
"""Tests for loop recording and detection logic."""
def test_no_loop_below_threshold(self):
ld = LoopDetector(repetition_threshold=3)
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("search", {"q": "test"})
assert ld.is_loop_detected() is False
def test_loop_at_threshold(self):
ld = LoopDetector(repetition_threshold=3)
for _ in range(3):
ld.record_tool_call("search", {"q": "test"})
assert ld.is_loop_detected() is True
def test_loop_above_threshold(self):
ld = LoopDetector(repetition_threshold=3)
for _ in range(5):
ld.record_tool_call("search", {"q": "test"})
assert ld.is_loop_detected() is True
def test_no_loop_with_different_tools(self):
ld = LoopDetector(repetition_threshold=3)
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("fetch", {"url": "http://example.com"})
ld.record_tool_call("read", {"file": "data.txt"})
assert ld.is_loop_detected() is False
def test_no_loop_with_different_args(self):
ld = LoopDetector(repetition_threshold=3)
ld.record_tool_call("search", {"q": "test1"})
ld.record_tool_call("search", {"q": "test2"})
ld.record_tool_call("search", {"q": "test3"})
assert ld.is_loop_detected() is False
def test_loop_with_mixed_calls(self):
"""Loop detected even if other calls are interspersed, as long as
threshold is met within the window."""
ld = LoopDetector(window_size=5, repetition_threshold=3)
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("fetch", {"url": "http://example.com"})
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("search", {"q": "test"})
assert ld.is_loop_detected() is True
def test_window_overflow_removes_old_calls(self):
"""Old calls slide out of the window, potentially clearing the loop."""
ld = LoopDetector(window_size=3, repetition_threshold=3)
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("search", {"q": "test"})
# Now add a different call — this pushes first "search" out when we add
# one more different call
ld.record_tool_call("fetch", {"url": "http://example.com"})
# Only 2 "search" in window at this point, not 3
assert ld.is_loop_detected() is False
def test_loop_with_string_args(self):
ld = LoopDetector(repetition_threshold=3)
for _ in range(3):
ld.record_tool_call("search", '{"q": "test"}')
assert ld.is_loop_detected() is True
def test_loop_with_equivalent_json_args(self):
"""Args with different key order should be normalized and treated as equal."""
ld = LoopDetector(repetition_threshold=3)
ld.record_tool_call("search", '{"b": 2, "a": 1}')
ld.record_tool_call("search", '{"a": 1, "b": 2}')
ld.record_tool_call("search", {"a": 1, "b": 2})
assert ld.is_loop_detected() is True
def test_empty_history(self):
ld = LoopDetector()
assert ld.is_loop_detected() is False
def test_single_call(self):
ld = LoopDetector()
ld.record_tool_call("search", {"q": "test"})
assert ld.is_loop_detected() is False
# ---------------------------------------------------------------------------
# LoopDetector — Reset
# ---------------------------------------------------------------------------
class TestLoopDetectorReset:
"""Tests for LoopDetector.reset()."""
def test_reset_clears_history(self):
ld = LoopDetector(repetition_threshold=3)
for _ in range(3):
ld.record_tool_call("search", {"q": "test"})
assert ld.is_loop_detected() is True
ld.reset()
assert ld.is_loop_detected() is False
def test_reset_allows_fresh_tracking(self):
ld = LoopDetector(repetition_threshold=3)
for _ in range(3):
ld.record_tool_call("search", {"q": "test"})
ld.reset()
# Now record again — should not trigger until threshold
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("search", {"q": "test"})
assert ld.is_loop_detected() is False
# ---------------------------------------------------------------------------
# LoopDetector — Messages and Tool Info
# ---------------------------------------------------------------------------
class TestLoopDetectorMessages:
"""Tests for get_loop_message and get_repeated_tool_info."""
def test_get_loop_message_with_callable(self):
def my_callback(detector: LoopDetector) -> str:
return "Custom loop message"
ld = LoopDetector(on_loop=my_callback)
assert ld.get_loop_message() == "Custom loop message"
def test_get_loop_message_inject_reflection_returns_empty(self):
ld = LoopDetector(on_loop="inject_reflection")
assert ld.get_loop_message() == ""
def test_get_loop_message_stop_returns_empty(self):
ld = LoopDetector(on_loop="stop")
assert ld.get_loop_message() == ""
def test_get_repeated_tool_info_with_loop(self):
ld = LoopDetector(repetition_threshold=3)
for _ in range(3):
ld.record_tool_call("search", {"q": "test"})
info = ld.get_repeated_tool_info()
assert info is not None
assert "search" in info
def test_get_repeated_tool_info_no_loop(self):
ld = LoopDetector(repetition_threshold=3)
ld.record_tool_call("search", {"q": "test"})
assert ld.get_repeated_tool_info() is None
def test_get_repeated_tool_info_empty_history(self):
ld = LoopDetector()
assert ld.get_repeated_tool_info() is None
def test_callback_receives_detector_instance(self):
received = {}
def my_callback(detector: LoopDetector) -> str:
received["detector"] = detector
return "msg"
ld = LoopDetector(on_loop=my_callback)
ld.get_loop_message()
assert received["detector"] is ld
# ---------------------------------------------------------------------------
# LoopDetectedEvent
# ---------------------------------------------------------------------------
class TestLoopDetectedEvent:
"""Tests for the LoopDetectedEvent model."""
def test_event_creation(self):
event = LoopDetectedEvent(
agent_role="Researcher",
agent_id="agent-1",
task_id="task-1",
repeated_tool="search({\"q\": \"test\"})",
action_taken="inject_reflection",
iteration=5,
)
assert event.agent_role == "Researcher"
assert event.repeated_tool == 'search({"q": "test"})'
assert event.action_taken == "inject_reflection"
assert event.iteration == 5
assert event.type == "loop_detected"
def test_event_with_agent_fingerprint(self):
mock_agent = MagicMock()
mock_agent.fingerprint.uuid_str = "fp-123"
mock_agent.fingerprint.metadata = {"key": "value"}
event = LoopDetectedEvent(
agent_role="Researcher",
action_taken="stop",
iteration=3,
agent=mock_agent,
)
assert event.source_fingerprint == "fp-123"
assert event.fingerprint_metadata == {"key": "value"}
def test_event_without_agent(self):
event = LoopDetectedEvent(
agent_role="Researcher",
action_taken="inject_reflection",
iteration=1,
)
assert event.agent is None
assert event.agent_id is None
# ---------------------------------------------------------------------------
# Integration: CrewAgentExecutor._record_and_check_loop
# ---------------------------------------------------------------------------
class TestRecordAndCheckLoop:
"""Tests for the _record_and_check_loop helper on CrewAgentExecutor."""
@pytest.fixture
def _make_executor(self):
"""Factory that builds a minimal CrewAgentExecutor-like object.
We import the real class and patch its __init__ so we can
test _record_and_check_loop without setting up the full executor.
"""
from crewai.agents.crew_agent_executor import CrewAgentExecutor
def _factory(
loop_detector: LoopDetector | None = None,
verbose: bool = False,
) -> CrewAgentExecutor:
executor = object.__new__(CrewAgentExecutor)
# Minimal attributes required by _record_and_check_loop
executor.loop_detector = loop_detector
mock_agent = Mock()
mock_agent.role = "Test Agent"
mock_agent.id = "agent-1"
mock_agent.verbose = verbose
executor.agent = mock_agent
mock_task = Mock()
mock_task.id = "task-1"
executor.task = mock_task
executor.iterations = 0
executor.messages = []
mock_printer = Mock()
executor._printer = mock_printer
mock_i18n = Mock()
mock_i18n.slice.return_value = (
"WARNING: You appear to be repeating the same action ({repeated_tool}). "
"You have called it {count} times."
)
executor._i18n = mock_i18n
mock_llm = Mock()
executor.llm = mock_llm
executor.callbacks = []
return executor
return _factory
def test_no_loop_detector_returns_none(self, _make_executor):
executor = _make_executor(loop_detector=None)
result = executor._record_and_check_loop("search", {"q": "test"})
assert result is None
def test_no_loop_detected_returns_none(self, _make_executor):
ld = LoopDetector(repetition_threshold=3)
executor = _make_executor(loop_detector=ld)
# Only 1 call — no loop
result = executor._record_and_check_loop("search", {"q": "test"})
assert result is None
@patch("crewai.agents.crew_agent_executor.crewai_event_bus")
def test_inject_reflection_appends_message(self, mock_bus, _make_executor):
ld = LoopDetector(repetition_threshold=3, on_loop="inject_reflection")
executor = _make_executor(loop_detector=ld)
# Record 2 calls before the 3rd (which triggers detection)
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("search", {"q": "test"})
result = executor._record_and_check_loop("search", {"q": "test"})
assert result is None # inject_reflection does NOT stop
assert len(executor.messages) == 1
assert "repeating" in executor.messages[0]["content"].lower()
# Event should have been emitted
mock_bus.emit.assert_called_once()
@patch("crewai.agents.crew_agent_executor.crewai_event_bus")
@patch("crewai.agents.crew_agent_executor.handle_max_iterations_exceeded")
def test_stop_returns_agent_finish(
self, mock_handle_max, mock_bus, _make_executor
):
from crewai.agents.parser import AgentFinish
mock_handle_max.return_value = AgentFinish(
thought="forced", output="Stopped", text="Stopped"
)
ld = LoopDetector(repetition_threshold=3, on_loop="stop")
executor = _make_executor(loop_detector=ld)
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("search", {"q": "test"})
result = executor._record_and_check_loop("search", {"q": "test"})
assert result is not None
assert isinstance(result, AgentFinish)
mock_handle_max.assert_called_once()
mock_bus.emit.assert_called_once()
@patch("crewai.agents.crew_agent_executor.crewai_event_bus")
def test_callable_on_loop_injects_callback_message(
self, mock_bus, _make_executor
):
def custom_callback(detector: LoopDetector) -> str:
return "CUSTOM: Stop repeating yourself!"
ld = LoopDetector(repetition_threshold=3, on_loop=custom_callback)
executor = _make_executor(loop_detector=ld)
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("search", {"q": "test"})
result = executor._record_and_check_loop("search", {"q": "test"})
assert result is None
assert len(executor.messages) == 1
assert "CUSTOM: Stop repeating yourself!" in executor.messages[0]["content"]
@patch("crewai.agents.crew_agent_executor.crewai_event_bus")
def test_detection_resets_after_intervention(self, mock_bus, _make_executor):
ld = LoopDetector(repetition_threshold=3, on_loop="inject_reflection")
executor = _make_executor(loop_detector=ld)
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("search", {"q": "test"})
# 3rd call triggers loop
executor._record_and_check_loop("search", {"q": "test"})
# After intervention, detector should be reset
assert ld.is_loop_detected() is False
@patch("crewai.agents.crew_agent_executor.crewai_event_bus")
def test_verbose_prints_message(self, mock_bus, _make_executor):
ld = LoopDetector(repetition_threshold=3, on_loop="inject_reflection")
executor = _make_executor(loop_detector=ld, verbose=True)
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("search", {"q": "test"})
executor._record_and_check_loop("search", {"q": "test"})
executor._printer.print.assert_called_once()
call_kwargs = executor._printer.print.call_args
assert "Loop detected" in call_kwargs.kwargs.get(
"content", call_kwargs[1].get("content", "")
)
@patch("crewai.agents.crew_agent_executor.crewai_event_bus")
def test_event_contains_correct_data(self, mock_bus, _make_executor):
ld = LoopDetector(repetition_threshold=3, on_loop="inject_reflection")
executor = _make_executor(loop_detector=ld)
ld.record_tool_call("search", {"q": "test"})
ld.record_tool_call("search", {"q": "test"})
executor._record_and_check_loop("search", {"q": "test"})
event = mock_bus.emit.call_args[0][1]
assert isinstance(event, LoopDetectedEvent)
assert event.agent_role == "Test Agent"
assert event.action_taken == "inject_reflection"
assert event.iteration == 0 # executor.iterations was 0
# ---------------------------------------------------------------------------
# Public API import
# ---------------------------------------------------------------------------
class TestPublicImport:
"""Verify LoopDetector is importable from the public crewai package."""
def test_import_from_crewai(self):
from crewai import LoopDetector as LD
assert LD is LoopDetector
def test_in_all(self):
import crewai
assert "LoopDetector" in crewai.__all__

View File

@@ -121,41 +121,3 @@ def test_telemetry_singleton_pattern():
thread.join()
assert all(instance is telemetry1 for instance in instances)
def test_no_signal_handler_traceback_in_non_main_thread():
"""Signal handler registration should be silently skipped in non-main threads.
Regression test for https://github.com/crewAIInc/crewAI/issues/4289
"""
errors: list[Exception] = []
mock_holder: dict = {}
def init_in_thread():
try:
Telemetry._instance = None
with (
patch.dict(
os.environ,
{"CREWAI_DISABLE_TELEMETRY": "false", "OTEL_SDK_DISABLED": "false"},
),
patch("crewai.telemetry.telemetry.TracerProvider"),
patch("signal.signal") as mock_signal,
patch("crewai.telemetry.telemetry.logger") as mock_logger,
):
Telemetry()
mock_holder["signal"] = mock_signal
mock_holder["logger"] = mock_logger
except Exception as exc:
errors.append(exc)
thread = threading.Thread(target=init_in_thread)
thread.start()
thread.join()
assert not errors, f"Unexpected error: {errors}"
assert mock_holder, "Thread did not execute"
mock_holder["signal"].assert_not_called()
mock_holder["logger"].debug.assert_any_call(
"Skipping signal handler registration: not running in main thread"
)

103
uv.lock generated
View File

@@ -1197,7 +1197,7 @@ requires-dist = [
{ name = "crewai-files", marker = "extra == 'file-processing'", editable = "lib/crewai-files" },
{ name = "crewai-tools", marker = "extra == 'tools'", editable = "lib/crewai-tools" },
{ name = "docling", marker = "extra == 'docling'", specifier = "~=2.75.0" },
{ name = "google-genai", marker = "extra == 'google-genai'", specifier = "~=1.65.0" },
{ name = "google-genai", marker = "extra == 'google-genai'", specifier = "~=1.49.0" },
{ name = "httpx", specifier = "~=0.28.1" },
{ name = "httpx-auth", marker = "extra == 'a2a'", specifier = "~=0.23.1" },
{ name = "httpx-sse", marker = "extra == 'a2a'", specifier = "~=0.4.0" },
@@ -1227,7 +1227,7 @@ requires-dist = [
{ name = "regex", specifier = "~=2026.1.15" },
{ name = "textual", specifier = ">=7.5.0" },
{ name = "tiktoken", marker = "extra == 'embeddings'", specifier = "~=0.8.0" },
{ name = "tokenizers", specifier = ">=0.21,<1" },
{ name = "tokenizers", specifier = "~=0.20.3" },
{ name = "tomli", specifier = "~=2.0.2" },
{ name = "tomli-w", specifier = "~=1.1.0" },
{ name = "uv", specifier = "~=0.9.13" },
@@ -2249,11 +2249,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" },
]
[package.optional-dependencies]
requests = [
{ name = "requests" },
]
[[package]]
name = "google-cloud-vision"
version = "3.12.1"
@@ -2272,23 +2267,21 @@ wheels = [
[[package]]
name = "google-genai"
version = "1.65.0"
version = "1.49.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "google-auth", extra = ["requests"] },
{ name = "google-auth" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "sniffio" },
{ name = "tenacity" },
{ name = "typing-extensions" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/f9/cc1191c2540d6a4e24609a586c4ed45d2db57cfef47931c139ee70e5874a/google_genai-1.65.0.tar.gz", hash = "sha256:d470eb600af802d58a79c7f13342d9ea0d05d965007cae8f76c7adff3d7a4750", size = 497206, upload-time = "2026-02-26T00:20:33.824Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/49/1a724ee3c3748fa50721d53a52d9fee88c67d0c43bb16eb2b10ee89ab239/google_genai-1.49.0.tar.gz", hash = "sha256:35eb16023b72e298571ae30e919c810694f258f2ba68fc77a2185c7c8829ad5a", size = 253493, upload-time = "2025-11-05T22:41:03.278Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/3c/3fea4e7c91357c71782d7dcaad7a2577d636c90317e003386893c25bc62c/google_genai-1.65.0-py3-none-any.whl", hash = "sha256:68c025205856919bc03edb0155c11b4b833810b7ce17ad4b7a9eeba5158f6c44", size = 724429, upload-time = "2026-02-26T00:20:32.186Z" },
{ url = "https://files.pythonhosted.org/packages/d5/d3/84a152746dc7bdebb8ba0fd7d6157263044acd1d14b2a53e8df4a307b6b7/google_genai-1.49.0-py3-none-any.whl", hash = "sha256:ad49cd5be5b63397069e7aef9a4fe0a84cbdf25fcd93408e795292308db4ef32", size = 256098, upload-time = "2025-11-05T22:41:01.429Z" },
]
[[package]]
@@ -7650,32 +7643,68 @@ wheels = [
[[package]]
name = "tokenizers"
version = "0.22.2"
version = "0.20.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
]
sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" }
sdist = { url = "https://files.pythonhosted.org/packages/da/25/b1681c1c30ea3ea6e584ae3fffd552430b12faa599b558c4c4783f56d7ff/tokenizers-0.20.3.tar.gz", hash = "sha256:2278b34c5d0dd78e087e1ca7f9b1dcbf129d80211afa645f214bd6e051037539", size = 340513, upload-time = "2024-11-05T17:34:10.403Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
{ url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
{ url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
{ url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
{ url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
{ url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
{ url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
{ url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
{ url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
{ url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
{ url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
{ url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
{ url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
{ url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
{ url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
{ url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" },
{ url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" },
{ url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" },
{ url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" },
{ url = "https://files.pythonhosted.org/packages/c8/51/421bb0052fc4333f7c1e3231d8c6607552933d919b628c8fabd06f60ba1e/tokenizers-0.20.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:31ccab28dbb1a9fe539787210b0026e22debeab1662970f61c2d921f7557f7e4", size = 2674308, upload-time = "2024-11-05T17:30:25.423Z" },
{ url = "https://files.pythonhosted.org/packages/a6/e9/f651f8d27614fd59af387f4dfa568b55207e5fac8d06eec106dc00b921c4/tokenizers-0.20.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6361191f762bda98c773da418cf511cbaa0cb8d0a1196f16f8c0119bde68ff8", size = 2559363, upload-time = "2024-11-05T17:30:28.841Z" },
{ url = "https://files.pythonhosted.org/packages/e3/e8/0e9f81a09ab79f409eabfd99391ca519e315496694671bebca24c3e90448/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f128d5da1202b78fa0a10d8d938610472487da01b57098d48f7e944384362514", size = 2892896, upload-time = "2024-11-05T17:30:30.429Z" },
{ url = "https://files.pythonhosted.org/packages/b0/72/15fdbc149e05005e99431ecd471807db2241983deafe1e704020f608f40e/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79c4121a2e9433ad7ef0769b9ca1f7dd7fa4c0cd501763d0a030afcbc6384481", size = 2802785, upload-time = "2024-11-05T17:30:32.045Z" },
{ url = "https://files.pythonhosted.org/packages/26/44/1f8aea48f9bb117d966b7272484671b33a509f6217a8e8544d79442c90db/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7850fde24197fe5cd6556e2fdba53a6d3bae67c531ea33a3d7c420b90904141", size = 3086060, upload-time = "2024-11-05T17:30:34.11Z" },
{ url = "https://files.pythonhosted.org/packages/2e/83/82ba40da99870b3a0b801cffaf4f099f088a84c7e07d32cc6ca751ce08e6/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b357970c095dc134978a68c67d845a1e3803ab7c4fbb39195bde914e7e13cf8b", size = 3096760, upload-time = "2024-11-05T17:30:36.276Z" },
{ url = "https://files.pythonhosted.org/packages/f3/46/7a025404201d937f86548928616c0a164308aa3998e546efdf798bf5ee9c/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a333d878c4970b72d6c07848b90c05f6b045cf9273fc2bc04a27211721ad6118", size = 3380165, upload-time = "2024-11-05T17:30:37.642Z" },
{ url = "https://files.pythonhosted.org/packages/aa/49/15fae66ac62e49255eeedbb7f4127564b2c3f3aef2009913f525732d1a08/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd9fee817f655a8f50049f685e224828abfadd436b8ff67979fc1d054b435f1", size = 2994038, upload-time = "2024-11-05T17:30:40.075Z" },
{ url = "https://files.pythonhosted.org/packages/f4/64/693afc9ba2393c2eed85c02bacb44762f06a29f0d1a5591fa5b40b39c0a2/tokenizers-0.20.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e7816808b402129393a435ea2a509679b41246175d6e5e9f25b8692bfaa272b", size = 8977285, upload-time = "2024-11-05T17:30:42.095Z" },
{ url = "https://files.pythonhosted.org/packages/be/7e/6126c18694310fe07970717929e889898767c41fbdd95b9078e8aec0f9ef/tokenizers-0.20.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba96367db9d8a730d3a1d5996b4b7babb846c3994b8ef14008cd8660f55db59d", size = 9294890, upload-time = "2024-11-05T17:30:44.563Z" },
{ url = "https://files.pythonhosted.org/packages/71/7d/5e3307a1091c8608a1e58043dff49521bc19553c6e9548c7fac6840cc2c4/tokenizers-0.20.3-cp310-none-win32.whl", hash = "sha256:ee31ba9d7df6a98619426283e80c6359f167e2e9882d9ce1b0254937dbd32f3f", size = 2196883, upload-time = "2024-11-05T17:30:46.792Z" },
{ url = "https://files.pythonhosted.org/packages/47/62/aaf5b2a526b3b10c20985d9568ff8c8f27159345eaef3347831e78cd5894/tokenizers-0.20.3-cp310-none-win_amd64.whl", hash = "sha256:a845c08fdad554fe0871d1255df85772f91236e5fd6b9287ef8b64f5807dbd0c", size = 2381637, upload-time = "2024-11-05T17:30:48.156Z" },
{ url = "https://files.pythonhosted.org/packages/c6/93/6742ef9206409d5ce1fdf44d5ca1687cdc3847ba0485424e2c731e6bcf67/tokenizers-0.20.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:585b51e06ca1f4839ce7759941e66766d7b060dccfdc57c4ca1e5b9a33013a90", size = 2674224, upload-time = "2024-11-05T17:30:49.972Z" },
{ url = "https://files.pythonhosted.org/packages/aa/14/e75ece72e99f6ef9ae07777ca9fdd78608f69466a5cecf636e9bd2f25d5c/tokenizers-0.20.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61cbf11954f3b481d08723ebd048ba4b11e582986f9be74d2c3bdd9293a4538d", size = 2558991, upload-time = "2024-11-05T17:30:51.666Z" },
{ url = "https://files.pythonhosted.org/packages/46/54/033b5b2ba0c3ae01e026c6f7ced147d41a2fa1c573d00a66cb97f6d7f9b3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef820880d5e4e8484e2fa54ff8d297bb32519eaa7815694dc835ace9130a3eea", size = 2892476, upload-time = "2024-11-05T17:30:53.505Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b0/cc369fb3297d61f3311cab523d16d48c869dc2f0ba32985dbf03ff811041/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:67ef4dcb8841a4988cd00dd288fb95dfc8e22ed021f01f37348fd51c2b055ba9", size = 2802775, upload-time = "2024-11-05T17:30:55.229Z" },
{ url = "https://files.pythonhosted.org/packages/1a/74/62ad983e8ea6a63e04ed9c5be0b605056bf8aac2f0125f9b5e0b3e2b89fa/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff1ef8bd47a02b0dc191688ccb4da53600df5d4c9a05a4b68e1e3de4823e78eb", size = 3086138, upload-time = "2024-11-05T17:30:57.332Z" },
{ url = "https://files.pythonhosted.org/packages/6b/ac/4637ba619db25094998523f9e6f5b456e1db1f8faa770a3d925d436db0c3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:444d188186eab3148baf0615b522461b41b1f0cd58cd57b862ec94b6ac9780f1", size = 3098076, upload-time = "2024-11-05T17:30:59.455Z" },
{ url = "https://files.pythonhosted.org/packages/58/ce/9793f2dc2ce529369807c9c74e42722b05034af411d60f5730b720388c7d/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37c04c032c1442740b2c2d925f1857885c07619224a533123ac7ea71ca5713da", size = 3379650, upload-time = "2024-11-05T17:31:01.264Z" },
{ url = "https://files.pythonhosted.org/packages/50/f6/2841de926bc4118af996eaf0bdf0ea5b012245044766ffc0347e6c968e63/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453c7769d22231960ee0e883d1005c93c68015025a5e4ae56275406d94a3c907", size = 2994005, upload-time = "2024-11-05T17:31:02.985Z" },
{ url = "https://files.pythonhosted.org/packages/a3/b2/00915c4fed08e9505d37cf6eaab45b12b4bff8f6719d459abcb9ead86a4b/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4bb31f7b2847e439766aaa9cc7bccf7ac7088052deccdb2275c952d96f691c6a", size = 8977488, upload-time = "2024-11-05T17:31:04.424Z" },
{ url = "https://files.pythonhosted.org/packages/e9/ac/1c069e7808181ff57bcf2d39e9b6fbee9133a55410e6ebdaa89f67c32e83/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:843729bf0f991b29655a069a2ff58a4c24375a553c70955e15e37a90dd4e045c", size = 9294935, upload-time = "2024-11-05T17:31:06.882Z" },
{ url = "https://files.pythonhosted.org/packages/50/47/722feb70ee68d1c4412b12d0ea4acc2713179fd63f054913990f9e259492/tokenizers-0.20.3-cp311-none-win32.whl", hash = "sha256:efcce3a927b1e20ca694ba13f7a68c59b0bd859ef71e441db68ee42cf20c2442", size = 2197175, upload-time = "2024-11-05T17:31:09.385Z" },
{ url = "https://files.pythonhosted.org/packages/75/68/1b4f928b15a36ed278332ac75d66d7eb65d865bf344d049c452c18447bf9/tokenizers-0.20.3-cp311-none-win_amd64.whl", hash = "sha256:88301aa0801f225725b6df5dea3d77c80365ff2362ca7e252583f2b4809c4cc0", size = 2381616, upload-time = "2024-11-05T17:31:10.685Z" },
{ url = "https://files.pythonhosted.org/packages/07/00/92a08af2a6b0c88c50f1ab47d7189e695722ad9714b0ee78ea5e1e2e1def/tokenizers-0.20.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:49d12a32e190fad0e79e5bdb788d05da2f20d8e006b13a70859ac47fecf6ab2f", size = 2667951, upload-time = "2024-11-05T17:31:12.356Z" },
{ url = "https://files.pythonhosted.org/packages/ec/9a/e17a352f0bffbf415cf7d73756f5c73a3219225fc5957bc2f39d52c61684/tokenizers-0.20.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:282848cacfb9c06d5e51489f38ec5aa0b3cd1e247a023061945f71f41d949d73", size = 2555167, upload-time = "2024-11-05T17:31:13.839Z" },
{ url = "https://files.pythonhosted.org/packages/27/37/d108df55daf4f0fcf1f58554692ff71687c273d870a34693066f0847be96/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abe4e08c7d0cd6154c795deb5bf81d2122f36daf075e0c12a8b050d824ef0a64", size = 2898389, upload-time = "2024-11-05T17:31:15.12Z" },
{ url = "https://files.pythonhosted.org/packages/b2/27/32f29da16d28f59472fa7fb38e7782069748c7e9ab9854522db20341624c/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca94fc1b73b3883c98f0c88c77700b13d55b49f1071dfd57df2b06f3ff7afd64", size = 2795866, upload-time = "2024-11-05T17:31:16.857Z" },
{ url = "https://files.pythonhosted.org/packages/29/4e/8a9a3c89e128c4a40f247b501c10279d2d7ade685953407c4d94c8c0f7a7/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef279c7e239f95c8bdd6ff319d9870f30f0d24915b04895f55b1adcf96d6c60d", size = 3085446, upload-time = "2024-11-05T17:31:18.392Z" },
{ url = "https://files.pythonhosted.org/packages/b4/3b/a2a7962c496ebcd95860ca99e423254f760f382cd4bd376f8895783afaf5/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16384073973f6ccbde9852157a4fdfe632bb65208139c9d0c0bd0176a71fd67f", size = 3094378, upload-time = "2024-11-05T17:31:20.329Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f4/a8a33f0192a1629a3bd0afcad17d4d221bbf9276da4b95d226364208d5eb/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:312d522caeb8a1a42ebdec87118d99b22667782b67898a76c963c058a7e41d4f", size = 3385755, upload-time = "2024-11-05T17:31:21.778Z" },
{ url = "https://files.pythonhosted.org/packages/9e/65/c83cb3545a65a9eaa2e13b22c93d5e00bd7624b354a44adbdc93d5d9bd91/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2b7cb962564785a83dafbba0144ecb7f579f1d57d8c406cdaa7f32fe32f18ad", size = 2997679, upload-time = "2024-11-05T17:31:23.134Z" },
{ url = "https://files.pythonhosted.org/packages/55/e9/a80d4e592307688a67c7c59ab77e03687b6a8bd92eb5db763a2c80f93f57/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:124c5882ebb88dadae1fc788a582299fcd3a8bd84fc3e260b9918cf28b8751f5", size = 8989296, upload-time = "2024-11-05T17:31:24.953Z" },
{ url = "https://files.pythonhosted.org/packages/90/af/60c957af8d2244321124e893828f1a4817cde1a2d08d09d423b73f19bd2f/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2b6e54e71f84c4202111a489879005cb14b92616a87417f6c102c833af961ea2", size = 9303621, upload-time = "2024-11-05T17:31:27.341Z" },
{ url = "https://files.pythonhosted.org/packages/be/a9/96172310ee141009646d63a1ca267c099c462d747fe5ef7e33f74e27a683/tokenizers-0.20.3-cp312-none-win32.whl", hash = "sha256:83d9bfbe9af86f2d9df4833c22e94d94750f1d0cd9bfb22a7bb90a86f61cdb1c", size = 2188979, upload-time = "2024-11-05T17:31:29.483Z" },
{ url = "https://files.pythonhosted.org/packages/bd/68/61d85ae7ae96dde7d0974ff3538db75d5cdc29be2e4329cd7fc51a283e22/tokenizers-0.20.3-cp312-none-win_amd64.whl", hash = "sha256:44def74cee574d609a36e17c8914311d1b5dbcfe37c55fd29369d42591b91cf2", size = 2380725, upload-time = "2024-11-05T17:31:31.315Z" },
{ url = "https://files.pythonhosted.org/packages/07/19/36e9eaafb229616cb8502b42030fa7fe347550e76cb618de71b498fc3222/tokenizers-0.20.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0b630e0b536ef0e3c8b42c685c1bc93bd19e98c0f1543db52911f8ede42cf84", size = 2666813, upload-time = "2024-11-05T17:31:32.783Z" },
{ url = "https://files.pythonhosted.org/packages/b9/c7/e2ce1d4f756c8a62ef93fdb4df877c2185339b6d63667b015bf70ea9d34b/tokenizers-0.20.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a02d160d2b19bcbfdf28bd9a4bf11be4cb97d0499c000d95d4c4b1a4312740b6", size = 2555354, upload-time = "2024-11-05T17:31:34.208Z" },
{ url = "https://files.pythonhosted.org/packages/7c/cf/5309c2d173a6a67f9ec8697d8e710ea32418de6fd8541778032c202a1c3e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e3d80d89b068bc30034034b5319218c7c0a91b00af19679833f55f3becb6945", size = 2897745, upload-time = "2024-11-05T17:31:35.733Z" },
{ url = "https://files.pythonhosted.org/packages/2c/e5/af3078e32f225e680e69d61f78855880edb8d53f5850a1834d519b2b103f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:174a54910bed1b089226512b4458ea60d6d6fd93060254734d3bc3540953c51c", size = 2794385, upload-time = "2024-11-05T17:31:37.497Z" },
{ url = "https://files.pythonhosted.org/packages/0b/a7/bc421fe46650cc4eb4a913a236b88c243204f32c7480684d2f138925899e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:098b8a632b8656aa5802c46689462c5c48f02510f24029d71c208ec2c822e771", size = 3084580, upload-time = "2024-11-05T17:31:39.456Z" },
{ url = "https://files.pythonhosted.org/packages/c6/22/97e1e95ee81f75922c9f569c23cb2b1fdc7f5a7a29c4c9fae17e63f751a6/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78c8c143e3ae41e718588281eb3e212c2b31623c9d6d40410ec464d7d6221fb5", size = 3093581, upload-time = "2024-11-05T17:31:41.224Z" },
{ url = "https://files.pythonhosted.org/packages/d5/14/f0df0ee3b9e516121e23c0099bccd7b9f086ba9150021a750e99b16ce56f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b26b0aadb18cd8701077362ba359a06683662d5cafe3e8e8aba10eb05c037f1", size = 3385934, upload-time = "2024-11-05T17:31:43.811Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/7a171bd4929e3ffe61a29b4340fe5b73484709f92a8162a18946e124c34c/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07d7851a72717321022f3774e84aa9d595a041d643fafa2e87fbc9b18711dac0", size = 2997311, upload-time = "2024-11-05T17:31:46.224Z" },
{ url = "https://files.pythonhosted.org/packages/7c/64/f1993bb8ebf775d56875ca0d50a50f2648bfbbb143da92fe2e6ceeb4abd5/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bd44e48a430ada902c6266a8245f5036c4fe744fcb51f699999fbe82aa438797", size = 8988601, upload-time = "2024-11-05T17:31:47.907Z" },
{ url = "https://files.pythonhosted.org/packages/d6/3f/49fa63422159bbc2f2a4ac5bfc597d04d4ec0ad3d2ef46649b5e9a340e37/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a4c186bb006ccbe1f5cc4e0380d1ce7806f5955c244074fd96abc55e27b77f01", size = 9303950, upload-time = "2024-11-05T17:31:50.674Z" },
{ url = "https://files.pythonhosted.org/packages/66/11/79d91aeb2817ad1993ef61c690afe73e6dbedbfb21918b302ef5a2ba9bfb/tokenizers-0.20.3-cp313-none-win32.whl", hash = "sha256:6e19e0f1d854d6ab7ea0c743d06e764d1d9a546932be0a67f33087645f00fe13", size = 2188941, upload-time = "2024-11-05T17:31:53.334Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ff/ac8410f868fb8b14b5e619efa304aa119cb8a40bd7df29fc81a898e64f99/tokenizers-0.20.3-cp313-none-win_amd64.whl", hash = "sha256:d50ede425c7e60966a9680d41b58b3a0950afa1bb570488e2972fa61662c4273", size = 2380269, upload-time = "2024-11-05T17:31:54.796Z" },
{ url = "https://files.pythonhosted.org/packages/29/cd/ff1586dd572aaf1637d59968df3f6f6532fa255f4638fbc29f6d27e0b690/tokenizers-0.20.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e919f2e3e68bb51dc31de4fcbbeff3bdf9c1cad489044c75e2b982a91059bd3c", size = 2672044, upload-time = "2024-11-05T17:33:07.796Z" },
{ url = "https://files.pythonhosted.org/packages/b5/9e/7a2c00abbc8edb021ee0b1f12aab76a7b7824b49f94bcd9f075d0818d4b0/tokenizers-0.20.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b8e9608f2773996cc272156e305bd79066163a66b0390fe21750aff62df1ac07", size = 2558841, upload-time = "2024-11-05T17:33:09.542Z" },
{ url = "https://files.pythonhosted.org/packages/8e/c1/6af62ef61316f33ecf785bbb2bee4292f34ea62b491d4480ad9b09acf6b6/tokenizers-0.20.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39270a7050deaf50f7caff4c532c01b3c48f6608d42b3eacdebdc6795478c8df", size = 2897936, upload-time = "2024-11-05T17:33:11.413Z" },
{ url = "https://files.pythonhosted.org/packages/9a/0b/c076b2ff3ee6dc70c805181fbe325668b89cfee856f8dfa24cc9aa293c84/tokenizers-0.20.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e005466632b1c5d2d2120f6de8aa768cc9d36cd1ab7d51d0c27a114c91a1e6ee", size = 3082688, upload-time = "2024-11-05T17:33:13.538Z" },
{ url = "https://files.pythonhosted.org/packages/0a/60/56510124933136c2e90879e1c81603cfa753ae5a87830e3ef95056b20d8f/tokenizers-0.20.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a07962340b36189b6c8feda552ea1bfeee6cf067ff922a1d7760662c2ee229e5", size = 2998924, upload-time = "2024-11-05T17:33:16.249Z" },
{ url = "https://files.pythonhosted.org/packages/68/60/4107b618b7b9155cb34ad2e0fc90946b7e71f041b642122fb6314f660688/tokenizers-0.20.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:55046ad3dd5f2b3c67501fcc8c9cbe3e901d8355f08a3b745e9b57894855f85b", size = 8989514, upload-time = "2024-11-05T17:33:18.161Z" },
{ url = "https://files.pythonhosted.org/packages/e8/bd/48475818e614b73316baf37ac1e4e51b578bbdf58651812d7e55f43b88d8/tokenizers-0.20.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:efcf0eb939988b627558aaf2b9dc3e56d759cad2e0cfa04fcab378e4b48fc4fd", size = 9303476, upload-time = "2024-11-05T17:33:21.251Z" },
]
[[package]]
@@ -7829,7 +7858,7 @@ wheels = [
[[package]]
name = "transformers"
version = "4.57.6"
version = "4.46.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -7843,9 +7872,9 @@ dependencies = [
{ name = "tokenizers" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" }
sdist = { url = "https://files.pythonhosted.org/packages/37/5a/58f96c83e566f907ae39f16d4401bbefd8bb85c60bd1e6a95c419752ab90/transformers-4.46.3.tar.gz", hash = "sha256:8ee4b3ae943fe33e82afff8e837f4b052058b07ca9be3cb5b729ed31295f72cc", size = 8627944, upload-time = "2024-11-18T22:13:01.012Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" },
{ url = "https://files.pythonhosted.org/packages/51/51/b87caa939fedf307496e4dbf412f4b909af3d9ca8b189fc3b65c1faa456f/transformers-4.46.3-py3-none-any.whl", hash = "sha256:a12ef6f52841fd190a3e5602145b542d03507222f2c64ebb7ee92e8788093aef", size = 10034536, upload-time = "2024-11-18T22:12:57.024Z" },
]
[[package]]