mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
fix(agent): surface knowledge retrieval failures (#38640)
This commit is contained in:
parent
0038bcc4bb
commit
9b3650e2ca
@ -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})")
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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",
|
||||
*,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user