Files
dify/api/services/dify_builder/execution_messages.py
T

314 lines
15 KiB
Python

"""Finite sanitized execution-refusal copy; no graph/input/error interpolation."""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import replace
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
from services.dify_builder.native_launch_observation import FailureResource
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 bounded semantic 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 step 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", "身份认证失败"),
"permission_denied": ("provider permission denied", "提供商权限被拒绝"),
"credential_unavailable": ("credential missing or inaccessible", "凭据缺失或无权访问"),
"authorization_lookup_failed": ("credential access could not be verified", "无法验证凭据访问权限"),
"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.failure_resource
and summary.failure_reason
in {"authentication", "permission_denied", "credential_unavailable", "authorization_lookup_failed"}
and summary.sealed
and (
summary.safety_outcome == "preparation_failed"
or (summary.safety_outcome == "native_failed" and summary.failure_phase == "execution")
)
):
return _resource_setup_guidance(
reason=summary.failure_reason,
resource=summary.failure_resource,
node_ids=summary.failure_node_ids,
reply_language=reply_language,
)
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 (
"测试准备失败({reason}),工作流尚未开始执行。请检查模型/工具服务和配置,"
"或选择其他工作区资源,再准备并确认一次新测试。"
if reply_language.lower().startswith("zh")
else "Test preparation failed ({reason}) before workflow execution started. "
"Check model/tool services and configuration, or choose another workspace resource; "
"then prepare and confirm a fresh test."
).format(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:
"""Observed resource failure: preserve the Run, avoid speculative graph repair."""
summary = run.verification.execution_evidence if run.verification else None
if (
run.status == "failed"
and not run.dify_run_id
and summary is not None
and summary.sealed
and summary.safety_outcome == "preparation_failed"
and summary.failure_phase == "preparation"
and summary.failure_resource is not None
and bool(summary.failure_node_ids)
and summary.failure_reason in {"credential_unavailable", "authorization_lookup_failed"}
):
return True
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.failure_resource is not None
and bool(summary.failure_node_ids)
and summary.safety_outcome == "native_failed"
and summary.failure_phase == "execution"
and summary.failure_reason in {"service_unavailable", "authentication", "permission_denied"}
)
def _resource_setup_guidance(
*, reason: str, resource: FailureResource, node_ids: tuple[str, ...], reply_language: str
) -> str:
label = _display_label(_REASON_LABELS, reason, reply_language)
resource_label = "/".join(filter(None, resource[1:]))
nodes = ", ".join(node_ids)
if reply_language.lower().startswith("zh"):
recovery = (
"请检查工作区凭据访问权限后重新准备测试。"
if reason == "authorization_lookup_failed"
else "请配置或选择有权访问此资源的凭据,并检查提供商权限后重新准备测试。"
)
return f"资源设置失败:{label}。{recovery}\n{resource_label}\n{nodes}"
recovery = (
"Check workspace credential access before preparing a fresh test."
if reason == "authorization_lookup_failed"
else "Configure or select an authorized credential and check provider permissions "
"before preparing a fresh test."
)
return f"Resource setup failed: {label}. {recovery}\n{resource_label}\n{nodes}"
def sanitize_observed_failure(run: Run) -> Run:
"""Bound Builder errors only; native authorized Run storage keeps original evidence."""
summary = run.verification.execution_evidence if run.verification else None
if (
not native_service_failure(run)
or summary is None
or summary.failure_resource is None
or summary.failure_reason is None
or summary.failure_reason == "service_unavailable"
):
return run
guidance = _resource_setup_guidance(
reason=summary.failure_reason,
resource=summary.failure_resource,
node_ids=summary.failure_node_ids,
reply_language="en",
)
return replace(
run,
error=guidance,
per_node=[
replace(node, error=guidance) if node.node_id in summary.failure_node_ids and node.error else node
for node in run.per_node
],
)
def sanitize_resource_failure_event(
payload: Mapping[str, Any], *, graph: Mapping[str, Any], recognized_errors: dict[str, str]
) -> Mapping[str, Any]:
"""Suppress recognized provider bodies in Builder streams, never seal evidence.
The admitted graph and native stream ID scope presentation only. Terminal
classification still requires the persisted executed graph and native rows.
Code/unattributed errors retain their existing diagnostics.
"""
from core.dify_builder.models import NodeOutput, Run
from services.dify_builder.native_runtime_failure import native_resource_failure
from services.dify_builder.run_mapping import run_id_from_stream_chunk
data = payload.get("data")
native_id = run_id_from_stream_chunk(payload)
if not native_id or not isinstance(data, Mapping) or data.get("status") not in {"failed", "exception"}:
return payload
error = data.get("error")
if not isinstance(error, str) or not error:
return payload
key = f"{native_id}:{error}"
guidance = None
if payload.get("event") == "node_finished" and isinstance(node_id := data.get("node_id"), str):
failure = native_resource_failure(
Run(
status="failed",
dify_run_id=native_id,
per_node=[NodeOutput(node_id=node_id, status="failed", error=error)],
),
graph,
)
if failure is not None and failure[2] in {"authentication", "permission_denied"}:
guidance = _resource_setup_guidance(
reason=failure[2], resource=failure[0], node_ids=failure[1], reply_language="en"
)
recognized_errors[key] = guidance
elif payload.get("event") == "workflow_finished":
guidance = recognized_errors.get(key)
return {**payload, "data": {**data, "error": guidance}} if guidance else payload
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
def capability_assessment_unavailable_message(
records: Sequence[Mapping[str, Any]], *, reply_language: str
) -> BuilderText:
"""Disclose unknown feasibility using only this assessment's observations.
The caller supplies records captured during the assessment invocation,
excluding prior discovery failures. Return fixed guidance and optional
sanitized model-failure copy; never claim an unsupported capability or
alter approval, bindings, requirements or retry policy. No I/O is performed.
"""
guidance = (
"计划可行性检查暂不可用。请在批准前检查计划和资源,或调整计划后重试。"
if reply_language.lower().startswith("zh")
else "The plan feasibility check is unavailable. Review the plan and resources before approving, "
"or adjust the plan to retry."
)
detail = cognition_failure_message(records, "", reply_language=reply_language)
return text_parts(guidance, "\n", detail) if detail else guidance