diff --git a/lib/crewai/src/crewai/task.py b/lib/crewai/src/crewai/task.py index ff8f9f1b1..8ab0695be 100644 --- a/lib/crewai/src/crewai/task.py +++ b/lib/crewai/src/crewai/task.py @@ -1110,7 +1110,7 @@ Follow these guidelines: ) def _export_output( - self, result: str + self, result: str | BaseModel ) -> tuple[BaseModel | None, dict[str, Any] | None]: pydantic_output: BaseModel | None = None json_output: dict[str, Any] | None = None diff --git a/lib/crewai/src/crewai/utilities/converter.py b/lib/crewai/src/crewai/utilities/converter.py index 23a544e2d..bcab4da18 100644 --- a/lib/crewai/src/crewai/utilities/converter.py +++ b/lib/crewai/src/crewai/utilities/converter.py @@ -153,16 +153,18 @@ class Converter(OutputConverter): def convert_to_model( - result: str, + result: str | BaseModel, output_pydantic: type[BaseModel] | None, output_json: type[BaseModel] | None, agent: Agent | BaseAgent | None = None, converter_cls: type[Converter] | None = None, ) -> dict[str, Any] | BaseModel | str: - """Convert a result string to a Pydantic model or JSON. + """Convert a result to a Pydantic model or JSON. Args: - result: The result string to convert. + result: The result to convert. Usually a JSON string, but a Pydantic + instance is also accepted when an upstream caller already produced + a structured object. output_pydantic: The Pydantic model class to convert to. output_json: The Pydantic model class to convert to JSON. agent: The agent instance. @@ -175,6 +177,11 @@ def convert_to_model( if model is None: return result + if isinstance(result, BaseModel): + if isinstance(result, model): + return result.model_dump() if output_json else result + result = result.model_dump_json() + if converter_cls: return convert_with_instructions( result=result, diff --git a/lib/crewai/tests/test_task_guardrails.py b/lib/crewai/tests/test_task_guardrails.py index 814de2f8f..aff9965b6 100644 --- a/lib/crewai/tests/test_task_guardrails.py +++ b/lib/crewai/tests/test_task_guardrails.py @@ -690,6 +690,27 @@ def test_multiple_guardrails_with_pydantic_output(): assert parsed["processed"] is True +def test_export_output_accepts_pydantic_input(): + """Regression test for #5458: _export_output must not crash with TypeError + when called with a Pydantic instance (e.g. when an upstream caller passes + an already-converted model from a context task).""" + from pydantic import BaseModel + + class StructuredResult(BaseModel): + value: str + + task = create_smart_task( + description="Test pydantic export", + expected_output="Structured output", + output_pydantic=StructuredResult, + ) + + instance = StructuredResult(value="ok") + pydantic_output, json_output = task._export_output(instance) + assert pydantic_output is instance + assert json_output is None + + def test_guardrails_vs_single_guardrail_mutual_exclusion(): """Test that guardrails list nullifies single guardrail.""" diff --git a/lib/crewai/tests/utilities/test_converter.py b/lib/crewai/tests/utilities/test_converter.py index a7adc4897..e436f709c 100644 --- a/lib/crewai/tests/utilities/test_converter.py +++ b/lib/crewai/tests/utilities/test_converter.py @@ -87,6 +87,31 @@ def test_convert_to_model_with_no_model() -> None: assert output == "Plain text" +def test_convert_to_model_with_basemodel_input_matching_pydantic() -> None: + instance = SimpleModel(name="John", age=30) + output = convert_to_model(instance, SimpleModel, None, None) + assert output is instance + + +def test_convert_to_model_with_basemodel_input_matching_json() -> None: + instance = SimpleModel(name="John", age=30) + output = convert_to_model(instance, None, SimpleModel, None) + assert output == {"name": "John", "age": 30} + + +def test_convert_to_model_with_basemodel_input_different_class() -> None: + class OtherModel(BaseModel): + name: str + age: int + extra: str = "default" + + instance = OtherModel(name="John", age=30, extra="ignored") + output = convert_to_model(instance, SimpleModel, None, None) + assert isinstance(output, SimpleModel) + assert output.name == "John" + assert output.age == 30 + + def test_convert_to_model_with_special_characters() -> None: json_string_test = """ { diff --git a/uv.lock b/uv.lock index 0ce24c628..4c56c5035 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-04-28T07:00:00Z" +exclude-newer = "2026-04-27T16:00:00Z" [manifest] members = [