mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
218 lines
8.2 KiB
Python
218 lines
8.2 KiB
Python
"""Expand persisted node executions into public terminal and retry trace records."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Mapping, Sequence
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
|
|
from core.app.workflow.retry_history import RETRY_HISTORY_PROCESS_DATA_KEY, WorkflowNodeRetryAttempt
|
|
from libs.helper import to_timestamp
|
|
from models.workflow import WorkflowNodeExecutionModel
|
|
from repositories.api_workflow_node_execution_repository import DifyAPIWorkflowNodeExecutionRepository
|
|
from services.variable_truncator import VariableTruncator
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class WorkflowNodeExecutionTrace(BaseModel):
|
|
"""Read model preserving node-execution identity for terminal and virtual retry traces."""
|
|
|
|
id: str
|
|
tenant_id: str
|
|
app_id: str
|
|
workflow_id: str
|
|
triggered_from: str
|
|
workflow_run_id: str | None = None
|
|
index: int | None = None
|
|
predecessor_node_id: str | None = None
|
|
node_execution_id: str | None = None
|
|
node_id: str | None = None
|
|
node_type: str | None = None
|
|
title: str | None = None
|
|
inputs: Mapping[str, Any] | None = None
|
|
process_data: Mapping[str, Any] | None = None
|
|
outputs: Mapping[str, Any] | None = None
|
|
status: str | None = None
|
|
error: str | None = None
|
|
elapsed_time: float | None = None
|
|
execution_metadata: Mapping[str, Any] | None = None
|
|
extras: Any = None
|
|
created_at: int | None = None
|
|
created_by_role: str | None = None
|
|
created_by: str
|
|
created_by_account: Any = None
|
|
created_by_end_user: Any = None
|
|
finished_at: int | None = None
|
|
inputs_truncated: bool = False
|
|
outputs_truncated: bool = False
|
|
process_data_truncated: bool = False
|
|
retry_index: int | None = None
|
|
|
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
|
|
|
|
def assemble_workflow_node_execution_traces(
|
|
executions: Sequence[WorkflowNodeExecutionModel],
|
|
repository: DifyAPIWorkflowNodeExecutionRepository,
|
|
) -> list[WorkflowNodeExecutionTrace]:
|
|
"""Expand valid persisted retry attempts before each terminal execution."""
|
|
traces: list[WorkflowNodeExecutionTrace] = []
|
|
for execution in executions:
|
|
traces.extend(_expand_execution(execution, repository))
|
|
return traces
|
|
|
|
|
|
def _expand_execution(
|
|
execution: WorkflowNodeExecutionModel,
|
|
repository: DifyAPIWorkflowNodeExecutionRepository,
|
|
) -> list[WorkflowNodeExecutionTrace]:
|
|
full_process_data = _load_full_process_data(execution, repository)
|
|
retry_attempts = _parse_retry_attempts(execution, full_process_data)
|
|
terminal_metadata = execution.execution_metadata_dict
|
|
traces = [_retry_trace(execution, attempt, terminal_metadata) for attempt in retry_attempts]
|
|
traces.append(_terminal_trace(execution))
|
|
return traces
|
|
|
|
|
|
def _load_full_process_data(
|
|
execution: WorkflowNodeExecutionModel,
|
|
repository: DifyAPIWorkflowNodeExecutionRepository,
|
|
) -> Mapping[str, Any] | None:
|
|
try:
|
|
return repository.load_full_process_data(execution)
|
|
except Exception:
|
|
logger.warning(
|
|
"Failed to load full Process Data for workflow run %s node execution %s; using inline preview",
|
|
execution.workflow_run_id,
|
|
execution.id,
|
|
exc_info=True,
|
|
)
|
|
return execution.process_data_dict
|
|
|
|
|
|
def _parse_retry_attempts(
|
|
execution: WorkflowNodeExecutionModel,
|
|
process_data: Mapping[str, Any] | None,
|
|
) -> list[WorkflowNodeRetryAttempt]:
|
|
raw_history = (process_data or {}).get(RETRY_HISTORY_PROCESS_DATA_KEY)
|
|
if not isinstance(raw_history, list):
|
|
return []
|
|
|
|
attempts: list[WorkflowNodeRetryAttempt] = []
|
|
seen_indices: set[int] = set()
|
|
for raw_attempt in raw_history:
|
|
try:
|
|
attempt = WorkflowNodeRetryAttempt.model_validate(raw_attempt)
|
|
except ValidationError:
|
|
logger.warning(
|
|
"Skipping malformed retry history for workflow run %s node execution %s",
|
|
execution.workflow_run_id,
|
|
execution.id,
|
|
exc_info=True,
|
|
)
|
|
continue
|
|
if attempt.retry_index in seen_indices:
|
|
logger.warning(
|
|
"Skipping duplicate retry index %s for workflow run %s node execution %s",
|
|
attempt.retry_index,
|
|
execution.workflow_run_id,
|
|
execution.id,
|
|
)
|
|
continue
|
|
seen_indices.add(attempt.retry_index)
|
|
attempts.append(attempt)
|
|
|
|
return sorted(attempts, key=lambda attempt: attempt.retry_index)
|
|
|
|
|
|
def _retry_trace(
|
|
execution: WorkflowNodeExecutionModel,
|
|
attempt: WorkflowNodeRetryAttempt,
|
|
terminal_metadata: Mapping[str, Any],
|
|
) -> WorkflowNodeExecutionTrace:
|
|
truncator = VariableTruncator.default()
|
|
inputs, inputs_truncated = truncator.truncate_variable_mapping(attempt.inputs)
|
|
process_data, process_data_truncated = truncator.truncate_variable_mapping(attempt.process_data)
|
|
outputs, outputs_truncated = truncator.truncate_variable_mapping(attempt.outputs)
|
|
execution_metadata = {**terminal_metadata, **attempt.execution_metadata}
|
|
return WorkflowNodeExecutionTrace(
|
|
id=f"{execution.id}:retry:{attempt.retry_index}",
|
|
tenant_id=execution.tenant_id,
|
|
app_id=execution.app_id,
|
|
workflow_id=execution.workflow_id,
|
|
triggered_from=_enum_value(execution.triggered_from) or "",
|
|
workflow_run_id=execution.workflow_run_id,
|
|
index=execution.index,
|
|
predecessor_node_id=execution.predecessor_node_id,
|
|
node_execution_id=execution.node_execution_id,
|
|
node_id=execution.node_id,
|
|
node_type=execution.node_type,
|
|
title=execution.title,
|
|
inputs=inputs,
|
|
process_data=process_data,
|
|
outputs=outputs,
|
|
status="retry",
|
|
error=attempt.error,
|
|
elapsed_time=attempt.elapsed_time,
|
|
execution_metadata=execution_metadata,
|
|
extras=execution.extras,
|
|
created_at=attempt.created_at,
|
|
created_by_role=_enum_value(execution.created_by_role),
|
|
created_by=execution.created_by,
|
|
created_by_account=execution.created_by_account,
|
|
created_by_end_user=execution.created_by_end_user,
|
|
finished_at=attempt.finished_at,
|
|
inputs_truncated=inputs_truncated,
|
|
outputs_truncated=outputs_truncated,
|
|
process_data_truncated=process_data_truncated,
|
|
retry_index=attempt.retry_index,
|
|
)
|
|
|
|
|
|
def _terminal_trace(execution: WorkflowNodeExecutionModel) -> WorkflowNodeExecutionTrace:
|
|
process_data = execution.process_data_dict
|
|
if process_data is not None and RETRY_HISTORY_PROCESS_DATA_KEY in process_data:
|
|
process_data = dict(process_data)
|
|
process_data.pop(RETRY_HISTORY_PROCESS_DATA_KEY, None)
|
|
return WorkflowNodeExecutionTrace(
|
|
id=execution.id,
|
|
tenant_id=execution.tenant_id,
|
|
app_id=execution.app_id,
|
|
workflow_id=execution.workflow_id,
|
|
triggered_from=_enum_value(execution.triggered_from) or "",
|
|
workflow_run_id=execution.workflow_run_id,
|
|
index=execution.index,
|
|
predecessor_node_id=execution.predecessor_node_id,
|
|
node_execution_id=execution.node_execution_id,
|
|
node_id=execution.node_id,
|
|
node_type=execution.node_type,
|
|
title=execution.title,
|
|
inputs=execution.inputs_dict,
|
|
process_data=process_data,
|
|
outputs=execution.outputs_dict,
|
|
status=_enum_value(execution.status),
|
|
error=execution.error,
|
|
elapsed_time=execution.elapsed_time,
|
|
execution_metadata=execution.execution_metadata_dict,
|
|
extras=execution.extras,
|
|
created_at=to_timestamp(execution.created_at),
|
|
created_by_role=_enum_value(execution.created_by_role),
|
|
created_by=execution.created_by,
|
|
created_by_account=execution.created_by_account,
|
|
created_by_end_user=execution.created_by_end_user,
|
|
finished_at=to_timestamp(execution.finished_at),
|
|
inputs_truncated=execution.inputs_truncated,
|
|
outputs_truncated=execution.outputs_truncated,
|
|
process_data_truncated=execution.process_data_truncated,
|
|
)
|
|
|
|
|
|
def _enum_value(value: Enum | str | None) -> str | None:
|
|
if value is None or isinstance(value, str):
|
|
return value
|
|
return str(value.value)
|