diff --git a/api/clients/agent_backend/errors.py b/api/clients/agent_backend/errors.py index ee88c65fa88..b48c4d08da1 100644 --- a/api/clients/agent_backend/errors.py +++ b/api/clients/agent_backend/errors.py @@ -54,8 +54,21 @@ class AgentBackendRunFailedError(AgentBackendError): run_id: str detail: Any + reason: str | None + source_event_id: str | None - def __init__(self, run_id: str, detail: Any) -> None: + def __init__( + self, + run_id: str, + detail: Any, + *, + message: str | None = None, + reason: str | None = None, + source_event_id: str | None = None, + ) -> None: self.run_id = run_id self.detail = detail - super().__init__(f"Agent backend run failed: {run_id}") + self.reason = reason + self.source_event_id = source_event_id + display_message = message or f"Agent backend run failed: {run_id}" + super().__init__(f"{display_message} (agent_run_id={run_id})") diff --git a/api/core/app/apps/agent_app/app_runner.py b/api/core/app/apps/agent_app/app_runner.py index 08416d6c9ae..3ea2919d631 100644 --- a/api/core/app/apps/agent_app/app_runner.py +++ b/api/core/app/apps/agent_app/app_runner.py @@ -27,6 +27,7 @@ from clients.agent_backend import ( AgentBackendInternalEventType, AgentBackendRunClient, AgentBackendRunEventAdapter, + AgentBackendRunFailedError, AgentBackendRunFailedInternalEvent, AgentBackendRunSucceededInternalEvent, AgentBackendStreamInternalEvent, @@ -94,7 +95,18 @@ def _agent_backend_failure_to_exception(event: AgentBackendRunFailedInternalEven err_cls = _AGENT_BACKEND_INVOKE_ERROR_BY_REASON.get(event.reason or "") if err_cls is not None: return err_cls(event.error) - return AgentBackendError(event.error or "Agent backend run did not complete successfully.") + message = event.error or "Agent backend run did not complete successfully." + return AgentBackendRunFailedError( + event.run_id, + { + "error": event.error, + "reason": event.reason, + "source_event_id": event.source_event_id, + }, + message=message, + reason=event.reason, + source_event_id=event.source_event_id, + ) def _prompt_messages_from_query(user_query: str | None) -> list[PromptMessage]: diff --git a/api/tests/unit_tests/core/app/apps/agent_app/test_app_runner.py b/api/tests/unit_tests/core/app/apps/agent_app/test_app_runner.py index 55d5be6d555..09038920230 100644 --- a/api/tests/unit_tests/core/app/apps/agent_app/test_app_runner.py +++ b/api/tests/unit_tests/core/app/apps/agent_app/test_app_runner.py @@ -37,6 +37,7 @@ from pydantic_ai.messages import ( from clients.agent_backend import ( AgentBackendError, AgentBackendRunEventAdapter, + AgentBackendRunFailedError, AgentBackendRunFailedInternalEvent, AgentBackendStreamInternalEvent, FakeAgentBackendRunClient, @@ -1207,7 +1208,7 @@ def test_failed_run_raises_agent_backend_error(): store = _FakeSessionStore() qm = _FakeQueueManager() - with pytest.raises(AgentBackendError): + with pytest.raises(AgentBackendRunFailedError, match="fake failure .*agent_run_id=fake-run-1"): _run(_runner(client, store), qm) # No message-end on failure; no snapshot saved. assert not [e for e in qm.events if isinstance(e, QueueMessageEndEvent)] @@ -1227,6 +1228,28 @@ def test_agent_backend_failure_to_exception_maps_rate_limit_reason(): assert str(err) == "quota exceeded" +def test_agent_backend_failure_to_exception_preserves_unknown_reason_context(): + err = app_runner_module._agent_backend_failure_to_exception( + AgentBackendRunFailedInternalEvent( + run_id="run-1", + source_event_id="event-1", + error="Knowledge retrieval failed", + reason="knowledge_retrieve_failed", + ) + ) + + assert isinstance(err, AgentBackendRunFailedError) + assert err.run_id == "run-1" + assert err.reason == "knowledge_retrieve_failed" + assert err.source_event_id == "event-1" + assert err.detail == { + "error": "Knowledge retrieval failed", + "reason": "knowledge_retrieve_failed", + "source_event_id": "event-1", + } + assert str(err) == "Knowledge retrieval failed (agent_run_id=run-1)" + + def test_stopped_task_cancels_agent_backend_run_and_skips_session_save(): client = _RecordingFakeAgentBackendRunClient() store = _FakeSessionStore() diff --git a/api/tests/unit_tests/core/app/task_pipeline/test_based_generate_task_pipeline.py b/api/tests/unit_tests/core/app/task_pipeline/test_based_generate_task_pipeline.py index 06823644b27..70d1ac3b7f8 100644 --- a/api/tests/unit_tests/core/app/task_pipeline/test_based_generate_task_pipeline.py +++ b/api/tests/unit_tests/core/app/task_pipeline/test_based_generate_task_pipeline.py @@ -3,6 +3,7 @@ from unittest.mock import Mock import pytest +from clients.agent_backend.errors import AgentBackendRunFailedError from core.app.apps.base_app_generate_response_converter import AppGenerateResponseConverter from core.app.entities.queue_entities import QueueErrorEvent from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline @@ -41,6 +42,22 @@ class TestBasedGenerateTaskPipeline: err = pipeline.handle_error(event=event) assert err is event.error + def test_handle_error_preserves_agent_backend_run_failed_error(self, pipeline): + event = QueueErrorEvent( + error=AgentBackendRunFailedError( + "run-1", + {"reason": "knowledge_retrieve_failed"}, + message="Knowledge retrieval failed", + reason="knowledge_retrieve_failed", + ) + ) + + err = pipeline.handle_error(event=event) + + assert err is event.error + assert "Knowledge retrieval failed" in str(err) + assert "agent_run_id=run-1" in str(err) + def test_handle_error_updates_message_when_found(self, pipeline): event = QueueErrorEvent(error=ValueError("oops")) message = SimpleNamespace(status=MessageStatus.NORMAL, error=None) @@ -74,6 +91,22 @@ class TestBasedGenerateTaskPipeline: assert data == {"code": "rate_limit_error", "status": 429, "message": "quota exceeded"} + def test_stream_converter_maps_agent_backend_run_failed_error(self): + data = AppGenerateResponseConverter._error_to_stream_response( + AgentBackendRunFailedError( + "run-1", + {"reason": "knowledge_retrieve_failed"}, + message="Knowledge retrieval failed", + reason="knowledge_retrieve_failed", + ) + ) + + assert data == { + "code": "completion_request_error", + "status": 400, + "message": "Knowledge retrieval failed (agent_run_id=run-1)", + } + def test_handle_output_moderation_when_flagged(self, pipeline): handler = Mock() handler.moderation_completion.return_value = ("filtered", True) diff --git a/dify-agent/src/dify_agent/layers/knowledge/client.py b/dify-agent/src/dify_agent/layers/knowledge/client.py index b80e363190a..ca9186ed08d 100644 --- a/dify-agent/src/dify_agent/layers/knowledge/client.py +++ b/dify-agent/src/dify_agent/layers/knowledge/client.py @@ -178,11 +178,16 @@ class DifyKnowledgeBaseClient: def _build_http_error(response: httpx.Response) -> DifyKnowledgeBaseClientError: detail = _decode_error_detail(response) retryable = response.status_code in {429, 502} + error_code = detail["error_code"] message = detail["message"] or f"HTTP {response.status_code}" + if error_code: + message = f"Knowledge base search failed with HTTP {response.status_code} ({error_code}): {message}" + else: + message = f"Knowledge base search failed with HTTP {response.status_code}: {message}" return DifyKnowledgeBaseClientError( message, status_code=response.status_code, - error_code=detail["error_code"], + error_code=error_code, retryable=retryable, ) diff --git a/dify-agent/src/dify_agent/layers/knowledge/layer.py b/dify-agent/src/dify_agent/layers/knowledge/layer.py index df07dc3cd36..ed1005e40ae 100644 --- a/dify-agent/src/dify_agent/layers/knowledge/layer.py +++ b/dify-agent/src/dify_agent/layers/knowledge/layer.py @@ -243,7 +243,9 @@ class DifyKnowledgeBaseLayer( "knowledge_set_id": knowledge_set.id, "error_code": exc.error_code, "status_code": exc.status_code, + "error_message": str(exc), }, + exc_info=True, ) eager_results.append( DifyKnowledgeEagerResult( @@ -264,7 +266,9 @@ class DifyKnowledgeBaseLayer( "knowledge_set_id": knowledge_set.id, "error_code": exc.error_code, "status_code": exc.status_code, + "error_message": str(exc), }, + exc_info=True, ) raise @@ -313,7 +317,9 @@ class DifyKnowledgeBaseLayer( "knowledge_set_id": knowledge_set.id, "error_code": exc.error_code, "status_code": exc.status_code, + "error_message": str(exc), }, + exc_info=True, ) return TEMPORARY_UNAVAILABLE_OBSERVATION logger.error( @@ -325,7 +331,9 @@ class DifyKnowledgeBaseLayer( "knowledge_set_id": knowledge_set.id, "error_code": exc.error_code, "status_code": exc.status_code, + "error_message": str(exc), }, + exc_info=True, ) raise return _format_observation(response, self.config) diff --git a/dify-agent/src/dify_agent/runtime/runner.py b/dify-agent/src/dify_agent/runtime/runner.py index b7d1fe1260e..97c1ce7ef1f 100644 --- a/dify-agent/src/dify_agent/runtime/runner.py +++ b/dify-agent/src/dify_agent/runtime/runner.py @@ -49,6 +49,7 @@ from dify_agent.layers.ask_human.layer import get_ask_human_layer, validate_ask_ from dify_agent.layers.dify_core_tools.layer import DifyCoreToolsLayer from dify_agent.layers.dify_plugin.llm_layer import DifyPluginLLMLayer from dify_agent.layers.dify_plugin.tools_layer import DifyPluginToolsLayer +from dify_agent.layers.knowledge.client import DifyKnowledgeBaseClientError from dify_agent.layers.knowledge.layer import DifyKnowledgeBaseLayer from dify_agent.protocol.schemas import ( AgentRunUsage, @@ -128,6 +129,9 @@ def _run_failed_error_payload(exc: Exception) -> tuple[str, str | None]: if reason is None and exc.status_code == 429: reason = "InvokeRateLimitError" + if isinstance(exc, DifyKnowledgeBaseClientError): + reason = exc.error_code or "DifyKnowledgeBaseClientError" + return message, reason diff --git a/dify-agent/tests/local/dify_agent/runtime/test_runner.py b/dify-agent/tests/local/dify_agent/runtime/test_runner.py index d131675dcf5..25814034220 100644 --- a/dify-agent/tests/local/dify_agent/runtime/test_runner.py +++ b/dify-agent/tests/local/dify_agent/runtime/test_runner.py @@ -48,6 +48,7 @@ from dify_agent.layers.dify_core_tools.configs import ( DifyCoreToolsLayerConfig, ) from dify_agent.layers.dify_core_tools.layer import DifyCoreToolsLayer +from dify_agent.layers.knowledge.client import DifyKnowledgeBaseClientError from dify_agent.layers.knowledge.configs import DIFY_KNOWLEDGE_BASE_LAYER_TYPE_ID, DifyKnowledgeBaseLayerConfig from dify_agent.layers.knowledge.layer import DifyKnowledgeBaseLayer from dify_agent.layers.output import DIFY_OUTPUT_LAYER_TYPE_ID, DifyOutputLayerConfig @@ -149,6 +150,20 @@ def test_run_failed_error_payload_infers_rate_limit_reason_from_status_code() -> assert reason == "InvokeRateLimitError" +def test_run_failed_error_payload_preserves_knowledge_error_code() -> None: + exc = DifyKnowledgeBaseClientError( + "Knowledge base search failed with HTTP 400 (dataset_not_found): Dataset not found", + status_code=400, + error_code="dataset_not_found", + retryable=False, + ) + + message, reason = _run_failed_error_payload(exc) + + assert message == "Knowledge base search failed with HTTP 400 (dataset_not_found): Dataset not found" + assert reason == "dataset_not_found" + + def _request( user: str | list[str] = "hello", *,