mirror of
https://github.com/langgenius/dify.git
synced 2026-11-12 06:11:12 +08:00
165 lines
8.4 KiB
Python
165 lines
8.4 KiB
Python
"""Finite sanitized execution-refusal copy; no graph/input/error interpolation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping, Sequence
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from runtime_contracts.builder.copy_ids import BuilderCopyId
|
|
from runtime_contracts.builder.execution import ExecutionEvidenceSummary
|
|
from runtime_contracts.builder.presentation import BuilderText, copy_text, text_parts
|
|
|
|
if TYPE_CHECKING:
|
|
from core.dify_builder.models import Run
|
|
|
|
FALLBACK = copy_text(BuilderCopyId.REPLIES_EXECUTION_SAFETY_REQUIRES_REVIEW_OR_REVISED)
|
|
_MESSAGES = {
|
|
"execution_permission_denied": copy_text(BuilderCopyId.TEST_PERMISSION_DENIED),
|
|
"execution_owner_mismatch": copy_text(BuilderCopyId.TEST_PERMISSION_DENIED),
|
|
"agent_access_denied": copy_text(BuilderCopyId.TEST_PERMISSION_DENIED),
|
|
"stale_execution_revision": copy_text(BuilderCopyId.TEST_CONTEXT_CHANGED),
|
|
"test_input_binding_mismatch": copy_text(BuilderCopyId.TEST_CONTEXT_CHANGED),
|
|
"effective_input_binding_mismatch": copy_text(BuilderCopyId.TEST_CONTEXT_CHANGED),
|
|
"context_digest_mismatch": copy_text(BuilderCopyId.TEST_CONTEXT_CHANGED),
|
|
"agent_capability_changed": copy_text(BuilderCopyId.TEST_CONTEXT_CHANGED),
|
|
"code_risk_assessment_stale": copy_text(BuilderCopyId.TEST_POLICY_REVIEW),
|
|
"installed_tool_unavailable": copy_text(BuilderCopyId.RESOURCES_CATALOGUE_UNAVAILABLE),
|
|
"agent_tool_metadata_unavailable": copy_text(BuilderCopyId.RESOURCES_CATALOGUE_UNAVAILABLE),
|
|
"agent_not_available": copy_text(BuilderCopyId.RESOURCES_CATALOGUE_UNAVAILABLE),
|
|
"unsupported_node_implementation": copy_text(BuilderCopyId.REPLIES_BUILDER_DOES_NOT_YET_HAVE_SAFE),
|
|
"unsupported_json_tool_configuration": copy_text(BuilderCopyId.REPLIES_BUILDER_CANNOT_SAFELY_TEST_THIS_TOOL),
|
|
"unsupported_json_credentials": copy_text(BuilderCopyId.REPLIES_BUILDER_CANNOT_SAFELY_TEST_THIS_TOOL2),
|
|
"unsupported_json_content_constant": copy_text(BuilderCopyId.REPLIES_PUT_THIS_J_S_O_N_CONTENT_IN_A),
|
|
"unsupported_json_filter": copy_text(BuilderCopyId.REPLIES_USE_A_SUPPORTED_J_S_O_N_PATH_SELECTION),
|
|
"unsupported_json_selector": copy_text(BuilderCopyId.REPLIES_BIND_J_S_O_N_PARSE_TEXT_FROM_A),
|
|
"invalid_json_parameters": copy_text(BuilderCopyId.REPLIES_SET_J_S_O_N_PARSE_CONTENT_AND_FILTER),
|
|
"json_execution_limit": copy_text(BuilderCopyId.REPLIES_REDUCE_J_S_O_N_PARSE_INPUT_OR_OUTPUT),
|
|
"installed_json_tool_unavailable": copy_text(BuilderCopyId.REPLIES_INSTALL_THE_QUALIFIED_J_S_O_N_PROCESS_VERSION),
|
|
"unsupported_json_tool_installation": copy_text(
|
|
BuilderCopyId.REPLIES_INSTALL_THE_QUALIFIED_J_S_O_N_PROCESS_VERSION
|
|
),
|
|
"json_tool_installation_changed": copy_text(BuilderCopyId.REPLIES_THE_INSTALLED_J_S_O_N_PROCESS_VERSION_CHANGED),
|
|
"unsupported_template_syntax": copy_text(BuilderCopyId.REPLIES_TEMPLATE_SUPPORTS_SCALAR_TEXT_IF_ELIF),
|
|
"unsupported_template_configuration": copy_text(BuilderCopyId.REPLIES_EDIT_TEMPLATE_VARIABLES_TO_USE_UNIQUE),
|
|
"unsupported_template_selector": copy_text(BuilderCopyId.REPLIES_USE_TEMPLATE_INPUTS_FROM_START_SCALARS),
|
|
"template_execution_limit": copy_text(BuilderCopyId.REPLIES_REDUCE_TEMPLATE_SIZE_OR_SELECTED_TEXT),
|
|
}
|
|
|
|
|
|
def execution_refusal_message(reason_code: str) -> BuilderText:
|
|
return _MESSAGES.get(reason_code, FALLBACK)
|
|
|
|
|
|
# One Code-only preparation fallback. Existing fixed-copy IDs are translated by
|
|
# Web; this backend-only disclosure cannot add a frontend catalog dependency.
|
|
_CODE_RISK_UNAVAILABLE = {
|
|
"en": "The Code risk explanation is unavailable. This test will simulate this step "
|
|
"and cannot verify its real behavior. "
|
|
"Prepare a new test to try the risk review again.",
|
|
"zh": "暂时无法说明此代码步骤的风险。本次测试将模拟该节点,无法验证其业务行为。请重新准备测试以重试风险评估。",
|
|
}
|
|
|
|
|
|
def code_risk_unavailable_notice(reply_language: str) -> str:
|
|
return _CODE_RISK_UNAVAILABLE["zh" if reply_language.lower().startswith("zh") else "en"]
|
|
|
|
|
|
# Only bounded backend-owned display labels are translated here. Technical
|
|
# codes remain in diagnostics/failure_code; resource names and user text are
|
|
# never interpreted as localization keys. Other locales use the English label.
|
|
_REASON_LABELS = {
|
|
"connection_refused": ("connection refused", "连接被拒绝"),
|
|
"timeout": ("request timed out", "请求超时"),
|
|
"authentication": ("authentication failed", "身份认证失败"),
|
|
"rate_limit": ("rate limit exceeded", "请求超过频率限制"),
|
|
"service_unavailable": ("service unavailable", "服务不可用"),
|
|
"provider_invocation_failed": ("model invocation failed", "模型调用失败"),
|
|
"model_resolution_failed": ("model configuration unavailable", "模型配置不可用"),
|
|
"empty_response": ("empty model response", "模型响应为空"),
|
|
"json_decode_failed": ("invalid JSON response", "模型响应不是有效的 JSON"),
|
|
"response_schema_invalid": ("response does not match the required schema", "模型响应不符合要求的结构"),
|
|
"unknown": ("cause unavailable", "暂未获取到具体原因"),
|
|
}
|
|
_SCOPE_LABELS = {"structure": ("structure", "结构"), "configuration": ("configuration", "配置")}
|
|
|
|
|
|
def _display_label(labels: Mapping[str, tuple[str, str]], value: str, reply_language: str) -> str:
|
|
pair = labels.get(value)
|
|
return pair[1 if reply_language.lower().startswith("zh") else 0] if pair else value
|
|
|
|
|
|
def change_scope_label(scope: str, reply_language: str) -> str:
|
|
"""Translate the display scope without mutating the ChangeSet's machine scope."""
|
|
return _display_label(_SCOPE_LABELS, scope, reply_language)
|
|
|
|
|
|
def observed_failure_message(
|
|
summary: ExecutionEvidenceSummary | None, *, reply_language: str = "en-US"
|
|
) -> BuilderText | None:
|
|
"""Observed preparation failure and unknown effects need different recovery."""
|
|
if summary is None:
|
|
return None
|
|
if summary.safety_outcome == "preparation_failed" or (
|
|
summary.safety_outcome == "native_failed" and summary.failure_phase == "execution"
|
|
):
|
|
message = (
|
|
copy_text(BuilderCopyId.TEST_RUNTIME_SERVICE_FAILED)
|
|
if summary.failure_phase == "execution"
|
|
else copy_text(
|
|
BuilderCopyId.TEST_PREPARATION_FAILED,
|
|
reason=_display_label(_REASON_LABELS, summary.failure_reason or "unknown", reply_language),
|
|
)
|
|
)
|
|
if summary.failure_resource:
|
|
_, provider, name = summary.failure_resource
|
|
return text_parts(
|
|
message, "\n", "/".join(filter(None, (provider, name))), "\n", ", ".join(summary.failure_node_ids)
|
|
)
|
|
return message
|
|
if summary.safety_outcome == "execution_evidence_unknown":
|
|
return copy_text(BuilderCopyId.REPLIES_EXECUTION_OUTCOME_IS_UNKNOWN_RUN_AGAIN_BEFORE_PUBLISHING)
|
|
return None
|
|
|
|
|
|
def native_service_failure(run: Run) -> bool:
|
|
"""Known terminal node failure: preserve the Run, avoid speculative graph repair."""
|
|
summary = run.verification.execution_evidence if run.verification else None
|
|
return bool(
|
|
run.status == "failed"
|
|
and run.dify_run_id
|
|
and summary is not None
|
|
and summary.sealed
|
|
and summary.native_run_id == run.dify_run_id
|
|
and summary.safety_outcome == "native_failed"
|
|
and summary.failure_phase == "execution"
|
|
and summary.failure_reason == "service_unavailable"
|
|
)
|
|
|
|
|
|
def cognition_failure_message(
|
|
records: Sequence[Mapping[str, Any]], fallback: BuilderText, *, reply_language: str = "en-US"
|
|
) -> BuilderText:
|
|
"""Use bounded observations from this operation; never provider error text."""
|
|
for record in reversed(records):
|
|
code = record.get("code")
|
|
if isinstance(code, str) and code in {
|
|
"provider_invocation_failed",
|
|
"model_resolution_failed",
|
|
"empty_response",
|
|
"json_decode_failed",
|
|
"response_schema_invalid",
|
|
}:
|
|
reason = record.get("reason")
|
|
if not isinstance(reason, str) or reason not in {
|
|
"connection_refused",
|
|
"timeout",
|
|
"authentication",
|
|
"rate_limit",
|
|
"service_unavailable",
|
|
}:
|
|
reason = code
|
|
return copy_text(
|
|
BuilderCopyId.OPERATION_MODEL_FAILED, reason=_display_label(_REASON_LABELS, reason, reply_language)
|
|
)
|
|
return fallback
|