fix(agent): surface knowledge retrieval failures (#38640)

This commit is contained in:
zyssyz123 2026-07-10 12:40:46 +08:00 committed by GitHub
parent 0038bcc4bb
commit 9b3650e2ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 118 additions and 5 deletions

View File

@ -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})")

View File

@ -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]:

View File

@ -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()

View File

@ -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)

View File

@ -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,
)

View File

@ -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)

View File

@ -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

View File

@ -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",
*,