mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
240 lines
8.0 KiB
Python
240 lines
8.0 KiB
Python
import threading
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from typing import TypedDict
|
|
|
|
import contexts
|
|
from core.workflow.nodes.human_input.pause_reason import HumanInputRequired
|
|
from graphon.enums import WorkflowExecutionStatus
|
|
from libs.infinite_scroll_pagination import InfiniteScrollPagination
|
|
from machinery.context import RequestContext
|
|
from models import (
|
|
WorkflowRun,
|
|
WorkflowRunTriggeredFrom,
|
|
)
|
|
from repositories.api_workflow_node_execution_repository import DifyAPIWorkflowNodeExecutionRepository
|
|
from repositories.sqlalchemy_api_workflow_run_repository import DifyAPISQLAlchemyWorkflowRunRepository
|
|
from services.workflow_node_execution_trace_service import (
|
|
WorkflowNodeExecutionTrace,
|
|
assemble_workflow_node_execution_traces,
|
|
)
|
|
|
|
|
|
class WorkflowRunListArgs(TypedDict, total=False):
|
|
"""Expected shape of the args dict passed to workflow run pagination methods."""
|
|
|
|
limit: int
|
|
last_id: str
|
|
status: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class WorkflowRunPausedNode:
|
|
node_id: str
|
|
node_title: str
|
|
form_id: str
|
|
form_token: str | None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class WorkflowRunPauseDetails:
|
|
paused_at: datetime | None
|
|
paused_nodes: tuple[WorkflowRunPausedNode, ...]
|
|
|
|
|
|
class WorkflowRunService:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
workflow_runs: DifyAPISQLAlchemyWorkflowRunRepository,
|
|
node_executions: DifyAPIWorkflowNodeExecutionRepository,
|
|
) -> None:
|
|
self._workflow_runs = workflow_runs
|
|
self._node_executions = node_executions
|
|
|
|
def get_paginate_advanced_chat_workflow_runs(
|
|
self,
|
|
context: RequestContext,
|
|
*,
|
|
app_id: str,
|
|
args: WorkflowRunListArgs,
|
|
triggered_from: WorkflowRunTriggeredFrom = WorkflowRunTriggeredFrom.DEBUGGING,
|
|
) -> InfiniteScrollPagination:
|
|
"""
|
|
Get advanced chat app workflow run list
|
|
|
|
:param context: admitted Console request context
|
|
:param app_id: app id
|
|
:param args: request args
|
|
:param triggered_from: workflow run triggered from (default: DEBUGGING for preview runs)
|
|
"""
|
|
|
|
class WorkflowWithMessage:
|
|
message_id: str
|
|
conversation_id: str
|
|
|
|
def __init__(self, workflow_run: WorkflowRun):
|
|
self._workflow_run = workflow_run
|
|
|
|
def __getattr__(self, item):
|
|
return getattr(self._workflow_run, item)
|
|
|
|
pagination = self.get_paginate_workflow_runs(
|
|
context,
|
|
app_id=app_id,
|
|
args=args,
|
|
triggered_from=triggered_from,
|
|
)
|
|
|
|
# Batch-load the associated Message for every run in a single query to avoid
|
|
# an N+1 pattern: the deprecated WorkflowRun.message property issues one query
|
|
# per run. The filter matches that property exactly (app_id + workflow_run_id).
|
|
workflow_runs = pagination.data
|
|
run_ids = [workflow_run.id for workflow_run in workflow_runs]
|
|
messages_by_run_id = self._workflow_runs.get_message_refs(
|
|
app_id=app_id,
|
|
workflow_run_ids=run_ids,
|
|
)
|
|
|
|
with_message_workflow_runs = []
|
|
for workflow_run in workflow_runs:
|
|
message = messages_by_run_id.get(workflow_run.id)
|
|
with_message_workflow_run = WorkflowWithMessage(workflow_run=workflow_run)
|
|
if message:
|
|
with_message_workflow_run.message_id = message.message_id
|
|
with_message_workflow_run.conversation_id = message.conversation_id
|
|
|
|
with_message_workflow_runs.append(with_message_workflow_run)
|
|
|
|
pagination.data = with_message_workflow_runs
|
|
return pagination
|
|
|
|
def get_paginate_workflow_runs(
|
|
self,
|
|
context: RequestContext,
|
|
*,
|
|
app_id: str,
|
|
args: WorkflowRunListArgs,
|
|
triggered_from: WorkflowRunTriggeredFrom = WorkflowRunTriggeredFrom.DEBUGGING,
|
|
) -> InfiniteScrollPagination:
|
|
"""
|
|
Get workflow run list
|
|
|
|
:param context: admitted Console request context
|
|
:param app_id: app id
|
|
:param args: request args
|
|
:param triggered_from: workflow run triggered from (default: DEBUGGING)
|
|
"""
|
|
limit = int(args.get("limit", 20))
|
|
last_id = args.get("last_id")
|
|
status = args.get("status")
|
|
|
|
return self._workflow_runs.get_paginated_workflow_runs(
|
|
tenant_id=context.active_workspace_id,
|
|
app_id=app_id,
|
|
triggered_from=triggered_from,
|
|
limit=limit,
|
|
last_id=last_id,
|
|
status=status,
|
|
)
|
|
|
|
def get_workflow_run(self, context: RequestContext, *, app_id: str, run_id: str) -> WorkflowRun | None:
|
|
"""
|
|
Get workflow run detail
|
|
|
|
:param context: admitted Console request context
|
|
:param app_id: app id
|
|
:param run_id: workflow run id
|
|
"""
|
|
return self._workflow_runs.get_workflow_run_by_id(
|
|
tenant_id=context.active_workspace_id,
|
|
app_id=app_id,
|
|
run_id=run_id,
|
|
)
|
|
|
|
def get_workflow_runs_count(
|
|
self,
|
|
context: RequestContext,
|
|
*,
|
|
app_id: str,
|
|
status: str | None = None,
|
|
time_range: str | None = None,
|
|
triggered_from: WorkflowRunTriggeredFrom = WorkflowRunTriggeredFrom.DEBUGGING,
|
|
) -> dict[str, int]:
|
|
"""
|
|
Get workflow runs count statistics
|
|
|
|
:param context: admitted Console request context
|
|
:param app_id: app id
|
|
:param status: optional status filter
|
|
:param time_range: optional time range filter (e.g., "7d", "4h", "30m", "30s")
|
|
:param triggered_from: workflow run triggered from (default: DEBUGGING)
|
|
:return: dict with total and status counts
|
|
"""
|
|
return self._workflow_runs.get_workflow_runs_count(
|
|
tenant_id=context.active_workspace_id,
|
|
app_id=app_id,
|
|
triggered_from=triggered_from,
|
|
status=status,
|
|
time_range=time_range,
|
|
)
|
|
|
|
def get_workflow_run_node_executions(
|
|
self,
|
|
context: RequestContext,
|
|
*,
|
|
app_id: str,
|
|
run_id: str,
|
|
) -> list[WorkflowNodeExecutionTrace]:
|
|
"""
|
|
Get workflow run node execution list
|
|
"""
|
|
workflow_run = self.get_workflow_run(context, app_id=app_id, run_id=run_id)
|
|
|
|
contexts.plugin_tool_providers.set({})
|
|
contexts.plugin_tool_providers_lock.set(threading.Lock())
|
|
|
|
if not workflow_run:
|
|
return []
|
|
|
|
node_executions = self._node_executions.get_executions_by_workflow_run(
|
|
tenant_id=context.active_workspace_id,
|
|
app_id=app_id,
|
|
workflow_run_id=run_id,
|
|
)
|
|
return assemble_workflow_node_execution_traces(node_executions, self._node_executions)
|
|
|
|
def get_pause_details(
|
|
self,
|
|
context: RequestContext,
|
|
*,
|
|
workflow_run_id: str,
|
|
) -> WorkflowRunPauseDetails | None:
|
|
pause_record = self._workflow_runs.get_pause_record(
|
|
workspace_id=context.active_workspace_id,
|
|
workflow_run_id=workflow_run_id,
|
|
)
|
|
if pause_record is None:
|
|
return None
|
|
if pause_record.status != WorkflowExecutionStatus.PAUSED:
|
|
return WorkflowRunPauseDetails(paused_at=None, paused_nodes=())
|
|
|
|
human_input_reasons: list[HumanInputRequired] = []
|
|
for reason in pause_record.reasons:
|
|
if not isinstance(reason, HumanInputRequired):
|
|
raise NotImplementedError(f"Pause details do not support {type(reason).__name__}")
|
|
human_input_reasons.append(reason)
|
|
|
|
return WorkflowRunPauseDetails(
|
|
paused_at=pause_record.paused_at,
|
|
paused_nodes=tuple(
|
|
WorkflowRunPausedNode(
|
|
node_id=reason.node_id,
|
|
node_title=reason.node_title,
|
|
form_id=reason.form_id,
|
|
form_token=pause_record.form_tokens.get(reason.form_id),
|
|
)
|
|
for reason in human_input_reasons
|
|
),
|
|
)
|