diff --git a/api/controllers/console/app/workflow_app_log.py b/api/controllers/console/app/workflow_app_log.py index c9bf48103b..1a2599604d 100644 --- a/api/controllers/console/app/workflow_app_log.py +++ b/api/controllers/console/app/workflow_app_log.py @@ -139,6 +139,11 @@ class WorkflowAppLogPartialResponse(ResponseModel): return int(value.timestamp()) return value + @field_validator("evaluation", mode="before") + @classmethod + def _normalize_evaluation(cls, value: Any) -> list[dict[str, Any]] | list[WorkflowAppLogEvaluationItemResponse]: + return value or [] + class WorkflowArchivedLogPartialResponse(ResponseModel): id: str diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index f489bb1f4a..24c729d185 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -174,6 +174,11 @@ class WorkflowAppLogPartialResponse(ResponseModel): def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return _to_timestamp(value) + @field_validator("evaluation", mode="before") + @classmethod + def _normalize_evaluation(cls, value: Any) -> list[dict[str, Any]] | list[WorkflowAppLogEvaluationItemResponse]: + return value or [] + class WorkflowAppLogPaginationResponse(ResponseModel): page: int diff --git a/api/fields/workflow_app_log_fields.py b/api/fields/workflow_app_log_fields.py index 689e3c30c6..a5eb12e468 100644 --- a/api/fields/workflow_app_log_fields.py +++ b/api/fields/workflow_app_log_fields.py @@ -136,6 +136,11 @@ class WorkflowAppLogPartialResponse(ResponseModel): def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return _to_timestamp(value) + @field_validator("evaluation", mode="before") + @classmethod + def _normalize_evaluation(cls, value: Any) -> list[dict[str, Any]] | list[WorkflowAppLogEvaluationItemResponse]: + return value or [] + class WorkflowArchivedLogPartialResponse(ResponseModel): id: str diff --git a/api/services/workflow_app_service.py b/api/services/workflow_app_service.py index d2d6c7a92c..995c301e3f 100644 --- a/api/services/workflow_app_service.py +++ b/api/services/workflow_app_service.py @@ -24,6 +24,8 @@ class LogView: - Exposes `details_` for marshalling to `details` in API response - Exposes `evaluation_` for marshalling evaluation metrics in API response + - Normalizes missing evaluation results to an empty list because the API + schema treats "no evaluation data" as an empty collection, not `null` - Proxies all other attributes to the underlying `WorkflowAppLog` """ @@ -35,14 +37,14 @@ class LogView: ): self.log = log self.details_ = details - self.evaluation_ = evaluation + self.evaluation_ = evaluation or [] @property def details(self) -> LogViewDetails | None: return self.details_ @property - def evaluation(self) -> list[dict] | None: + def evaluation(self) -> list[dict]: return self.evaluation_ def __getattr__(self, name): @@ -192,7 +194,7 @@ class WorkflowAppService: eval_map = self._batch_query_evaluation_metrics(session, workflow_run_ids) items = [ - LogView(log, details, evaluation=eval_map.get(log.workflow_run_id)) + LogView(log, details, evaluation=eval_map.get(log.workflow_run_id, [])) for log, details in logs_with_details ] return { diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_app_log_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_app_log_api.py index fa3830ada4..3dc94bae59 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_app_log_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_app_log_api.py @@ -65,6 +65,25 @@ def test_workflow_app_log_pagination_response_normalizes_nested_fields(): assert response["data"][0]["evaluation"][0]["nodeInfo"]["node_id"] == "node-1" +def test_workflow_app_log_pagination_response_normalizes_null_evaluation_to_empty_list(): + response = workflow_app_log_module.WorkflowAppLogPaginationResponse.model_validate( + { + "page": 1, + "limit": 20, + "total": 1, + "has_more": False, + "data": [ + { + "id": "log-1", + "evaluation": None, + } + ], + } + ).model_dump(mode="json") + + assert response["data"][0]["evaluation"] == [] + + def test_workflow_archived_log_pagination_response_normalizes_nested_fields(): created_at = datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC) response = workflow_app_log_module.WorkflowArchivedLogPaginationResponse.model_validate( diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py index 74a3c75839..d883d2d2b9 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py @@ -29,6 +29,7 @@ from controllers.service_api.app.workflow import ( DifyAPIRepositoryFactory, GraphEngineManager, WorkflowAppLogApi, + WorkflowAppLogPaginationResponse, WorkflowLogQuery, WorkflowRunApi, WorkflowRunByIdApi, @@ -238,6 +239,25 @@ class TestWorkflowAppService: assert result.limit == 20 +def test_workflow_app_log_pagination_response_normalizes_null_evaluation_to_empty_list(): + response = WorkflowAppLogPaginationResponse.model_validate( + { + "page": 1, + "limit": 20, + "total": 1, + "has_more": False, + "data": [ + { + "id": "log-1", + "evaluation": None, + } + ], + } + ).model_dump(mode="json") + + assert response["data"][0]["evaluation"] == [] + + class TestWorkflowExecutionStatus: """Test WorkflowExecutionStatus enum."""