mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-01 07:13:00 +00:00
fix: ensure TaskOutput.pydantic is populated on first guardrail invocation
Previously, when a task had guardrails configured, _export_output() was intentionally skipped on the first attempt, causing TaskOutput.pydantic and TaskOutput.json_dict to be None. This made it difficult to write guardrail functions that need to access the structured Pydantic output. This fix removes the conditional skip of _export_output() in both _execute_core() and _aexecute_core(), ensuring consistent behavior across all guardrail invocations. Fixes #4369 Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
@@ -579,10 +579,7 @@ class Task(BaseModel):
|
|||||||
tools=tools,
|
tools=tools,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self._guardrails and not self._guardrail:
|
pydantic_output, json_output = self._export_output(result)
|
||||||
pydantic_output, json_output = self._export_output(result)
|
|
||||||
else:
|
|
||||||
pydantic_output, json_output = None, None
|
|
||||||
|
|
||||||
task_output = TaskOutput(
|
task_output = TaskOutput(
|
||||||
name=self.name or self.description,
|
name=self.name or self.description,
|
||||||
@@ -674,10 +671,7 @@ class Task(BaseModel):
|
|||||||
tools=tools,
|
tools=tools,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self._guardrails and not self._guardrail:
|
pydantic_output, json_output = self._export_output(result)
|
||||||
pydantic_output, json_output = self._export_output(result)
|
|
||||||
else:
|
|
||||||
pydantic_output, json_output = None, None
|
|
||||||
|
|
||||||
task_output = TaskOutput(
|
task_output = TaskOutput(
|
||||||
name=self.name or self.description,
|
name=self.name or self.description,
|
||||||
|
|||||||
@@ -768,3 +768,152 @@ def test_per_guardrail_independent_retry_tracking():
|
|||||||
assert call_counts["g3"] == 1
|
assert call_counts["g3"] == 1
|
||||||
|
|
||||||
assert "G3(1)" in result.raw
|
assert "G3(1)" in result.raw
|
||||||
|
|
||||||
|
|
||||||
|
def test_guardrail_receives_pydantic_output_on_first_attempt():
|
||||||
|
"""Test that TaskOutput.pydantic is populated on the first guardrail invocation.
|
||||||
|
|
||||||
|
This is a regression test for issue #4369 where TaskOutput.pydantic was None
|
||||||
|
on the first guardrail call but correctly parsed on retry attempts.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
class MyOutput(BaseModel):
|
||||||
|
message: str = Field(description="A message")
|
||||||
|
status: str = Field(description="A status")
|
||||||
|
|
||||||
|
pydantic_values_received: list[MyOutput | None] = []
|
||||||
|
|
||||||
|
def my_guardrail(task_output: TaskOutput) -> tuple[bool, TaskOutput]:
|
||||||
|
pydantic_values_received.append(task_output.pydantic)
|
||||||
|
return (True, task_output)
|
||||||
|
|
||||||
|
agent = Mock()
|
||||||
|
agent.role = "test_agent"
|
||||||
|
agent.execute_task.return_value = json.dumps(
|
||||||
|
{"message": "Hello", "status": "success"}
|
||||||
|
)
|
||||||
|
agent.crew = None
|
||||||
|
agent.last_messages = []
|
||||||
|
|
||||||
|
task = create_smart_task(
|
||||||
|
description="Test pydantic parsing in guardrail",
|
||||||
|
expected_output="JSON with message and status",
|
||||||
|
output_pydantic=MyOutput,
|
||||||
|
guardrail=my_guardrail,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = task.execute_sync(agent=agent)
|
||||||
|
|
||||||
|
assert len(pydantic_values_received) == 1
|
||||||
|
assert pydantic_values_received[0] is not None, (
|
||||||
|
"TaskOutput.pydantic should not be None on first guardrail invocation"
|
||||||
|
)
|
||||||
|
assert isinstance(pydantic_values_received[0], MyOutput)
|
||||||
|
assert pydantic_values_received[0].message == "Hello"
|
||||||
|
assert pydantic_values_received[0].status == "success"
|
||||||
|
|
||||||
|
assert result.pydantic is not None
|
||||||
|
assert isinstance(result.pydantic, MyOutput)
|
||||||
|
|
||||||
|
|
||||||
|
def test_guardrail_receives_json_output_on_first_attempt():
|
||||||
|
"""Test that TaskOutput.json_dict is populated on the first guardrail invocation.
|
||||||
|
|
||||||
|
This is a regression test for issue #4369 where TaskOutput.json_dict was None
|
||||||
|
on the first guardrail call but correctly parsed on retry attempts.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
class MyOutput(BaseModel):
|
||||||
|
message: str = Field(description="A message")
|
||||||
|
status: str = Field(description="A status")
|
||||||
|
|
||||||
|
json_values_received: list[dict | None] = []
|
||||||
|
|
||||||
|
def my_guardrail(task_output: TaskOutput) -> tuple[bool, TaskOutput]:
|
||||||
|
json_values_received.append(task_output.json_dict)
|
||||||
|
return (True, task_output)
|
||||||
|
|
||||||
|
agent = Mock()
|
||||||
|
agent.role = "test_agent"
|
||||||
|
agent.execute_task.return_value = json.dumps(
|
||||||
|
{"message": "Hello", "status": "success"}
|
||||||
|
)
|
||||||
|
agent.crew = None
|
||||||
|
agent.last_messages = []
|
||||||
|
|
||||||
|
task = create_smart_task(
|
||||||
|
description="Test json parsing in guardrail",
|
||||||
|
expected_output="JSON with message and status",
|
||||||
|
output_json=MyOutput,
|
||||||
|
guardrail=my_guardrail,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = task.execute_sync(agent=agent)
|
||||||
|
|
||||||
|
assert len(json_values_received) == 1
|
||||||
|
assert json_values_received[0] is not None, (
|
||||||
|
"TaskOutput.json_dict should not be None on first guardrail invocation"
|
||||||
|
)
|
||||||
|
assert json_values_received[0]["message"] == "Hello"
|
||||||
|
assert json_values_received[0]["status"] == "success"
|
||||||
|
|
||||||
|
assert result.json_dict is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_guardrails_list_receives_pydantic_output_on_first_attempt():
|
||||||
|
"""Test that TaskOutput.pydantic is populated on first invocation with guardrails list.
|
||||||
|
|
||||||
|
This is a regression test for issue #4369 ensuring the fix works for both
|
||||||
|
single guardrail and guardrails list configurations.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
class MyOutput(BaseModel):
|
||||||
|
value: int = Field(description="A numeric value")
|
||||||
|
|
||||||
|
pydantic_values_received: list[MyOutput | None] = []
|
||||||
|
|
||||||
|
def first_guardrail(task_output: TaskOutput) -> tuple[bool, TaskOutput]:
|
||||||
|
pydantic_values_received.append(task_output.pydantic)
|
||||||
|
return (True, task_output)
|
||||||
|
|
||||||
|
def second_guardrail(task_output: TaskOutput) -> tuple[bool, TaskOutput]:
|
||||||
|
pydantic_values_received.append(task_output.pydantic)
|
||||||
|
return (True, task_output)
|
||||||
|
|
||||||
|
agent = Mock()
|
||||||
|
agent.role = "test_agent"
|
||||||
|
agent.execute_task.return_value = json.dumps({"value": 42})
|
||||||
|
agent.crew = None
|
||||||
|
agent.last_messages = []
|
||||||
|
|
||||||
|
task = create_smart_task(
|
||||||
|
description="Test pydantic parsing with guardrails list",
|
||||||
|
expected_output="JSON with value",
|
||||||
|
output_pydantic=MyOutput,
|
||||||
|
guardrails=[first_guardrail, second_guardrail],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = task.execute_sync(agent=agent)
|
||||||
|
|
||||||
|
assert len(pydantic_values_received) == 2
|
||||||
|
assert pydantic_values_received[0] is not None, (
|
||||||
|
"TaskOutput.pydantic should not be None on first guardrail invocation"
|
||||||
|
)
|
||||||
|
assert isinstance(pydantic_values_received[0], MyOutput)
|
||||||
|
assert pydantic_values_received[0].value == 42
|
||||||
|
|
||||||
|
assert pydantic_values_received[1] is not None
|
||||||
|
assert isinstance(pydantic_values_received[1], MyOutput)
|
||||||
|
|
||||||
|
assert result.pydantic is not None
|
||||||
|
assert isinstance(result.pydantic, MyOutput)
|
||||||
|
assert result.pydantic.value == 42
|
||||||
|
|||||||
Reference in New Issue
Block a user