mirror of
https://github.com/langgenius/dify.git
synced 2026-11-12 14:21:29 +08:00
240 lines
11 KiB
Python
240 lines
11 KiB
Python
"""Pure publication policy and execution presentation; no native graph interpretation."""
|
|
|
|
from core.dify_builder.contract import TestResultCard
|
|
from core.dify_builder.models import PublicationDecision, Run
|
|
from runtime_contracts.builder.copy_ids import BuilderCopyId
|
|
from runtime_contracts.builder.presentation import copy_text, text_parts
|
|
from services.dify_builder.effect_presentation import mocked_note
|
|
from services.dify_builder.execution_messages import FALLBACK, execution_refusal_message
|
|
from services.dify_builder.json_execution_presentation import json_execution_blocks_repair, json_failed_reason
|
|
|
|
REVIEW_NOTE = copy_text(BuilderCopyId.REPLIES_OTHER_PATHS_AND_GOAL_ACCEPTANCE_WERE_NOT_VERIFIED)
|
|
SUCCESS_REPLY = copy_text(BuilderCopyId.LABELS_EXECUTION_SUCCEEDED_OUTPUT_NEEDS_REVIEW)
|
|
|
|
|
|
def is_execution_policy_blocker(run: Run) -> bool:
|
|
"""Known policy denials and incomplete execution proof cannot enter repair."""
|
|
if run.execution_refusal is not None:
|
|
return True
|
|
summary = run.verification.execution_evidence if run.verification else None
|
|
if summary is None:
|
|
return run.kind == "verify"
|
|
return (
|
|
bool(summary.blocked_node_ids)
|
|
or not summary.sealed
|
|
or json_execution_blocks_repair(summary, run.status)
|
|
or summary.safety_outcome
|
|
in {
|
|
"unsupported_safe_execution",
|
|
"execution_blocked",
|
|
"execution_evidence_unknown",
|
|
"simulation_completed",
|
|
"preparation_failed",
|
|
}
|
|
)
|
|
|
|
|
|
def publication_decision(
|
|
run: Run | None, *, session_id: str, current_revision: str, current_graph_revision: str
|
|
) -> PublicationDecision:
|
|
if run is None or run.session_id != session_id or run.kind != "verify" or not run.immutable:
|
|
return PublicationDecision(allowed=False, reason="no_verified_run")
|
|
summary = run.verification.execution_evidence if run.verification else None
|
|
if run.execution_refusal is not None or (summary and summary.blocked_node_ids):
|
|
return PublicationDecision(allowed=False, reason="execution_policy_blocked")
|
|
if summary is None or not summary.sealed or not summary.native_run_id or summary.native_run_id != run.dify_run_id:
|
|
return PublicationDecision(allowed=False, reason="execution_provenance_unbound")
|
|
if summary.safety_outcome in {"unsupported_safe_execution", "execution_blocked"}:
|
|
return PublicationDecision(allowed=False, reason="execution_policy_blocked")
|
|
if summary.safety_outcome == "execution_evidence_unknown":
|
|
return PublicationDecision(allowed=False, reason="execution_evidence_unknown")
|
|
if summary.safety_outcome == "simulation_completed":
|
|
return PublicationDecision(allowed=False, reason="simulation_only")
|
|
# PRD §19.2: a run that mocked any step proves nothing about that step.
|
|
if summary.mode == "confirmed_run" and summary.simulated_node_ids:
|
|
return PublicationDecision(allowed=False, reason="simulation_only")
|
|
restricted_success = summary.safety_outcome == "restricted_execution_completed" and summary.mode == "restricted"
|
|
confirmed_run_success = (
|
|
summary.safety_outcome == "confirmed_run_completed"
|
|
and summary.mode == "confirmed_run"
|
|
and summary.policy_version == "builder-confirmed-run-v1"
|
|
)
|
|
# Confirmation is not publication approval: only the sealed exact-revision
|
|
# confirmed success passes, and every check below still applies.
|
|
confirmed_success = (
|
|
summary.safety_outcome == "confirmed_tool_completed"
|
|
and summary.mode == "confirmed_tool"
|
|
and summary.policy_version == "builder-confirmed-gmail-read-v1"
|
|
and summary.action_state == "completed"
|
|
and summary.consent_id is not None
|
|
)
|
|
if not (restricted_success or confirmed_run_success or confirmed_success):
|
|
return PublicationDecision(allowed=False, reason="execution_failed")
|
|
if run.status == "failed":
|
|
return PublicationDecision(allowed=False, reason="execution_failed")
|
|
if run.status != "succeeded":
|
|
return PublicationDecision(allowed=False, reason="execution_unknown")
|
|
evidence = run.verification
|
|
if (
|
|
evidence is None
|
|
or not evidence.execution_revision
|
|
or not evidence.executed_graph_revision
|
|
or not current_revision
|
|
or not current_graph_revision
|
|
):
|
|
return PublicationDecision(allowed=False, reason="revision_unbound")
|
|
if evidence.execution_revision != current_revision or evidence.executed_graph_revision != current_graph_revision:
|
|
return PublicationDecision(allowed=False, reason="stale_revision")
|
|
if any(f.state == "unresolved" for f in evidence.output_findings):
|
|
return PublicationDecision(allowed=False, reason="required_output_unresolved")
|
|
if evidence.no_output_dead_branch:
|
|
return PublicationDecision(allowed=False, reason="dead_branch_without_output")
|
|
return PublicationDecision(allowed=True, reason="eligible")
|
|
|
|
|
|
def has_output_blocker(run: Run) -> bool:
|
|
evidence = run.verification
|
|
return evidence is not None and (
|
|
evidence.no_output_dead_branch or any(f.state == "unresolved" for f in evidence.output_findings)
|
|
)
|
|
|
|
|
|
CONFIRMED_COMPLETED_NOTE = copy_text(BuilderCopyId.CONFIRMED_COMPLETED_NOTE)
|
|
CONFIRMED_FAILED_REASON = copy_text(BuilderCopyId.CONFIRMED_FAILED)
|
|
CONFIRMED_UNKNOWN_REASON = copy_text(BuilderCopyId.CONFIRMED_UNKNOWN)
|
|
|
|
|
|
def _confirmed_result_card(run: Run) -> TestResultCard:
|
|
"""IDs, statuses, counts, reason codes and the Run link only; never native content or error text."""
|
|
evidence = run.verification
|
|
summary = evidence.execution_evidence if evidence else None
|
|
assert summary is not None
|
|
completed = summary.safety_outcome == "confirmed_tool_completed" and run.status == "succeeded"
|
|
card = TestResultCard(
|
|
status="succeeded" if completed else "failed",
|
|
dify_run_id=run.dify_run_id,
|
|
review_note=CONFIRMED_COMPLETED_NOTE,
|
|
terminal_outputs=None,
|
|
executed_node_ids=list(evidence.executed_node_ids) if evidence else [],
|
|
execution_mode=summary.mode,
|
|
safety_outcome=summary.safety_outcome,
|
|
policy_version=summary.policy_version,
|
|
consent_id=summary.consent_id,
|
|
action_state=summary.action_state,
|
|
action_reason_code=summary.action_reason_code,
|
|
)
|
|
if completed and has_output_blocker(run):
|
|
card.status = "failed"
|
|
card.outcome = "required_output_unresolved"
|
|
card.failure_reason = copy_text(BuilderCopyId.REPLIES_A_REQUIRED_OUTPUT_REFERENCE_WAS_UNRESOLVED)
|
|
elif completed:
|
|
card.outcome = "execution_succeeded_needs_review"
|
|
elif summary.safety_outcome == "native_failed":
|
|
card.outcome = "execution_failed"
|
|
card.failure_reason = CONFIRMED_FAILED_REASON
|
|
else:
|
|
card.outcome = "execution_unknown"
|
|
card.failure_reason = CONFIRMED_UNKNOWN_REASON
|
|
return card
|
|
|
|
|
|
def _project_acceptance(run: Run, card: TestResultCard) -> TestResultCard:
|
|
"""Project typed business state independently of execution and localized prose."""
|
|
finding = run.verification.business_acceptance if run.verification else {}
|
|
if finding:
|
|
from services.dify_builder.business_acceptance import CaseAcceptance
|
|
|
|
try:
|
|
result = CaseAcceptance.model_validate(finding)
|
|
except ValueError:
|
|
card.business_acceptance_state = "unverified"
|
|
else:
|
|
card.business_acceptance_state = result.state
|
|
card.business_acceptance_case_id = result.case_id
|
|
return card
|
|
|
|
|
|
def result_card(run: Run, *, reply_language: str = "en-US") -> TestResultCard:
|
|
evidence = run.verification
|
|
summary = evidence.execution_evidence if evidence else None
|
|
if summary is not None and summary.mode == "confirmed_tool":
|
|
return _project_acceptance(run, _confirmed_result_card(run))
|
|
card = TestResultCard(
|
|
status="succeeded" if run.status == "succeeded" else "failed",
|
|
dify_run_id=run.dify_run_id,
|
|
review_note=REVIEW_NOTE,
|
|
terminal_outputs=evidence.terminal_outputs if evidence else None,
|
|
executed_node_ids=evidence.executed_node_ids if evidence else [],
|
|
)
|
|
_project_acceptance(run, card)
|
|
summary = evidence.execution_evidence if evidence else None
|
|
if summary:
|
|
card.execution_mode = summary.mode
|
|
card.safety_outcome = summary.safety_outcome
|
|
card.policy_version = summary.policy_version
|
|
card.simulated_node_ids = list(summary.simulated_node_ids)
|
|
card.blocked_node_ids = list(summary.blocked_node_ids)
|
|
elif run.execution_refusal:
|
|
card.safety_outcome = run.execution_refusal.safety_outcome
|
|
else:
|
|
card.safety_outcome = "execution_evidence_unknown"
|
|
from services.dify_builder.execution_messages import observed_failure_message
|
|
|
|
observed_failure = observed_failure_message(summary, reply_language=reply_language)
|
|
if observed_failure is not None:
|
|
card.outcome = (
|
|
"execution_failed"
|
|
if summary and summary.safety_outcome in {"preparation_failed", "native_failed"}
|
|
else "execution_unknown"
|
|
)
|
|
card.failure_reason = observed_failure
|
|
card.failure_phase = summary.failure_phase if summary else None
|
|
card.failure_code = summary.failure_reason if summary else None
|
|
card.failure_resource = (
|
|
"/".join(filter(None, summary.failure_resource[1:])) if summary and summary.failure_resource else None
|
|
)
|
|
card.failure_node_ids = list(summary.failure_node_ids) if summary else []
|
|
card.terminal_outputs = None
|
|
return card
|
|
if is_execution_policy_blocker(run):
|
|
failed_reason = json_failed_reason(summary, run.status)
|
|
card.outcome = "execution_failed" if failed_reason else "execution_unknown"
|
|
card.failure_reason = (
|
|
execution_refusal_message(run.execution_refusal.reason_code)
|
|
if run.execution_refusal
|
|
else failed_reason or FALLBACK
|
|
)
|
|
if failed_reason:
|
|
card.terminal_outputs = None
|
|
if run.execution_refusal is not None:
|
|
card.terminal_outputs = None
|
|
if not run.dify_run_id:
|
|
card.executed_node_ids = []
|
|
return card
|
|
if run.status == "succeeded" and has_output_blocker(run):
|
|
card.status = "failed"
|
|
card.outcome = "required_output_unresolved"
|
|
card.failure_reason = copy_text(BuilderCopyId.REPLIES_A_REQUIRED_OUTPUT_REFERENCE_WAS_UNRESOLVED)
|
|
elif run.status == "succeeded":
|
|
card.outcome = "execution_succeeded_needs_review"
|
|
if evidence is None or not evidence.execution_revision:
|
|
card.review_note = copy_text(BuilderCopyId.REPLIES_OTHER_PATHS_AND_GOAL_ACCEPTANCE_WERE)
|
|
mocked = (
|
|
mocked_note(summary.simulated_node_ids, run.per_node)
|
|
if summary is not None and summary.mode == "confirmed_run"
|
|
else None
|
|
)
|
|
if mocked is not None:
|
|
card.review_note = text_parts(card.review_note, " ", mocked) if card.review_note else mocked
|
|
elif run.status == "failed":
|
|
card.outcome = "execution_failed"
|
|
card.failure_reason = run.error or next(
|
|
(n.error for n in run.per_node if n.error), copy_text(BuilderCopyId.LABELS_EXECUTION_FAILED)
|
|
)
|
|
else:
|
|
card.outcome = "execution_unknown"
|
|
card.failure_reason = run.error or copy_text(
|
|
BuilderCopyId.REPLIES_EXECUTION_OUTCOME_IS_UNKNOWN_RUN_AGAIN_BEFORE_PUBLISHING
|
|
)
|
|
return card
|