mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-16 04:18:35 +00:00
Add compact mode and context management features to address issue #3912
- Add 5 new Agent configuration fields: - compact_mode: Enable compact prompt mode to reduce context size - tools_prompt_strategy: Choose between 'full' or 'names_only' for tools - proactive_context_trimming: Enable proactive message trimming - memory_max_chars: Cap memory context length - knowledge_max_chars: Cap knowledge context length - Implement compact prompt mode in utilities/prompts.py - Caps role to 100 chars, goal to 150 chars - Omits backstory entirely in compact mode - Implement tools_prompt_strategy in Agent.create_agent_executor - 'names_only' uses get_tool_names for minimal tool descriptions - 'full' uses render_text_description_and_args (default) - Implement memory/knowledge size bounds in Agent.execute_task - Truncates memory and knowledge contexts when limits are set - Add trim_messages_structurally helper in agent_utils.py - Structural trimming without LLM calls - Keeps system messages and last N message pairs - Integrate proactive trimming in CrewAgentExecutor._invoke_loop - Trims messages before each LLM call when enabled - Update LangGraphAdapter and OpenAIAdapter to honor compact_mode - Compacts role/goal/backstory in system prompts - Add comprehensive tests for all new features: - test_prompts_compact_mode.py - test_memory_knowledge_truncation.py - test_tools_prompt_strategy.py - test_proactive_context_trimming.py All changes are opt-in with conservative defaults to maintain backward compatibility. Fixes #3912 Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
@@ -213,6 +213,26 @@ class Agent(BaseAgent):
|
|||||||
default=None,
|
default=None,
|
||||||
description="A2A (Agent-to-Agent) configuration for delegating tasks to remote agents. Can be a single A2AConfig or a dict mapping agent IDs to configs.",
|
description="A2A (Agent-to-Agent) configuration for delegating tasks to remote agents. Can be a single A2AConfig or a dict mapping agent IDs to configs.",
|
||||||
)
|
)
|
||||||
|
compact_mode: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Enable compact prompt mode to reduce context size by shortening role, goal, and backstory in prompts.",
|
||||||
|
)
|
||||||
|
tools_prompt_strategy: Literal["full", "names_only"] = Field(
|
||||||
|
default="full",
|
||||||
|
description="Strategy for including tools in prompts: 'full' includes complete descriptions, 'names_only' includes only tool names.",
|
||||||
|
)
|
||||||
|
proactive_context_trimming: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Enable proactive trimming of conversation history before each LLM call to prevent context overflow.",
|
||||||
|
)
|
||||||
|
memory_max_chars: int | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Maximum character length for memory context. If set, memory content will be truncated to this length.",
|
||||||
|
)
|
||||||
|
knowledge_max_chars: int | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Maximum character length for knowledge context. If set, knowledge content will be truncated to this length.",
|
||||||
|
)
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
def validate_from_repository(cls, v: Any) -> dict[str, Any] | None | Any: # noqa: N805
|
def validate_from_repository(cls, v: Any) -> dict[str, Any] | None | Any: # noqa: N805
|
||||||
@@ -366,6 +386,8 @@ class Agent(BaseAgent):
|
|||||||
)
|
)
|
||||||
memory = contextual_memory.build_context_for_task(task, context or "")
|
memory = contextual_memory.build_context_for_task(task, context or "")
|
||||||
if memory.strip() != "":
|
if memory.strip() != "":
|
||||||
|
if self.memory_max_chars and len(memory) > self.memory_max_chars:
|
||||||
|
memory = memory[:self.memory_max_chars] + "..."
|
||||||
task_prompt += self.i18n.slice("memory").format(memory=memory)
|
task_prompt += self.i18n.slice("memory").format(memory=memory)
|
||||||
|
|
||||||
crewai_event_bus.emit(
|
crewai_event_bus.emit(
|
||||||
@@ -406,6 +428,8 @@ class Agent(BaseAgent):
|
|||||||
agent_knowledge_snippets
|
agent_knowledge_snippets
|
||||||
)
|
)
|
||||||
if self.agent_knowledge_context:
|
if self.agent_knowledge_context:
|
||||||
|
if self.knowledge_max_chars and len(self.agent_knowledge_context) > self.knowledge_max_chars:
|
||||||
|
self.agent_knowledge_context = self.agent_knowledge_context[:self.knowledge_max_chars] + "..."
|
||||||
task_prompt += self.agent_knowledge_context
|
task_prompt += self.agent_knowledge_context
|
||||||
|
|
||||||
# Quering crew specific knowledge
|
# Quering crew specific knowledge
|
||||||
@@ -417,6 +441,8 @@ class Agent(BaseAgent):
|
|||||||
knowledge_snippets
|
knowledge_snippets
|
||||||
)
|
)
|
||||||
if self.crew_knowledge_context:
|
if self.crew_knowledge_context:
|
||||||
|
if self.knowledge_max_chars and len(self.crew_knowledge_context) > self.knowledge_max_chars:
|
||||||
|
self.crew_knowledge_context = self.crew_knowledge_context[:self.knowledge_max_chars] + "..."
|
||||||
task_prompt += self.crew_knowledge_context
|
task_prompt += self.crew_knowledge_context
|
||||||
|
|
||||||
crewai_event_bus.emit(
|
crewai_event_bus.emit(
|
||||||
@@ -632,6 +658,11 @@ class Agent(BaseAgent):
|
|||||||
self.response_template.split("{{ .Response }}")[1].strip()
|
self.response_template.split("{{ .Response }}")[1].strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.tools_prompt_strategy == "names_only":
|
||||||
|
tools_description = get_tool_names(parsed_tools)
|
||||||
|
else:
|
||||||
|
tools_description = render_text_description_and_args(parsed_tools)
|
||||||
|
|
||||||
self.agent_executor = CrewAgentExecutor(
|
self.agent_executor = CrewAgentExecutor(
|
||||||
llm=self.llm,
|
llm=self.llm,
|
||||||
task=task, # type: ignore[arg-type]
|
task=task, # type: ignore[arg-type]
|
||||||
@@ -644,7 +675,7 @@ class Agent(BaseAgent):
|
|||||||
max_iter=self.max_iter,
|
max_iter=self.max_iter,
|
||||||
tools_handler=self.tools_handler,
|
tools_handler=self.tools_handler,
|
||||||
tools_names=get_tool_names(parsed_tools),
|
tools_names=get_tool_names(parsed_tools),
|
||||||
tools_description=render_text_description_and_args(parsed_tools),
|
tools_description=tools_description,
|
||||||
step_callback=self.step_callback,
|
step_callback=self.step_callback,
|
||||||
function_calling_llm=self.function_calling_llm,
|
function_calling_llm=self.function_calling_llm,
|
||||||
respect_context_window=self.respect_context_window,
|
respect_context_window=self.respect_context_window,
|
||||||
|
|||||||
@@ -144,12 +144,33 @@ class LangGraphAgentAdapter(BaseAgentAdapter):
|
|||||||
Returns:
|
Returns:
|
||||||
The complete system prompt string.
|
The complete system prompt string.
|
||||||
"""
|
"""
|
||||||
base_prompt = f"""
|
compact_mode = getattr(self, "compact_mode", False)
|
||||||
You are {self.role}.
|
role = self.role
|
||||||
|
goal = self.goal
|
||||||
|
backstory = self.backstory
|
||||||
|
|
||||||
Your goal is: {self.goal}
|
if compact_mode:
|
||||||
|
if len(role) > 100:
|
||||||
|
role = role[:97] + "..."
|
||||||
|
if len(goal) > 150:
|
||||||
|
goal = goal[:147] + "..."
|
||||||
|
backstory = ""
|
||||||
|
|
||||||
Your backstory: {self.backstory}
|
if backstory:
|
||||||
|
base_prompt = f"""
|
||||||
|
You are {role}.
|
||||||
|
|
||||||
|
Your goal is: {goal}
|
||||||
|
|
||||||
|
Your backstory: {backstory}
|
||||||
|
|
||||||
|
When working on tasks, think step-by-step and use the available tools when necessary.
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
base_prompt = f"""
|
||||||
|
You are {role}.
|
||||||
|
|
||||||
|
Your goal is: {goal}
|
||||||
|
|
||||||
When working on tasks, think step-by-step and use the available tools when necessary.
|
When working on tasks, think step-by-step and use the available tools when necessary.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -90,12 +90,33 @@ class OpenAIAgentAdapter(BaseAgentAdapter):
|
|||||||
Returns:
|
Returns:
|
||||||
The complete system prompt string.
|
The complete system prompt string.
|
||||||
"""
|
"""
|
||||||
base_prompt = f"""
|
compact_mode = getattr(self, "compact_mode", False)
|
||||||
You are {self.role}.
|
role = self.role
|
||||||
|
goal = self.goal
|
||||||
|
backstory = self.backstory
|
||||||
|
|
||||||
Your goal is: {self.goal}
|
if compact_mode:
|
||||||
|
if len(role) > 100:
|
||||||
|
role = role[:97] + "..."
|
||||||
|
if len(goal) > 150:
|
||||||
|
goal = goal[:147] + "..."
|
||||||
|
backstory = ""
|
||||||
|
|
||||||
Your backstory: {self.backstory}
|
if backstory:
|
||||||
|
base_prompt = f"""
|
||||||
|
You are {role}.
|
||||||
|
|
||||||
|
Your goal is: {goal}
|
||||||
|
|
||||||
|
Your backstory: {backstory}
|
||||||
|
|
||||||
|
When working on tasks, think step-by-step and use the available tools when necessary.
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
base_prompt = f"""
|
||||||
|
You are {role}.
|
||||||
|
|
||||||
|
Your goal is: {goal}
|
||||||
|
|
||||||
When working on tasks, think step-by-step and use the available tools when necessary.
|
When working on tasks, think step-by-step and use the available tools when necessary.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -224,6 +224,10 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if self.agent and getattr(self.agent, "proactive_context_trimming", False):
|
||||||
|
from crewai.utilities.agent_utils import trim_messages_structurally
|
||||||
|
trim_messages_structurally(self.messages)
|
||||||
|
|
||||||
enforce_rpm_limit(self.request_within_rpm_limit)
|
enforce_rpm_limit(self.request_within_rpm_limit)
|
||||||
|
|
||||||
answer = get_llm_response(
|
answer = get_llm_response(
|
||||||
|
|||||||
@@ -458,6 +458,38 @@ def handle_context_length(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def trim_messages_structurally(
|
||||||
|
messages: list[LLMMessage],
|
||||||
|
keep_last_n: int = 3,
|
||||||
|
max_total_chars: int = 50000,
|
||||||
|
) -> None:
|
||||||
|
"""Trim messages structurally without LLM calls.
|
||||||
|
|
||||||
|
Keeps system message and last N message pairs, drops oldest messages
|
||||||
|
until total character count is under the threshold.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: List of messages to trim in-place
|
||||||
|
keep_last_n: Number of recent message pairs to keep
|
||||||
|
max_total_chars: Maximum total character count for all messages
|
||||||
|
"""
|
||||||
|
if not messages:
|
||||||
|
return
|
||||||
|
|
||||||
|
system_messages = [msg for msg in messages if msg.get("role") == "system"]
|
||||||
|
non_system_messages = [msg for msg in messages if msg.get("role") != "system"]
|
||||||
|
|
||||||
|
total_chars = sum(len(str(msg.get("content", ""))) for msg in messages)
|
||||||
|
|
||||||
|
if total_chars <= max_total_chars:
|
||||||
|
return
|
||||||
|
|
||||||
|
messages_to_keep = system_messages + non_system_messages[-keep_last_n * 2:]
|
||||||
|
|
||||||
|
messages.clear()
|
||||||
|
messages.extend(messages_to_keep)
|
||||||
|
|
||||||
|
|
||||||
def summarize_messages(
|
def summarize_messages(
|
||||||
messages: list[LLMMessage],
|
messages: list[LLMMessage],
|
||||||
llm: LLM | BaseLLM,
|
llm: LLM | BaseLLM,
|
||||||
|
|||||||
@@ -129,8 +129,20 @@ class Prompts(BaseModel):
|
|||||||
else:
|
else:
|
||||||
prompt = f"{system}\n{prompt}"
|
prompt = f"{system}\n{prompt}"
|
||||||
|
|
||||||
|
compact_mode = getattr(self.agent, "compact_mode", False)
|
||||||
|
role = self.agent.role
|
||||||
|
goal = self.agent.goal
|
||||||
|
backstory = self.agent.backstory
|
||||||
|
|
||||||
|
if compact_mode:
|
||||||
|
if len(role) > 100:
|
||||||
|
role = role[:97] + "..."
|
||||||
|
if len(goal) > 150:
|
||||||
|
goal = goal[:147] + "..."
|
||||||
|
backstory = ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
prompt.replace("{goal}", self.agent.goal)
|
prompt.replace("{goal}", goal)
|
||||||
.replace("{role}", self.agent.role)
|
.replace("{role}", role)
|
||||||
.replace("{backstory}", self.agent.backstory)
|
.replace("{backstory}", backstory)
|
||||||
)
|
)
|
||||||
|
|||||||
114
lib/crewai/tests/utilities/test_memory_knowledge_truncation.py
Normal file
114
lib/crewai/tests/utilities/test_memory_knowledge_truncation.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"""Tests for memory and knowledge truncation."""
|
||||||
|
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_truncate_text_helper():
|
||||||
|
"""Test basic text truncation helper logic."""
|
||||||
|
text = "A" * 1000
|
||||||
|
max_chars = 500
|
||||||
|
|
||||||
|
if len(text) > max_chars:
|
||||||
|
truncated = text[:max_chars] + "..."
|
||||||
|
|
||||||
|
assert len(truncated) == max_chars + 3
|
||||||
|
assert truncated.endswith("...")
|
||||||
|
assert truncated.startswith("A" * 100)
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_truncation_when_max_chars_set():
|
||||||
|
"""Test that memory is truncated when memory_max_chars is set."""
|
||||||
|
from crewai.agent import Agent
|
||||||
|
|
||||||
|
long_memory = "M" * 2000
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
memory_max_chars=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
if agent.memory_max_chars and len(long_memory) > agent.memory_max_chars:
|
||||||
|
truncated_memory = long_memory[:agent.memory_max_chars] + "..."
|
||||||
|
|
||||||
|
assert len(truncated_memory) == 1003
|
||||||
|
assert truncated_memory.endswith("...")
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_not_truncated_when_max_chars_none():
|
||||||
|
"""Test that memory is not truncated when memory_max_chars is None."""
|
||||||
|
from crewai.agent import Agent
|
||||||
|
|
||||||
|
long_memory = "M" * 2000
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
memory_max_chars=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
result_memory = long_memory
|
||||||
|
if agent.memory_max_chars and len(long_memory) > agent.memory_max_chars:
|
||||||
|
result_memory = long_memory[:agent.memory_max_chars] + "..."
|
||||||
|
|
||||||
|
assert len(result_memory) == 2000
|
||||||
|
assert not result_memory.endswith("...")
|
||||||
|
|
||||||
|
|
||||||
|
def test_knowledge_truncation_when_max_chars_set():
|
||||||
|
"""Test that knowledge is truncated when knowledge_max_chars is set."""
|
||||||
|
from crewai.agent import Agent
|
||||||
|
|
||||||
|
long_knowledge = "K" * 3000
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
knowledge_max_chars=1500,
|
||||||
|
)
|
||||||
|
|
||||||
|
if agent.knowledge_max_chars and len(long_knowledge) > agent.knowledge_max_chars:
|
||||||
|
truncated_knowledge = long_knowledge[:agent.knowledge_max_chars] + "..."
|
||||||
|
|
||||||
|
assert len(truncated_knowledge) == 1503
|
||||||
|
assert truncated_knowledge.endswith("...")
|
||||||
|
|
||||||
|
|
||||||
|
def test_knowledge_not_truncated_when_max_chars_none():
|
||||||
|
"""Test that knowledge is not truncated when knowledge_max_chars is None."""
|
||||||
|
from crewai.agent import Agent
|
||||||
|
|
||||||
|
long_knowledge = "K" * 3000
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
knowledge_max_chars=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
result_knowledge = long_knowledge
|
||||||
|
if agent.knowledge_max_chars and len(long_knowledge) > agent.knowledge_max_chars:
|
||||||
|
result_knowledge = long_knowledge[:agent.knowledge_max_chars] + "..."
|
||||||
|
|
||||||
|
assert len(result_knowledge) == 3000
|
||||||
|
assert not result_knowledge.endswith("...")
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_config_fields_exist():
|
||||||
|
"""Test that new configuration fields exist on Agent."""
|
||||||
|
from crewai.agent import Agent
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
memory_max_chars=1000,
|
||||||
|
knowledge_max_chars=2000,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hasattr(agent, "memory_max_chars")
|
||||||
|
assert hasattr(agent, "knowledge_max_chars")
|
||||||
|
assert agent.memory_max_chars == 1000
|
||||||
|
assert agent.knowledge_max_chars == 2000
|
||||||
134
lib/crewai/tests/utilities/test_proactive_context_trimming.py
Normal file
134
lib/crewai/tests/utilities/test_proactive_context_trimming.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
"""Tests for proactive context trimming."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from crewai.utilities.agent_utils import trim_messages_structurally
|
||||||
|
|
||||||
|
|
||||||
|
def test_trim_messages_structurally_keeps_system_message():
|
||||||
|
"""Test that trim_messages_structurally preserves system messages."""
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "You are a helpful assistant"},
|
||||||
|
{"role": "user", "content": "Hello"},
|
||||||
|
{"role": "assistant", "content": "Hi there"},
|
||||||
|
{"role": "user", "content": "How are you?"},
|
||||||
|
{"role": "assistant", "content": "I'm doing well"},
|
||||||
|
]
|
||||||
|
|
||||||
|
trim_messages_structurally(messages, keep_last_n=1, max_total_chars=100)
|
||||||
|
|
||||||
|
system_messages = [msg for msg in messages if msg.get("role") == "system"]
|
||||||
|
assert len(system_messages) == 1
|
||||||
|
assert system_messages[0]["content"] == "You are a helpful assistant"
|
||||||
|
|
||||||
|
|
||||||
|
def test_trim_messages_structurally_keeps_last_n_pairs():
|
||||||
|
"""Test that trim_messages_structurally keeps last N message pairs."""
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "System"},
|
||||||
|
{"role": "user", "content": "A" * 10000},
|
||||||
|
{"role": "assistant", "content": "B" * 10000},
|
||||||
|
{"role": "user", "content": "C" * 10000},
|
||||||
|
{"role": "assistant", "content": "D" * 10000},
|
||||||
|
{"role": "user", "content": "E" * 100},
|
||||||
|
{"role": "assistant", "content": "F" * 100},
|
||||||
|
]
|
||||||
|
|
||||||
|
trim_messages_structurally(messages, keep_last_n=1, max_total_chars=1000)
|
||||||
|
|
||||||
|
assert len(messages) == 3
|
||||||
|
assert messages[0]["role"] == "system"
|
||||||
|
assert messages[1]["content"] == "E" * 100
|
||||||
|
assert messages[2]["content"] == "F" * 100
|
||||||
|
|
||||||
|
|
||||||
|
def test_trim_messages_structurally_no_trim_when_under_limit():
|
||||||
|
"""Test that trim_messages_structurally doesn't trim when under limit."""
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "System"},
|
||||||
|
{"role": "user", "content": "Hello"},
|
||||||
|
{"role": "assistant", "content": "Hi"},
|
||||||
|
]
|
||||||
|
|
||||||
|
original_length = len(messages)
|
||||||
|
trim_messages_structurally(messages, keep_last_n=3, max_total_chars=50000)
|
||||||
|
|
||||||
|
assert len(messages) == original_length
|
||||||
|
|
||||||
|
|
||||||
|
def test_trim_messages_structurally_handles_empty_messages():
|
||||||
|
"""Test that trim_messages_structurally handles empty message list."""
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
trim_messages_structurally(messages, keep_last_n=3, max_total_chars=1000)
|
||||||
|
|
||||||
|
assert len(messages) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_trim_messages_structurally_with_multiple_system_messages():
|
||||||
|
"""Test that trim_messages_structurally preserves all system messages."""
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "System 1"},
|
||||||
|
{"role": "system", "content": "System 2"},
|
||||||
|
{"role": "user", "content": "A" * 10000},
|
||||||
|
{"role": "assistant", "content": "B" * 10000},
|
||||||
|
{"role": "user", "content": "C" * 100},
|
||||||
|
{"role": "assistant", "content": "D" * 100},
|
||||||
|
]
|
||||||
|
|
||||||
|
trim_messages_structurally(messages, keep_last_n=1, max_total_chars=1000)
|
||||||
|
|
||||||
|
system_messages = [msg for msg in messages if msg.get("role") == "system"]
|
||||||
|
assert len(system_messages) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_proactive_context_trimming_config():
|
||||||
|
"""Test that Agent has proactive_context_trimming configuration field."""
|
||||||
|
from crewai.agent import Agent
|
||||||
|
|
||||||
|
agent_with_trimming = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
proactive_context_trimming=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
agent_without_trimming = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
proactive_context_trimming=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hasattr(agent_with_trimming, "proactive_context_trimming")
|
||||||
|
assert hasattr(agent_without_trimming, "proactive_context_trimming")
|
||||||
|
assert agent_with_trimming.proactive_context_trimming is True
|
||||||
|
assert agent_without_trimming.proactive_context_trimming is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_proactive_context_trimming_default_is_false():
|
||||||
|
"""Test that proactive_context_trimming defaults to False."""
|
||||||
|
from crewai.agent import Agent
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert agent.proactive_context_trimming is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_trim_messages_structurally_calculates_total_chars_correctly():
|
||||||
|
"""Test that trim_messages_structurally calculates total characters correctly."""
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "12345"},
|
||||||
|
{"role": "user", "content": "67890"},
|
||||||
|
{"role": "assistant", "content": "ABCDE"},
|
||||||
|
]
|
||||||
|
|
||||||
|
total_chars = sum(len(str(msg.get("content", ""))) for msg in messages)
|
||||||
|
assert total_chars == 15
|
||||||
|
|
||||||
|
trim_messages_structurally(messages, keep_last_n=3, max_total_chars=20)
|
||||||
|
assert len(messages) == 3
|
||||||
85
lib/crewai/tests/utilities/test_prompts_compact_mode.py
Normal file
85
lib/crewai/tests/utilities/test_prompts_compact_mode.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""Tests for compact mode in prompt generation."""
|
||||||
|
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from crewai.utilities.prompts import Prompts
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompts_compact_mode_shortens_role():
|
||||||
|
"""Test that compact mode caps role length to 100 characters."""
|
||||||
|
agent = Mock()
|
||||||
|
agent.role = "A" * 200
|
||||||
|
agent.goal = "Test goal"
|
||||||
|
agent.backstory = "Test backstory"
|
||||||
|
agent.compact_mode = True
|
||||||
|
|
||||||
|
prompts = Prompts(agent=agent, has_tools=False)
|
||||||
|
result = prompts._build_prompt(["role_playing"])
|
||||||
|
|
||||||
|
assert len(agent.role) == 200
|
||||||
|
assert "A" * 97 + "..." in result
|
||||||
|
assert "A" * 100 not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompts_compact_mode_shortens_goal():
|
||||||
|
"""Test that compact mode caps goal length to 150 characters."""
|
||||||
|
agent = Mock()
|
||||||
|
agent.role = "Test role"
|
||||||
|
agent.goal = "B" * 200
|
||||||
|
agent.backstory = "Test backstory"
|
||||||
|
agent.compact_mode = True
|
||||||
|
|
||||||
|
prompts = Prompts(agent=agent, has_tools=False)
|
||||||
|
result = prompts._build_prompt(["role_playing"])
|
||||||
|
|
||||||
|
assert len(agent.goal) == 200
|
||||||
|
assert "B" * 147 + "..." in result
|
||||||
|
assert "B" * 150 not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompts_compact_mode_omits_backstory():
|
||||||
|
"""Test that compact mode omits backstory entirely."""
|
||||||
|
agent = Mock()
|
||||||
|
agent.role = "Test role"
|
||||||
|
agent.goal = "Test goal"
|
||||||
|
agent.backstory = "This is a very long backstory that should be omitted in compact mode"
|
||||||
|
agent.compact_mode = True
|
||||||
|
|
||||||
|
prompts = Prompts(agent=agent, has_tools=False)
|
||||||
|
result = prompts._build_prompt(["role_playing"])
|
||||||
|
|
||||||
|
assert "backstory" not in result.lower() or result.count("{backstory}") > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompts_normal_mode_preserves_full_content():
|
||||||
|
"""Test that normal mode (compact_mode=False) preserves full role, goal, and backstory."""
|
||||||
|
agent = Mock()
|
||||||
|
agent.role = "A" * 200
|
||||||
|
agent.goal = "B" * 200
|
||||||
|
agent.backstory = "C" * 200
|
||||||
|
agent.compact_mode = False
|
||||||
|
|
||||||
|
prompts = Prompts(agent=agent, has_tools=False)
|
||||||
|
result = prompts._build_prompt(["role_playing"])
|
||||||
|
|
||||||
|
assert "A" * 200 in result
|
||||||
|
assert "B" * 200 in result
|
||||||
|
assert "C" * 200 in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompts_compact_mode_default_false():
|
||||||
|
"""Test that compact mode defaults to False when not set."""
|
||||||
|
agent = Mock()
|
||||||
|
agent.role = "A" * 200
|
||||||
|
agent.goal = "B" * 200
|
||||||
|
agent.backstory = "C" * 200
|
||||||
|
del agent.compact_mode
|
||||||
|
|
||||||
|
prompts = Prompts(agent=agent, has_tools=False)
|
||||||
|
result = prompts._build_prompt(["role_playing"])
|
||||||
|
|
||||||
|
assert "A" * 200 in result
|
||||||
|
assert "B" * 200 in result
|
||||||
|
assert "C" * 200 in result
|
||||||
95
lib/crewai/tests/utilities/test_tools_prompt_strategy.py
Normal file
95
lib/crewai/tests/utilities/test_tools_prompt_strategy.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""Tests for tools_prompt_strategy configuration."""
|
||||||
|
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from crewai.utilities.agent_utils import get_tool_names, render_text_description_and_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_tool_names_returns_comma_separated_names():
|
||||||
|
"""Test that get_tool_names returns comma-separated tool names."""
|
||||||
|
tool1 = Mock()
|
||||||
|
tool1.name = "search_tool"
|
||||||
|
tool2 = Mock()
|
||||||
|
tool2.name = "calculator_tool"
|
||||||
|
tool3 = Mock()
|
||||||
|
tool3.name = "file_reader_tool"
|
||||||
|
|
||||||
|
tools = [tool1, tool2, tool3]
|
||||||
|
result = get_tool_names(tools)
|
||||||
|
|
||||||
|
assert result == "search_tool, calculator_tool, file_reader_tool"
|
||||||
|
assert "description" not in result.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_text_description_includes_descriptions():
|
||||||
|
"""Test that render_text_description_and_args includes full descriptions."""
|
||||||
|
tool1 = Mock()
|
||||||
|
tool1.description = "This is a search tool that searches the web for information"
|
||||||
|
tool2 = Mock()
|
||||||
|
tool2.description = "This is a calculator tool that performs mathematical operations"
|
||||||
|
|
||||||
|
tools = [tool1, tool2]
|
||||||
|
result = render_text_description_and_args(tools)
|
||||||
|
|
||||||
|
assert "search tool" in result
|
||||||
|
assert "calculator tool" in result
|
||||||
|
assert "searches the web" in result
|
||||||
|
assert "mathematical operations" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_names_only_strategy_is_shorter_than_full():
|
||||||
|
"""Test that names_only strategy produces shorter output than full descriptions."""
|
||||||
|
tool1 = Mock()
|
||||||
|
tool1.name = "search_tool"
|
||||||
|
tool1.description = "This is a very long description " * 10
|
||||||
|
tool2 = Mock()
|
||||||
|
tool2.name = "calculator_tool"
|
||||||
|
tool2.description = "This is another very long description " * 10
|
||||||
|
|
||||||
|
tools = [tool1, tool2]
|
||||||
|
|
||||||
|
names_only = get_tool_names(tools)
|
||||||
|
full_description = render_text_description_and_args(tools)
|
||||||
|
|
||||||
|
assert len(names_only) < len(full_description)
|
||||||
|
assert len(names_only) < 100
|
||||||
|
assert len(full_description) > 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_tools_prompt_strategy_config():
|
||||||
|
"""Test that Agent has tools_prompt_strategy configuration field."""
|
||||||
|
from crewai.agent import Agent
|
||||||
|
|
||||||
|
agent_full = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
tools_prompt_strategy="full",
|
||||||
|
)
|
||||||
|
|
||||||
|
agent_names = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
tools_prompt_strategy="names_only",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hasattr(agent_full, "tools_prompt_strategy")
|
||||||
|
assert hasattr(agent_names, "tools_prompt_strategy")
|
||||||
|
assert agent_full.tools_prompt_strategy == "full"
|
||||||
|
assert agent_names.tools_prompt_strategy == "names_only"
|
||||||
|
|
||||||
|
|
||||||
|
def test_tools_prompt_strategy_default_is_full():
|
||||||
|
"""Test that tools_prompt_strategy defaults to 'full'."""
|
||||||
|
from crewai.agent import Agent
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert agent.tools_prompt_strategy == "full"
|
||||||
Reference in New Issue
Block a user