Files
crewAI/lib/crewai/tests/tracing/test_trace_serialization.py
Alex b40098b28e fix: address PR review comments on trace serialization
1. Remove unused import Mock from test_trace_serialization.py
2. Remove unused import safe_serialize_to_dict from test_trace_serialization.py
3. Fix LLM event tools data being silently excluded: Remove 'tools' from
   TRACE_EXCLUDE_FIELDS since LLMCallStartedEvent.tools is a lightweight
   list of tool schemas, not heavy Agent.tools objects. Agent.tools is
   already excluded explicitly in _build_crew_started_data.
4. Remove dead code: complex_events class variable from TraceCollectionListener

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-30 11:00:28 -07:00

587 lines
21 KiB
Python

"""Tests for trace serialization optimization to prevent trace table bloat.
These tests verify that trace events don't contain redundant full crew/task/agent
objects, reducing event sizes from 50-100KB to a few KB per event.
"""
import uuid
from unittest.mock import MagicMock
import pytest
from crewai.events.listeners.tracing.trace_listener import (
TRACE_EXCLUDE_FIELDS,
TraceCollectionListener,
_serialize_for_trace,
)
class TestTraceExcludeFields:
"""Test that TRACE_EXCLUDE_FIELDS contains all the heavy/redundant fields."""
def test_contains_back_references(self):
"""Verify back-reference fields are excluded."""
back_refs = {"crew", "agent", "agents", "tasks", "context"}
assert back_refs.issubset(TRACE_EXCLUDE_FIELDS)
def test_contains_heavy_fields(self):
"""Verify heavy objects are excluded.
Note: 'tools' is NOT in TRACE_EXCLUDE_FIELDS because LLMCallStartedEvent.tools
is a lightweight list of tool schemas. Agent.tools exclusion is handled
explicitly in _build_crew_started_data.
"""
heavy_fields = {
"llm",
"function_calling_llm",
"step_callback",
"task_callback",
"crew_callback",
"callbacks",
"_memory",
"_cache",
"knowledge_sources",
}
assert heavy_fields.issubset(TRACE_EXCLUDE_FIELDS)
# tools is NOT excluded globally - LLM events need it
assert "tools" not in TRACE_EXCLUDE_FIELDS
class TestSerializeForTrace:
"""Test the _serialize_for_trace helper function."""
def test_excludes_crew_field(self):
"""Verify crew field is excluded from serialization."""
event = MagicMock()
event.crew = MagicMock(name="TestCrew")
event.crew_name = "TestCrew"
event.timestamp = None
result = _serialize_for_trace(event)
# crew_name should be present (scalar field)
# crew should be excluded (back-reference)
assert "crew" not in result or result.get("crew") is None
def test_excludes_agent_field(self):
"""Verify agent field is excluded from serialization."""
event = MagicMock()
event.agent = MagicMock(role="TestAgent")
event.agent_role = "TestAgent"
result = _serialize_for_trace(event)
assert "agent" not in result or result.get("agent") is None
def test_preserves_tools_field(self):
"""Verify tools field is preserved for LLM events (lightweight schemas)."""
class EventWithTools:
def __init__(self):
self.tools = [{"name": "search", "description": "Search tool"}]
self.tool_name = "test_tool"
event = EventWithTools()
result = _serialize_for_trace(event)
# tools should be preserved (lightweight for LLM events)
assert "tools" in result
assert result["tools"] == [{"name": "search", "description": "Search tool"}]
def test_preserves_scalar_fields(self):
"""Verify scalar fields needed by AMP frontend are preserved."""
class SimpleEvent:
def __init__(self):
self.agent_role = "Researcher"
self.task_name = "Research Task"
self.task_id = str(uuid.uuid4())
self.duration_ms = 1500
self.tokens_used = 500
event = SimpleEvent()
result = _serialize_for_trace(event)
# Scalar fields should be preserved
assert result.get("agent_role") == "Researcher"
assert result.get("task_name") == "Research Task"
assert result.get("duration_ms") == 1500
assert result.get("tokens_used") == 500
def test_extra_exclude_parameter(self):
"""Verify extra_exclude adds to the default exclusions."""
class EventWithCustomField:
def __init__(self):
self.custom_heavy_field = {"large": "data" * 1000}
self.keep_this = "small"
event = EventWithCustomField()
result = _serialize_for_trace(event, extra_exclude={"custom_heavy_field"})
assert "custom_heavy_field" not in result
assert result.get("keep_this") == "small"
class TestBuildEventData:
"""Test _build_event_data method for different event types."""
@pytest.fixture
def listener(self):
"""Create a trace listener for testing."""
# Reset singleton
TraceCollectionListener._instance = None
TraceCollectionListener._initialized = False
TraceCollectionListener._listeners_setup = False
return TraceCollectionListener()
def test_task_started_no_full_task_object(self, listener):
"""Verify task_started event doesn't include full task object."""
mock_task = MagicMock()
mock_task.name = "Test Task"
mock_task.description = "A test task description"
mock_task.expected_output = "Expected result"
mock_task.id = uuid.uuid4()
# Add heavy fields that should NOT appear in output
mock_task.crew = MagicMock(name="HeavyCrew")
mock_task.agent = MagicMock(role="HeavyAgent")
mock_task.context = [MagicMock(), MagicMock()]
mock_task.tools = [MagicMock(), MagicMock()]
mock_event = MagicMock()
mock_event.task = mock_task
mock_event.context = "test context"
mock_source = MagicMock()
mock_source.agent = MagicMock()
mock_source.agent.role = "Worker"
result = listener._build_event_data("task_started", mock_event, mock_source)
# Should have scalar fields
assert result["task_name"] == "Test Task"
assert result["task_description"] == "A test task description"
assert result["agent_role"] == "Worker"
assert result["task_id"] == str(mock_task.id)
# Should NOT have full objects
assert "crew" not in result
assert "tools" not in result
# task and agent should not be full objects
assert result.get("task") is None or not hasattr(result.get("task"), "crew")
def test_task_completed_no_full_task_object(self, listener):
"""Verify task_completed event doesn't include full task object."""
mock_task = MagicMock()
mock_task.name = "Completed Task"
mock_task.description = "Task description"
mock_task.id = uuid.uuid4()
mock_output = MagicMock()
mock_output.raw = "Task result"
mock_output.output_format = "text"
mock_output.agent = "Worker"
mock_event = MagicMock()
mock_event.task = mock_task
mock_event.output = mock_output
result = listener._build_event_data("task_completed", mock_event, None)
# Should have scalar fields
assert result["task_name"] == "Completed Task"
assert result["output_raw"] == "Task result"
assert result["agent_role"] == "Worker"
# Should NOT have full task object
assert "crew" not in result
assert "tools" not in result
def test_agent_execution_started_no_full_agent(self, listener):
"""Verify agent_execution_started extracts only scalar fields."""
mock_agent = MagicMock()
mock_agent.role = "Analyst"
mock_agent.goal = "Analyze data"
mock_agent.backstory = "Expert analyst"
# Heavy fields
mock_agent.tools = [MagicMock(), MagicMock()]
mock_agent.llm = MagicMock()
mock_agent.crew = MagicMock()
mock_event = MagicMock()
mock_event.agent = mock_agent
result = listener._build_event_data(
"agent_execution_started", mock_event, None
)
# Should have scalar fields
assert result["agent_role"] == "Analyst"
assert result["agent_goal"] == "Analyze data"
assert result["agent_backstory"] == "Expert analyst"
# Should NOT have heavy objects
assert "tools" not in result
assert "llm" not in result
assert "crew" not in result
def test_llm_call_started_excludes_heavy_fields(self, listener):
"""Verify llm_call_started uses lightweight serialization.
LLMCallStartedEvent.tools is a lightweight list of tool schemas (dicts),
not heavy Agent.tools objects, so it should be preserved.
"""
class MockLLMEvent:
def __init__(self):
self.task_name = "LLM Task"
self.model = "gpt-4"
self.tokens = 100
# Heavy fields that should be excluded
self.crew = MagicMock()
self.agent = MagicMock()
# LLM event tools are lightweight schemas (dicts), should be kept
self.tools = [{"name": "search", "description": "Search tool"}]
mock_event = MockLLMEvent()
result = listener._build_event_data("llm_call_started", mock_event, None)
# task_name should be present
assert result["task_name"] == "LLM Task"
# Heavy fields should be excluded
assert "crew" not in result or result.get("crew") is None
assert "agent" not in result or result.get("agent") is None
# LLM tools (lightweight schemas) should be preserved
assert result.get("tools") == [{"name": "search", "description": "Search tool"}]
def test_llm_call_completed_excludes_heavy_fields(self, listener):
"""Verify llm_call_completed uses lightweight serialization."""
class MockLLMCompletedEvent:
def __init__(self):
self.response = "LLM response"
self.tokens_used = 150
self.duration_ms = 500
# Heavy fields
self.crew = MagicMock()
self.agent = MagicMock()
mock_event = MockLLMCompletedEvent()
result = listener._build_event_data("llm_call_completed", mock_event, None)
# Scalar fields preserved
assert result.get("response") == "LLM response"
assert result.get("tokens_used") == 150
# Heavy fields excluded
assert "crew" not in result or result.get("crew") is None
assert "agent" not in result or result.get("agent") is None
class TestCrewKickoffStartedEvent:
"""Test that crew_kickoff_started event has full structure."""
@pytest.fixture
def listener(self):
"""Create a trace listener for testing."""
TraceCollectionListener._instance = None
TraceCollectionListener._initialized = False
TraceCollectionListener._listeners_setup = False
return TraceCollectionListener()
def test_crew_started_has_crew_structure(self, listener):
"""Verify crew_kickoff_started includes the crew_structure field."""
# Create mock crew with agents and tasks
mock_agent1 = MagicMock()
mock_agent1.id = uuid.uuid4()
mock_agent1.role = "Researcher"
mock_agent1.goal = "Research things"
mock_agent1.backstory = "Expert researcher"
mock_agent1.verbose = True
mock_agent1.allow_delegation = False
mock_agent1.max_iter = 10
mock_agent1.max_rpm = None
mock_agent1.tools = [MagicMock(name="search_tool"), MagicMock(name="read_tool")]
mock_agent2 = MagicMock()
mock_agent2.id = uuid.uuid4()
mock_agent2.role = "Writer"
mock_agent2.goal = "Write content"
mock_agent2.backstory = "Expert writer"
mock_agent2.verbose = False
mock_agent2.allow_delegation = True
mock_agent2.max_iter = 5
mock_agent2.max_rpm = 10
mock_agent2.tools = []
mock_task1 = MagicMock()
mock_task1.id = uuid.uuid4()
mock_task1.name = "Research Task"
mock_task1.description = "Do research"
mock_task1.expected_output = "Research results"
mock_task1.async_execution = False
mock_task1.human_input = False
mock_task1.agent = mock_agent1
mock_task1.context = None
mock_task2 = MagicMock()
mock_task2.id = uuid.uuid4()
mock_task2.name = "Writing Task"
mock_task2.description = "Write report"
mock_task2.expected_output = "Written report"
mock_task2.async_execution = True
mock_task2.human_input = True
mock_task2.agent = mock_agent2
mock_task2.context = [mock_task1]
mock_crew = MagicMock()
mock_crew.agents = [mock_agent1, mock_agent2]
mock_crew.tasks = [mock_task1, mock_task2]
mock_crew.process = "sequential"
mock_crew.verbose = True
mock_crew.memory = False
mock_event = MagicMock()
mock_event.crew = mock_crew
mock_event.crew_name = "TestCrew"
mock_event.inputs = {"key": "value"}
result = listener._build_event_data("crew_kickoff_started", mock_event, None)
# Should have crew_structure
assert "crew_structure" in result
crew_structure = result["crew_structure"]
# Verify agents are serialized with tool names
assert len(crew_structure["agents"]) == 2
agent1_data = crew_structure["agents"][0]
assert agent1_data["role"] == "Researcher"
assert agent1_data["goal"] == "Research things"
assert "tool_names" in agent1_data
assert len(agent1_data["tool_names"]) == 2
# Verify tasks have lightweight agent references
assert len(crew_structure["tasks"]) == 2
task2_data = crew_structure["tasks"][1]
assert task2_data["name"] == "Writing Task"
assert "agent_ref" in task2_data
assert task2_data["agent_ref"]["role"] == "Writer"
# Verify context uses task IDs
assert "context_task_ids" in task2_data
assert str(mock_task1.id) in task2_data["context_task_ids"]
def test_crew_started_agents_no_full_tools(self, listener):
"""Verify agents in crew_structure have tool_names, not full tool objects."""
mock_tool = MagicMock()
mock_tool.name = "web_search"
mock_tool.description = "Search the web"
mock_tool.func = lambda x: x # Heavy callable
mock_tool.args_schema = {"type": "object"} # Schema
mock_agent = MagicMock()
mock_agent.id = uuid.uuid4()
mock_agent.role = "Searcher"
mock_agent.goal = "Search"
mock_agent.backstory = "Expert"
mock_agent.verbose = False
mock_agent.allow_delegation = False
mock_agent.max_iter = 5
mock_agent.max_rpm = None
mock_agent.tools = [mock_tool]
mock_crew = MagicMock()
mock_crew.agents = [mock_agent]
mock_crew.tasks = []
mock_crew.process = "sequential"
mock_crew.verbose = False
mock_crew.memory = False
mock_event = MagicMock()
mock_event.crew = mock_crew
result = listener._build_event_data("crew_kickoff_started", mock_event, None)
agent_data = result["crew_structure"]["agents"][0]
# Should have tool_names (list of strings)
assert "tool_names" in agent_data
assert agent_data["tool_names"] == ["web_search"]
# Should NOT have full tools array
assert "tools" not in agent_data
def test_crew_started_tasks_no_full_agent(self, listener):
"""Verify tasks have agent_ref, not full agent object."""
mock_agent = MagicMock()
mock_agent.id = uuid.uuid4()
mock_agent.role = "Worker"
mock_agent.goal = "Work hard"
mock_agent.backstory = "Dedicated worker"
mock_agent.tools = [MagicMock(), MagicMock()]
mock_agent.llm = MagicMock()
mock_task = MagicMock()
mock_task.id = uuid.uuid4()
mock_task.name = "Work Task"
mock_task.description = "Do work"
mock_task.expected_output = "Work done"
mock_task.async_execution = False
mock_task.human_input = False
mock_task.agent = mock_agent
mock_task.context = None
mock_crew = MagicMock()
mock_crew.agents = [mock_agent]
mock_crew.tasks = [mock_task]
mock_crew.process = "sequential"
mock_crew.verbose = False
mock_crew.memory = False
mock_event = MagicMock()
mock_event.crew = mock_crew
result = listener._build_event_data("crew_kickoff_started", mock_event, None)
task_data = result["crew_structure"]["tasks"][0]
# Should have lightweight agent_ref
assert "agent_ref" in task_data
assert task_data["agent_ref"]["id"] == str(mock_agent.id)
assert task_data["agent_ref"]["role"] == "Worker"
# agent_ref should ONLY have id and role (not tools, llm, etc.)
assert len(task_data["agent_ref"]) == 2
# Should NOT have full agent
assert "agent" not in task_data
class TestNonCrewStartedEvents:
"""Test that non-crew_started events don't have redundant data."""
@pytest.fixture
def listener(self):
"""Create a trace listener for testing."""
TraceCollectionListener._instance = None
TraceCollectionListener._initialized = False
TraceCollectionListener._listeners_setup = False
return TraceCollectionListener()
def test_generic_event_no_crew(self, listener):
"""Verify generic events exclude crew object.
Note: 'tools' is now preserved since LLMCallStartedEvent.tools is lightweight.
"""
class GenericEvent:
def __init__(self):
self.event_type = "some_event"
self.data = "some_data"
# These should be excluded
self.crew = MagicMock()
self.agents = [MagicMock()]
self.tasks = [MagicMock()]
# tools is now preserved (for LLM events it's lightweight)
self.tools = [{"name": "search"}]
mock_event = GenericEvent()
result = listener._build_event_data("some_event", mock_event, None)
# Scalar fields preserved
assert result.get("event_type") == "some_event"
assert result.get("data") == "some_data"
# Heavy fields excluded
assert "crew" not in result or result.get("crew") is None
assert "agents" not in result or result.get("agents") is None
assert "tasks" not in result or result.get("tasks") is None
# tools is now preserved (lightweight for LLM events)
assert result.get("tools") == [{"name": "search"}]
def test_crew_kickoff_completed_no_full_crew(self, listener):
"""Verify crew_kickoff_completed doesn't repeat full crew structure."""
class CrewCompletedEvent:
def __init__(self):
self.crew_name = "TestCrew"
self.total_tokens = 5000
self.output = "Final output"
# Should be excluded
self.crew = MagicMock()
self.crew.agents = [MagicMock(), MagicMock()]
self.crew.tasks = [MagicMock()]
mock_event = CrewCompletedEvent()
result = listener._build_event_data("crew_kickoff_completed", mock_event, None)
# Scalar fields preserved
assert result.get("crew_name") == "TestCrew"
assert result.get("total_tokens") == 5000
# Should NOT have full crew object
assert "crew" not in result or result.get("crew") is None
# Should NOT have crew_structure (that's only for crew_started)
assert "crew_structure" not in result
class TestSizeReduction:
"""Test that the optimization actually reduces serialized size."""
@pytest.fixture
def listener(self):
"""Create a trace listener for testing."""
TraceCollectionListener._instance = None
TraceCollectionListener._initialized = False
TraceCollectionListener._listeners_setup = False
return TraceCollectionListener()
def test_task_event_size_reduction(self, listener):
"""Verify task events are much smaller than naive serialization."""
import json
# Create a realistic task with many fields
mock_agent = MagicMock()
mock_agent.id = uuid.uuid4()
mock_agent.role = "Researcher"
mock_agent.goal = "Research" * 50 # Longer goal
mock_agent.backstory = "Expert" * 100 # Longer backstory
mock_agent.tools = [MagicMock() for _ in range(5)]
mock_agent.llm = MagicMock()
mock_agent.crew = MagicMock()
mock_task = MagicMock()
mock_task.name = "Research Task"
mock_task.description = "Detailed description" * 20
mock_task.expected_output = "Expected" * 10
mock_task.id = uuid.uuid4()
mock_task.agent = mock_agent
mock_task.context = [MagicMock() for _ in range(3)]
mock_task.crew = MagicMock()
mock_task.tools = [MagicMock() for _ in range(3)]
mock_event = MagicMock()
mock_event.task = mock_task
mock_event.context = "test context"
mock_source = MagicMock()
mock_source.agent = mock_agent
result = listener._build_event_data("task_started", mock_event, mock_source)
# The result should be relatively small
serialized = json.dumps(result, default=str)
# Should be under 2KB for task_started (was potentially 50-100KB before)
assert len(serialized) < 2000, f"task_started too large: {len(serialized)} bytes"
# Should have the essential fields
assert "task_name" in result
assert "task_id" in result
assert "agent_role" in result