mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-22 14:48:13 +00:00
Compare commits
3 Commits
devin/1768
...
devin/1763
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c5ff235bd | ||
|
|
31357f7f2a | ||
|
|
ebcf0c98d5 |
@@ -696,10 +696,17 @@ def _map_task_variables(
|
|||||||
callback_functions: Dictionary of available callbacks.
|
callback_functions: Dictionary of available callbacks.
|
||||||
output_pydantic_functions: Dictionary of Pydantic output class wrappers.
|
output_pydantic_functions: Dictionary of Pydantic output class wrappers.
|
||||||
"""
|
"""
|
||||||
if context_list := task_info.get("context"):
|
if "context" in task_info:
|
||||||
self.tasks_config[task_name]["context"] = [
|
context_value = task_info["context"]
|
||||||
tasks[context_task_name]() for context_task_name in context_list
|
if context_value is None:
|
||||||
]
|
self.tasks_config[task_name]["context"] = None
|
||||||
|
elif isinstance(context_value, list):
|
||||||
|
if context_value:
|
||||||
|
self.tasks_config[task_name]["context"] = [
|
||||||
|
tasks[context_task_name]() for context_task_name in context_value
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.tasks_config[task_name]["context"] = []
|
||||||
|
|
||||||
if tools := task_info.get("tools"):
|
if tools := task_info.get("tools"):
|
||||||
if _is_string_list(tools):
|
if _is_string_list(tools):
|
||||||
|
|||||||
@@ -15,24 +15,28 @@ def process_config(
|
|||||||
Returns:
|
Returns:
|
||||||
The updated values dictionary.
|
The updated values dictionary.
|
||||||
"""
|
"""
|
||||||
|
from crewai.utilities.constants import NOT_SPECIFIED
|
||||||
|
|
||||||
config = values.get("config", {})
|
config = values.get("config", {})
|
||||||
if not config:
|
if not config:
|
||||||
return values
|
return values
|
||||||
|
|
||||||
# Copy values from config (originally from YAML) to the model's attributes.
|
|
||||||
# Only copy if the attribute isn't already set, preserving any explicitly defined values.
|
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
if key not in model_class.model_fields or values.get(key) is not None:
|
if key not in model_class.model_fields:
|
||||||
|
continue
|
||||||
|
|
||||||
|
current = values.get(key)
|
||||||
|
|
||||||
|
if current is not None and current is not NOT_SPECIFIED:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
if isinstance(values.get(key), dict):
|
if isinstance(current, dict):
|
||||||
values[key].update(value)
|
current.update(value)
|
||||||
else:
|
else:
|
||||||
values[key] = value
|
values[key] = value
|
||||||
else:
|
else:
|
||||||
values[key] = value
|
values[key] = value
|
||||||
|
|
||||||
# Remove the config from values to avoid duplicate processing
|
|
||||||
values.pop("config", None)
|
values.pop("config", None)
|
||||||
return values
|
return values
|
||||||
|
|||||||
220
lib/crewai/tests/project/test_crew_base_context_none.py
Normal file
220
lib/crewai/tests/project/test_crew_base_context_none.py
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
"""Tests for crew_base._map_task_variables handling context: None."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from crewai import Agent, Task
|
||||||
|
from crewai.project.crew_base import CrewBase
|
||||||
|
|
||||||
|
|
||||||
|
@CrewBase
|
||||||
|
class TestCrew:
|
||||||
|
"""Test crew for context: None handling."""
|
||||||
|
|
||||||
|
agents_config = "config/agents.yaml"
|
||||||
|
tasks_config = "config/tasks.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
class TestMapTaskVariablesContextNone:
|
||||||
|
"""Test suite for _map_task_variables handling context: None."""
|
||||||
|
|
||||||
|
def test_map_task_variables_with_context_none(self):
|
||||||
|
"""Test that context: None in task_info is preserved in tasks_config."""
|
||||||
|
from crewai.project.crew_base import _map_task_variables
|
||||||
|
|
||||||
|
class MockCrewInstance:
|
||||||
|
def __init__(self):
|
||||||
|
self.tasks_config = {
|
||||||
|
"test_task": {
|
||||||
|
"description": "Test task",
|
||||||
|
"expected_output": "Test output",
|
||||||
|
"context": None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = MockCrewInstance()
|
||||||
|
task_info = {"context": None}
|
||||||
|
|
||||||
|
_map_task_variables(
|
||||||
|
instance,
|
||||||
|
task_name="test_task",
|
||||||
|
task_info=task_info,
|
||||||
|
agents={},
|
||||||
|
tasks={},
|
||||||
|
output_json_functions={},
|
||||||
|
tool_functions={},
|
||||||
|
callback_functions={},
|
||||||
|
output_pydantic_functions={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert instance.tasks_config["test_task"]["context"] is None
|
||||||
|
|
||||||
|
def test_map_task_variables_with_context_empty_list(self):
|
||||||
|
"""Test that context: [] in task_info is preserved as empty list."""
|
||||||
|
from crewai.project.crew_base import _map_task_variables
|
||||||
|
|
||||||
|
class MockCrewInstance:
|
||||||
|
def __init__(self):
|
||||||
|
self.tasks_config = {
|
||||||
|
"test_task": {
|
||||||
|
"description": "Test task",
|
||||||
|
"expected_output": "Test output"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = MockCrewInstance()
|
||||||
|
task_info = {"context": []}
|
||||||
|
|
||||||
|
_map_task_variables(
|
||||||
|
instance,
|
||||||
|
task_name="test_task",
|
||||||
|
task_info=task_info,
|
||||||
|
agents={},
|
||||||
|
tasks={},
|
||||||
|
output_json_functions={},
|
||||||
|
tool_functions={},
|
||||||
|
callback_functions={},
|
||||||
|
output_pydantic_functions={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert instance.tasks_config["test_task"]["context"] == []
|
||||||
|
|
||||||
|
def test_map_task_variables_with_context_list(self):
|
||||||
|
"""Test that context with task names is resolved to Task instances."""
|
||||||
|
from crewai.project.crew_base import _map_task_variables
|
||||||
|
|
||||||
|
class MockCrewInstance:
|
||||||
|
def __init__(self):
|
||||||
|
self.tasks_config = {
|
||||||
|
"test_task": {
|
||||||
|
"description": "Test task",
|
||||||
|
"expected_output": "Test output"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = MockCrewInstance()
|
||||||
|
|
||||||
|
task1 = Task(description="Task 1", expected_output="Output 1")
|
||||||
|
task2 = Task(description="Task 2", expected_output="Output 2")
|
||||||
|
|
||||||
|
tasks = {
|
||||||
|
"task1": lambda: task1,
|
||||||
|
"task2": lambda: task2
|
||||||
|
}
|
||||||
|
|
||||||
|
task_info = {"context": ["task1", "task2"]}
|
||||||
|
|
||||||
|
_map_task_variables(
|
||||||
|
instance,
|
||||||
|
task_name="test_task",
|
||||||
|
task_info=task_info,
|
||||||
|
agents={},
|
||||||
|
tasks=tasks,
|
||||||
|
output_json_functions={},
|
||||||
|
tool_functions={},
|
||||||
|
callback_functions={},
|
||||||
|
output_pydantic_functions={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(instance.tasks_config["test_task"]["context"]) == 2
|
||||||
|
assert instance.tasks_config["test_task"]["context"][0] is task1
|
||||||
|
assert instance.tasks_config["test_task"]["context"][1] is task2
|
||||||
|
|
||||||
|
def test_map_task_variables_without_context_key(self):
|
||||||
|
"""Test that missing context key doesn't add context to tasks_config."""
|
||||||
|
from crewai.project.crew_base import _map_task_variables
|
||||||
|
|
||||||
|
class MockCrewInstance:
|
||||||
|
def __init__(self):
|
||||||
|
self.tasks_config = {
|
||||||
|
"test_task": {
|
||||||
|
"description": "Test task",
|
||||||
|
"expected_output": "Test output"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = MockCrewInstance()
|
||||||
|
task_info = {}
|
||||||
|
|
||||||
|
_map_task_variables(
|
||||||
|
instance,
|
||||||
|
task_name="test_task",
|
||||||
|
task_info=task_info,
|
||||||
|
agents={},
|
||||||
|
tasks={},
|
||||||
|
output_json_functions={},
|
||||||
|
tool_functions={},
|
||||||
|
callback_functions={},
|
||||||
|
output_pydantic_functions={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "context" not in instance.tasks_config["test_task"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskWithContextNoneFromConfig:
|
||||||
|
"""Integration tests for Task creation with context: None from config."""
|
||||||
|
|
||||||
|
def test_task_with_context_none_from_config(self):
|
||||||
|
"""Test that Task can be created with config containing context: None."""
|
||||||
|
task = Task(
|
||||||
|
description="Test task",
|
||||||
|
expected_output="Test output",
|
||||||
|
config={"context": None}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert task.context is None
|
||||||
|
assert task.description == "Test task"
|
||||||
|
assert task.expected_output == "Test output"
|
||||||
|
|
||||||
|
def test_task_with_context_none_direct(self):
|
||||||
|
"""Test that Task can be created with context=None directly."""
|
||||||
|
task = Task(
|
||||||
|
description="Test task",
|
||||||
|
expected_output="Test output",
|
||||||
|
context=None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert task.context is None
|
||||||
|
assert task.description == "Test task"
|
||||||
|
assert task.expected_output == "Test output"
|
||||||
|
|
||||||
|
def test_task_with_context_empty_list_from_config(self):
|
||||||
|
"""Test that Task can be created with config containing context: []."""
|
||||||
|
task = Task(
|
||||||
|
description="Test task",
|
||||||
|
expected_output="Test output",
|
||||||
|
config={"context": []}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert task.context == []
|
||||||
|
assert task.description == "Test task"
|
||||||
|
assert task.expected_output == "Test output"
|
||||||
|
|
||||||
|
def test_task_without_context_uses_default(self):
|
||||||
|
"""Test that Task without context uses NOT_SPECIFIED default."""
|
||||||
|
from crewai.utilities.constants import NOT_SPECIFIED
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Test task",
|
||||||
|
expected_output="Test output"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert task.context is NOT_SPECIFIED
|
||||||
|
assert task.description == "Test task"
|
||||||
|
assert task.expected_output == "Test output"
|
||||||
|
|
||||||
|
def test_task_with_context_list_from_config(self):
|
||||||
|
"""Test that Task can be created with config containing context list."""
|
||||||
|
context_task = Task(
|
||||||
|
description="Context task",
|
||||||
|
expected_output="Context output"
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Test task",
|
||||||
|
expected_output="Test output",
|
||||||
|
config={"context": [context_task]}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(task.context, list)
|
||||||
|
assert len(task.context) == 1
|
||||||
|
assert task.context[0] is context_task
|
||||||
200
lib/crewai/tests/utilities/test_config.py
Normal file
200
lib/crewai/tests/utilities/test_config.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
"""Tests for utilities.config.process_config function."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from typing import Any
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from crewai.utilities.config import process_config
|
||||||
|
from crewai.utilities.constants import NOT_SPECIFIED
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessConfig:
|
||||||
|
"""Test suite for process_config function."""
|
||||||
|
|
||||||
|
def test_process_config_with_none_overrides_not_specified(self):
|
||||||
|
"""Test that config with None value overrides NOT_SPECIFIED sentinel."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
context: Any = Field(default=NOT_SPECIFIED)
|
||||||
|
description: str = "default"
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"context": NOT_SPECIFIED,
|
||||||
|
"description": "test",
|
||||||
|
"config": {"context": None}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = process_config(values, TestModel)
|
||||||
|
|
||||||
|
assert result["context"] is None
|
||||||
|
assert result["description"] == "test"
|
||||||
|
assert "config" not in result
|
||||||
|
|
||||||
|
def test_process_config_with_none_overrides_none(self):
|
||||||
|
"""Test that config with None value can override existing None."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
context: list[str] | None = None
|
||||||
|
description: str = "default"
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"context": None,
|
||||||
|
"description": "test",
|
||||||
|
"config": {"context": None}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = process_config(values, TestModel)
|
||||||
|
|
||||||
|
assert result["context"] is None
|
||||||
|
assert result["description"] == "test"
|
||||||
|
assert "config" not in result
|
||||||
|
|
||||||
|
def test_process_config_preserves_explicit_values(self):
|
||||||
|
"""Test that config does not override explicitly set non-None values."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
context: list[str] | None = None
|
||||||
|
description: str = "default"
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"context": ["task1", "task2"],
|
||||||
|
"description": "test",
|
||||||
|
"config": {"context": None}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = process_config(values, TestModel)
|
||||||
|
|
||||||
|
assert result["context"] == ["task1", "task2"]
|
||||||
|
assert result["description"] == "test"
|
||||||
|
assert "config" not in result
|
||||||
|
|
||||||
|
def test_process_config_with_empty_list_from_config(self):
|
||||||
|
"""Test that config with empty list is preserved."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
context: Any = Field(default=NOT_SPECIFIED)
|
||||||
|
description: str = "default"
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"context": NOT_SPECIFIED,
|
||||||
|
"description": "test",
|
||||||
|
"config": {"context": []}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = process_config(values, TestModel)
|
||||||
|
|
||||||
|
assert result["context"] == []
|
||||||
|
assert result["description"] == "test"
|
||||||
|
assert "config" not in result
|
||||||
|
|
||||||
|
def test_process_config_does_not_override_false(self):
|
||||||
|
"""Test that config does not override explicit False value."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
flag: bool = True
|
||||||
|
description: str = "default"
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"flag": False,
|
||||||
|
"description": "test",
|
||||||
|
"config": {"flag": True}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = process_config(values, TestModel)
|
||||||
|
|
||||||
|
assert result["flag"] is False
|
||||||
|
assert result["description"] == "test"
|
||||||
|
assert "config" not in result
|
||||||
|
|
||||||
|
def test_process_config_does_not_override_zero(self):
|
||||||
|
"""Test that config does not override explicit 0 value."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
count: int = 10
|
||||||
|
description: str = "default"
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"count": 0,
|
||||||
|
"description": "test",
|
||||||
|
"config": {"count": 5}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = process_config(values, TestModel)
|
||||||
|
|
||||||
|
assert result["count"] == 0
|
||||||
|
assert result["description"] == "test"
|
||||||
|
assert "config" not in result
|
||||||
|
|
||||||
|
def test_process_config_does_not_override_empty_string(self):
|
||||||
|
"""Test that config does not override explicit empty string value."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
name: str = "default"
|
||||||
|
description: str = "default"
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"name": "",
|
||||||
|
"description": "test",
|
||||||
|
"config": {"name": "new_name"}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = process_config(values, TestModel)
|
||||||
|
|
||||||
|
assert result["name"] == ""
|
||||||
|
assert result["description"] == "test"
|
||||||
|
assert "config" not in result
|
||||||
|
|
||||||
|
def test_process_config_with_dict_merge(self):
|
||||||
|
"""Test that config properly merges dict values when current is None or NOT_SPECIFIED."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
settings: dict[str, str] | None = None
|
||||||
|
description: str = "default"
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"settings": None,
|
||||||
|
"description": "test",
|
||||||
|
"config": {"settings": {"key2": "value2"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = process_config(values, TestModel)
|
||||||
|
|
||||||
|
assert result["settings"] == {"key2": "value2"}
|
||||||
|
assert result["description"] == "test"
|
||||||
|
assert "config" not in result
|
||||||
|
|
||||||
|
def test_process_config_with_no_config(self):
|
||||||
|
"""Test that process_config handles missing config gracefully."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
context: list[str] | None = None
|
||||||
|
description: str = "default"
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"context": None,
|
||||||
|
"description": "test"
|
||||||
|
}
|
||||||
|
|
||||||
|
result = process_config(values, TestModel)
|
||||||
|
|
||||||
|
assert result["context"] is None
|
||||||
|
assert result["description"] == "test"
|
||||||
|
|
||||||
|
def test_process_config_with_empty_config(self):
|
||||||
|
"""Test that process_config handles empty config gracefully."""
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
context: list[str] | None = None
|
||||||
|
description: str = "default"
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"context": None,
|
||||||
|
"description": "test",
|
||||||
|
"config": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = process_config(values, TestModel)
|
||||||
|
|
||||||
|
assert result["context"] is None
|
||||||
|
assert result["description"] == "test"
|
||||||
|
assert result["config"] == {}
|
||||||
Reference in New Issue
Block a user