Fix Pydantic validation error in LLMCallStartedEvent when TokenCalcHandler in tools list

- Add model_validator to sanitize tools list before validation
- Filter out non-dict objects like TokenCalcHandler from tools list
- Preserve dict tools while removing problematic objects
- Add comprehensive test coverage for the fix and edge cases
- Resolves issue #3043

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2025-06-21 16:33:22 +00:00
parent 59032817c7
commit b2bda39e56
2 changed files with 145 additions and 1 deletions

View File

@@ -1,7 +1,7 @@
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel
from pydantic import BaseModel, model_validator
from crewai.utilities.events.base_events import BaseEvent
@@ -27,6 +27,15 @@ class LLMCallStartedEvent(BaseEvent):
callbacks: Optional[List[Any]] = None
available_functions: Optional[Dict[str, Any]] = None
@model_validator(mode='before')
@classmethod
def sanitize_tools(cls, values):
"""Sanitize tools list to only include dict objects, filtering out non-dict objects like TokenCalcHandler"""
if isinstance(values, dict) and 'tools' in values and values['tools'] is not None:
if isinstance(values['tools'], list):
values['tools'] = [tool for tool in values['tools'] if isinstance(tool, dict)]
return values
class LLMCallCompletedEvent(BaseEvent):
"""Event emitted when a LLM call completes"""

View File

@@ -0,0 +1,135 @@
import pytest
from crewai.utilities.events.llm_events import LLMCallStartedEvent
from crewai.utilities.token_counter_callback import TokenCalcHandler
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
class TestLLMCallStartedEventValidation:
"""Test cases for LLMCallStartedEvent validation and sanitization"""
def test_normal_dict_tools_work(self):
"""Test that normal dict tools work correctly"""
event = LLMCallStartedEvent(
messages=[{"role": "user", "content": "test message"}],
tools=[{"name": "tool1"}, {"name": "tool2"}],
callbacks=None
)
assert event.tools == [{"name": "tool1"}, {"name": "tool2"}]
assert event.type == "llm_call_started"
def test_token_calc_handler_in_tools_filtered_out(self):
"""Test that TokenCalcHandler objects in tools list are filtered out"""
token_handler = TokenCalcHandler(TokenProcess())
event = LLMCallStartedEvent(
messages=[{"role": "user", "content": "test message"}],
tools=[{"name": "tool1"}, token_handler, {"name": "tool2"}],
callbacks=None
)
assert event.tools == [{"name": "tool1"}, {"name": "tool2"}]
assert len(event.tools) == 2
def test_mixed_objects_in_tools_only_dicts_preserved(self):
"""Test that only dict objects are preserved when mixed types are in tools"""
token_handler = TokenCalcHandler(TokenProcess())
event = LLMCallStartedEvent(
messages=[{"role": "user", "content": "test message"}],
tools=[
{"name": "tool1"},
token_handler,
"string_tool",
{"name": "tool2"},
123,
{"name": "tool3"}
],
callbacks=None
)
assert event.tools == [{"name": "tool1"}, {"name": "tool2"}, {"name": "tool3"}]
assert len(event.tools) == 3
def test_empty_tools_list_handled(self):
"""Test that empty tools list is handled correctly"""
event = LLMCallStartedEvent(
messages=[{"role": "user", "content": "test message"}],
tools=[],
callbacks=None
)
assert event.tools == []
def test_none_tools_handled(self):
"""Test that None tools value is handled correctly"""
event = LLMCallStartedEvent(
messages=[{"role": "user", "content": "test message"}],
tools=None,
callbacks=None
)
assert event.tools is None
def test_all_non_dict_tools_results_in_empty_list(self):
"""Test that when all tools are non-dict objects, result is empty list"""
token_handler = TokenCalcHandler(TokenProcess())
event = LLMCallStartedEvent(
messages=[{"role": "user", "content": "test message"}],
tools=[token_handler, "string_tool", 123, ["list_tool"]],
callbacks=None
)
assert event.tools == []
def test_reproduction_case_from_issue_3043(self):
"""Test the exact reproduction case from GitHub issue #3043"""
token_handler = TokenCalcHandler(TokenProcess())
event = LLMCallStartedEvent(
messages=[{"role": "user", "content": "test message"}],
tools=[{"name": "tool1"}, token_handler],
callbacks=None
)
assert event.tools == [{"name": "tool1"}]
assert len(event.tools) == 1
def test_callbacks_with_token_handler_still_work(self):
"""Test that TokenCalcHandler in callbacks still works normally"""
token_handler = TokenCalcHandler(TokenProcess())
event = LLMCallStartedEvent(
messages=[{"role": "user", "content": "test message"}],
tools=[{"name": "tool1"}],
callbacks=[token_handler]
)
assert event.tools == [{"name": "tool1"}]
assert event.callbacks == [token_handler]
def test_string_messages_work(self):
"""Test that string messages work with tool sanitization"""
token_handler = TokenCalcHandler(TokenProcess())
event = LLMCallStartedEvent(
messages="test message",
tools=[{"name": "tool1"}, token_handler],
callbacks=None
)
assert event.messages == "test message"
assert event.tools == [{"name": "tool1"}]
def test_available_functions_preserved(self):
"""Test that available_functions are preserved during sanitization"""
token_handler = TokenCalcHandler(TokenProcess())
available_funcs = {"func1": lambda x: x}
event = LLMCallStartedEvent(
messages=[{"role": "user", "content": "test message"}],
tools=[{"name": "tool1"}, token_handler],
callbacks=None,
available_functions=available_funcs
)
assert event.tools == [{"name": "tool1"}]
assert event.available_functions == available_funcs