Compare commits

...

6 Commits

Author SHA1 Message Date
Devin AI
420ed828b7 fix: capture tool name/input before _handle_agent_action in ReAct loops
_handle_agent_action can convert AgentAction to AgentFinish (when
result_as_answer=True). Accessing .tool and .tool_input on AgentFinish
would raise AttributeError. Now we capture the values beforehand.

Co-Authored-By: João <joao@crewai.com>
2026-03-03 03:36:08 +00:00
Devin AI
52d36ccaf9 feat: add agent loop detection middleware (#4682)
- Add LoopDetector class with sliding window tracking, configurable
  repetition threshold, and intervention strategies (inject_reflection,
  stop, or custom callback)
- Add LoopDetectedEvent for observability via the event bus
- Integrate loop detection into all 4 executor loop methods (sync/async
  ReAct and native tools)
- Add loop_detector field to BaseAgent
- Export LoopDetector from crewai public API
- Add i18n support for loop detection warning message
- Add 50 comprehensive unit tests

Co-Authored-By: João <joao@crewai.com>
2026-03-03 03:33:33 +00:00
Greyson LaLonde
7972192d55 fix(deps): bump tokenizers lower bound to >=0.21 to avoid broken 0.20.3
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
2026-03-02 18:04:28 -05:00
Mike Plachta
b3f8a42321 feat: upgrade gemini genai
Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com>
Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com>
2026-03-02 14:27:56 -05:00
Greyson LaLonde
21224f2bc5 fix: conditionally pass plus header
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Empty strings are considered illegal values for bearer auth in `httpx`.
2026-03-02 09:27:54 -05:00
Giulio Leone
b76022c1e7 fix(telemetry): skip signal handler registration in non-main threads
* fix(telemetry): skip signal handler registration in non-main threads

When CrewAI is initialized from a non-main thread (e.g. Streamlit, Flask,
Django, Jupyter), the telemetry module attempted to register signal handlers
which only work in the main thread. This caused multiple noisy ValueError
tracebacks to be printed to stderr, confusing users even though the errors
were caught and non-fatal.

Check `threading.current_thread() is not threading.main_thread()` before
attempting signal registration, and skip silently with a debug-level log
message instead of printing full tracebacks.

Fixes crewAIInc/crewAI#4289

* fix(test): move Telemetry() inside signal.signal mock context

Refs: #4649

* fix(telemetry): move signal.signal mock inside thread to wrap Telemetry() construction

The patch context now activates inside init_in_thread so the mock
is guaranteed to be active before and during Telemetry.__init__,
addressing the Copilot review feedback.

Refs: #4289

* fix(test): mock logger.debug instead of capsys for deterministic assertion

Replace signal.signal-only mock with combined logger + signal mock.
Assert logger.debug was called with the skip message and signal.signal
was never invoked from the non-main thread.

Refs: #4289
2026-03-02 07:42:55 -05:00
13 changed files with 1015 additions and 71 deletions

View File

@@ -21,7 +21,7 @@ dependencies = [
"opentelemetry-exporter-otlp-proto-http~=1.34.0",
# Data Handling
"chromadb~=1.1.0",
"tokenizers~=0.20.3",
"tokenizers>=0.21,<1",
"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.49.0",
"google-genai~=1.65.0",
]
azure-ai-inference = [
"azure-ai-inference~=1.0.0b9",

View File

@@ -4,6 +4,7 @@ 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
@@ -99,6 +100,7 @@ __all__ = [
"Flow",
"Knowledge",
"LLMGuardrail",
"LoopDetector",
"Memory",
"Process",
"Task",

View File

@@ -22,6 +22,7 @@ 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
@@ -213,6 +214,15 @@ 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,6 +17,7 @@ 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,
@@ -156,6 +157,11 @@ 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())
@@ -410,6 +416,11 @@ 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,
@@ -427,6 +438,14 @@ 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]
@@ -783,6 +802,14 @@ 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",
@@ -807,6 +834,11 @@ 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",
@@ -1068,6 +1100,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
"result": result,
"from_cache": from_cache,
"original_tool": original_tool,
"tool_args": func_args,
}
def _append_tool_result_and_check_finality(
@@ -1246,6 +1279,11 @@ 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,
@@ -1263,6 +1301,14 @@ 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]
@@ -1462,6 +1508,94 @@ 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

@@ -0,0 +1,210 @@
"""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,14 +22,15 @@ 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:
def __init__(self, api_key: str | None = None) -> 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(api_key="")
self.plus_api = PlusAPI()
self.ephemeral_trace_url = None
def initialize_batch(

View File

@@ -0,0 +1,48 @@
"""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,6 +173,12 @@ 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,6 +15,7 @@
"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

@@ -0,0 +1,523 @@
"""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,3 +121,41 @@ 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.49.0" },
{ name = "google-genai", marker = "extra == 'google-genai'", specifier = "~=1.65.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.20.3" },
{ name = "tokenizers", specifier = ">=0.21,<1" },
{ name = "tomli", specifier = "~=2.0.2" },
{ name = "tomli-w", specifier = "~=1.1.0" },
{ name = "uv", specifier = "~=0.9.13" },
@@ -2249,6 +2249,11 @@ 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"
@@ -2267,21 +2272,23 @@ wheels = [
[[package]]
name = "google-genai"
version = "1.49.0"
version = "1.65.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "google-auth" },
{ name = "distro" },
{ name = "google-auth", extra = ["requests"] },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "sniffio" },
{ name = "tenacity" },
{ name = "typing-extensions" },
{ name = "websockets" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -7643,68 +7650,32 @@ wheels = [
[[package]]
name = "tokenizers"
version = "0.20.3"
version = "0.22.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -7858,7 +7829,7 @@ wheels = [
[[package]]
name = "transformers"
version = "4.46.3"
version = "4.57.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -7872,9 +7843,9 @@ dependencies = [
{ name = "tokenizers" },
{ name = "tqdm" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]