Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions lib/crewai/src/crewai/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,10 +584,12 @@ async def _aexecute_core(

if self.output_file:
content = (
json_output
if json_output
task_output.json_dict
if task_output.json_dict
else (
pydantic_output.model_dump_json() if pydantic_output else result
task_output.pydantic.model_dump_json()
if task_output.pydantic
else task_output.raw
)
)
self._save_file(content)
Expand Down Expand Up @@ -677,10 +679,12 @@ def _execute_core(

if self.output_file:
content = (
json_output
if json_output
task_output.json_dict
if task_output.json_dict
else (
pydantic_output.model_dump_json() if pydantic_output else result
task_output.pydantic.model_dump_json()
if task_output.pydantic
else task_output.raw
)
)
self._save_file(content)
Expand Down
159 changes: 159 additions & 0 deletions lib/crewai/tests/test_task_guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,3 +752,162 @@ def guardrail_3(result: TaskOutput) -> tuple[bool, str]:
assert call_counts["g3"] == 1

assert "G3(1)" in result.raw


def test_output_file_contains_guardrail_modified_raw_result(tmp_path, monkeypatch):
"""Test that output file contains the result after guardrail modification for raw output."""
monkeypatch.chdir(tmp_path)
output_file = tmp_path / "output.txt"

def modify_guardrail(result: TaskOutput) -> tuple[bool, str]:
return (True, "MODIFIED BY GUARDRAIL")

agent = Mock()
agent.role = "test_agent"
agent.execute_task.return_value = "original result"
agent.crew = None
agent.last_messages = []

task = create_smart_task(
description="Test task",
expected_output="Output",
guardrails=[modify_guardrail],
output_file="output.txt",
)

result = task.execute_sync(agent=agent)

assert result.raw == "MODIFIED BY GUARDRAIL"
assert output_file.read_text() == "MODIFIED BY GUARDRAIL"


def test_output_file_contains_guardrail_modified_json_result(tmp_path, monkeypatch):
"""Test that output file contains the result after guardrail modification for JSON output."""
import json

from pydantic import BaseModel

monkeypatch.chdir(tmp_path)

class TestModel(BaseModel):
message: str

output_file = tmp_path / "output.json"

def modify_guardrail(result: TaskOutput) -> tuple[bool, str]:
return (True, '{"message": "modified by guardrail"}')

agent = Mock()
agent.role = "test_agent"
agent.execute_task.return_value = '{"message": "original"}'
agent.crew = None
agent.last_messages = []

task = create_smart_task(
description="Test task",
expected_output="Output",
guardrails=[modify_guardrail],
output_json=TestModel,
output_file="output.json",
)

result = task.execute_sync(agent=agent)

assert result.json_dict == {"message": "modified by guardrail"}
file_content = json.loads(output_file.read_text())
assert file_content == {"message": "modified by guardrail"}


def test_output_file_contains_guardrail_modified_pydantic_result(tmp_path, monkeypatch):
"""Test that output file contains the result after guardrail modification for pydantic output."""
import json

from pydantic import BaseModel

monkeypatch.chdir(tmp_path)

class TestModel(BaseModel):
message: str

output_file = tmp_path / "output.json"

def modify_guardrail(result: TaskOutput) -> tuple[bool, str]:
return (True, '{"message": "modified by guardrail"}')

agent = Mock()
agent.role = "test_agent"
agent.execute_task.return_value = '{"message": "original"}'
agent.crew = None
agent.last_messages = []

task = create_smart_task(
description="Test task",
expected_output="Output",
guardrails=[modify_guardrail],
output_pydantic=TestModel,
output_file="output.json",
)

result = task.execute_sync(agent=agent)

assert result.pydantic is not None
assert result.pydantic.message == "modified by guardrail"
file_content = json.loads(output_file.read_text())
assert file_content == {"message": "modified by guardrail"}


def test_output_file_with_single_guardrail_modification(tmp_path, monkeypatch):
"""Test that output file contains the result after single guardrail modification."""
monkeypatch.chdir(tmp_path)
output_file = tmp_path / "output.txt"

def modify_guardrail(result: TaskOutput) -> tuple[bool, str]:
return (True, result.raw.upper())

agent = Mock()
agent.role = "test_agent"
agent.execute_task.return_value = "hello world"
agent.crew = None
agent.last_messages = []

task = create_smart_task(
description="Test task",
expected_output="Output",
guardrail=modify_guardrail,
output_file="output.txt",
)

result = task.execute_sync(agent=agent)

assert result.raw == "HELLO WORLD"
assert output_file.read_text() == "HELLO WORLD"


def test_output_file_with_multiple_guardrails_chained_modifications(tmp_path, monkeypatch):
"""Test that output file contains the final result after multiple guardrail modifications."""
monkeypatch.chdir(tmp_path)
output_file = tmp_path / "output.txt"

def first_guardrail(result: TaskOutput) -> tuple[bool, str]:
return (True, f"[FIRST] {result.raw}")

def second_guardrail(result: TaskOutput) -> tuple[bool, str]:
return (True, f"{result.raw} [SECOND]")

agent = Mock()
agent.role = "test_agent"
agent.execute_task.return_value = "original"
agent.crew = None
agent.last_messages = []

task = create_smart_task(
description="Test task",
expected_output="Output",
guardrails=[first_guardrail, second_guardrail],
output_file="output.txt",
)

result = task.execute_sync(agent=agent)

assert result.raw == "[FIRST] original [SECOND]"
assert output_file.read_text() == "[FIRST] original [SECOND]"
Loading