1414
1515from configs import dify_config
1616from core .app .entities .app_invoke_entities import InvokeFrom
17+ from core .workflow .node_execution_process_data import WORKFLOW_AGENT_BINDING_ID_KEY
1718from graphon .enums import WorkflowNodeExecutionStatus
1819from libs .helper import convert_datetime_to_date , escape_like_pattern , to_timestamp
1920from models .agent import WorkflowAgentNodeBinding
2021from models .enums import CreatorUserRole , FeedbackFromSource , FeedbackRating , MessageStatus
2122from models .model import App , Conversation , Message , MessageFeedback
22- from models .workflow import WorkflowNodeExecutionModel , WorkflowRun , WorkflowType
23+ from models .workflow import (
24+ Workflow ,
25+ WorkflowNodeExecutionModel ,
26+ WorkflowNodeExecutionTriggeredFrom ,
27+ WorkflowRun ,
28+ WorkflowType ,
29+ )
2330
2431
2532@dataclass (frozen = True )
@@ -326,17 +333,15 @@ def _list_workflow_conversation_logs(
326333 and_ (
327334 WorkflowAgentNodeBinding .tenant_id == app .tenant_id ,
328335 WorkflowAgentNodeBinding .agent_id == agent_id ,
329- WorkflowAgentNodeBinding .app_id == WorkflowRun .app_id ,
330- WorkflowAgentNodeBinding .workflow_id == WorkflowRun .workflow_id ,
331- WorkflowAgentNodeBinding .workflow_version == WorkflowRun .version ,
336+ self ._workflow_node_binding_condition (),
332337 ),
333338 )
334- .join (workflow_app , workflow_app .id == WorkflowAgentNodeBinding .app_id )
339+ .join (
340+ workflow_app ,
341+ and_ (workflow_app .id == WorkflowAgentNodeBinding .app_id , workflow_app .tenant_id == app .tenant_id ),
342+ )
335343 .where (
336344 WorkflowNodeExecutionModel .tenant_id == app .tenant_id ,
337- WorkflowNodeExecutionModel .app_id == WorkflowAgentNodeBinding .app_id ,
338- WorkflowNodeExecutionModel .workflow_id == WorkflowAgentNodeBinding .workflow_id ,
339- WorkflowNodeExecutionModel .node_id == WorkflowAgentNodeBinding .node_id ,
340345 )
341346 )
342347 stmt = self ._apply_workflow_node_filters (stmt , params = params , workflow_app = workflow_app )
@@ -443,18 +448,16 @@ def _list_workflow_messages(
443448 and_ (
444449 WorkflowAgentNodeBinding .tenant_id == app .tenant_id ,
445450 WorkflowAgentNodeBinding .agent_id == agent_id ,
446- WorkflowAgentNodeBinding .app_id == WorkflowRun .app_id ,
447- WorkflowAgentNodeBinding .workflow_id == WorkflowRun .workflow_id ,
448- WorkflowAgentNodeBinding .workflow_version == WorkflowRun .version ,
451+ self ._workflow_node_binding_condition (),
449452 ),
450453 )
451- .join (workflow_app , workflow_app .id == WorkflowAgentNodeBinding .app_id )
454+ .join (
455+ workflow_app ,
456+ and_ (workflow_app .id == WorkflowAgentNodeBinding .app_id , workflow_app .tenant_id == app .tenant_id ),
457+ )
452458 .where (
453459 WorkflowNodeExecutionModel .id == conversation_id ,
454460 WorkflowNodeExecutionModel .tenant_id == app .tenant_id ,
455- WorkflowNodeExecutionModel .app_id == WorkflowAgentNodeBinding .app_id ,
456- WorkflowNodeExecutionModel .workflow_id == WorkflowAgentNodeBinding .workflow_id ,
457- WorkflowNodeExecutionModel .node_id == WorkflowAgentNodeBinding .node_id ,
458461 )
459462 )
460463 stmt = self ._apply_workflow_node_filters (stmt , params = params , workflow_app = workflow_app )
@@ -466,6 +469,65 @@ def _list_workflow_messages(
466469 )
467470 return [self .serialize_workflow_node_message (execution ) for execution in executions ]
468471
472+ def _workflow_node_binding_condition (self , * , statistics : bool = False ) -> sa .ColumnElement [bool ]:
473+ """Resolve source ownership independently of a Tool's outer lifecycle run."""
474+ node = aliased (WorkflowNodeExecutionModel , name = "wne" ) if statistics else WorkflowNodeExecutionModel
475+ binding = aliased (WorkflowAgentNodeBinding , name = "wanb" ) if statistics else WorkflowAgentNodeBinding
476+ run = aliased (WorkflowRun , name = "wr" ) if statistics else WorkflowRun
477+ process_data_json = func .nullif (node .process_data , "" )
478+ if self ._session .get_bind ().dialect .name == "sqlite" :
479+ binding_id = func .json_extract (process_data_json , f"$.{ WORKFLOW_AGENT_BINDING_ID_KEY } " )
480+ else :
481+ binding_id = sa .cast (process_data_json , sa .JSON )[WORKFLOW_AGENT_BINDING_ID_KEY ].as_string ()
482+
483+ node_table = "wne" if statistics else WorkflowNodeExecutionModel .__tablename__
484+ source_version = (
485+ select (Workflow .version )
486+ .where (
487+ Workflow .id == sa .literal_column (f"{ node_table } .workflow_id" ),
488+ Workflow .app_id == sa .literal_column (f"{ node_table } .app_id" ),
489+ Workflow .tenant_id == sa .literal_column (f"{ node_table } .tenant_id" ),
490+ )
491+ .scalar_subquery ()
492+ )
493+ old_binding = aliased (WorkflowAgentNodeBinding , name = "old_binding" )
494+ single_binding_version = (
495+ select (func .min (old_binding .workflow_version ))
496+ .where (
497+ old_binding .tenant_id == sa .literal_column (f"{ node_table } .tenant_id" ),
498+ old_binding .app_id == sa .literal_column (f"{ node_table } .app_id" ),
499+ old_binding .workflow_id == sa .literal_column (f"{ node_table } .workflow_id" ),
500+ old_binding .node_id == sa .literal_column (f"{ node_table } .node_id" ),
501+ )
502+ .having (func .count () == 1 )
503+ .scalar_subquery ()
504+ )
505+ is_tool = node .triggered_from == WorkflowNodeExecutionTriggeredFrom .WORKFLOW_TOOL
506+ # Old Tool rows may outlive their source Workflow. A sole source binding
507+ # is still unambiguous; never choose among versions using the caller run.
508+ old_version = sa .case ((is_tool , func .coalesce (source_version , single_binding_version )), else_ = run .version )
509+ return and_ (
510+ node .tenant_id == run .tenant_id ,
511+ binding .tenant_id == node .tenant_id ,
512+ binding .app_id == node .app_id ,
513+ binding .workflow_id == node .workflow_id ,
514+ binding .node_id == node .node_id ,
515+ or_ (is_tool , and_ (node .app_id == run .app_id , node .workflow_id == run .workflow_id )),
516+ or_ (
517+ sa .cast (binding .id , sa .String ) == binding_id ,
518+ and_ (binding_id .is_ (None ), binding .workflow_version == old_version ),
519+ ),
520+ )
521+
522+ def _statistics_workflow_node_binding_join_sql (self ) -> str :
523+ # Reuse the log/detail owner predicate in both raw-SQL statistics queries.
524+ return str (
525+ self ._workflow_node_binding_condition (statistics = True ).compile (
526+ dialect = self ._session .get_bind ().dialect ,
527+ compile_kwargs = {"literal_binds" : True },
528+ )
529+ )
530+
469531 def _list_workflow_sources (self , * , app : App , agent_id : str ) -> list [dict [str , Any ]]:
470532 workflow_app = aliased (App )
471533 stmt = (
@@ -947,6 +1009,7 @@ def _load_workflow_daily_statistics(
9471009 ("agent_log" , "agent_backend" , "usage" , "completion_tokens" ), "BIGINT"
9481010 )
9491011 binding_filters = self ._statistics_workflow_binding_filters_sql (source_filter )
1012+ binding_join = self ._statistics_workflow_node_binding_join_sql ()
9501013 run_date_filters = ""
9511014 args : dict [str , Any ] = {
9521015 "tz" : params .timezone ,
@@ -981,17 +1044,14 @@ def _load_workflow_daily_statistics(
9811044 COALESCE(SUM(COALESCE(wne.elapsed_time, 0)), 0) AS latency,
9821045 COALESCE(SUM(COALESCE({ completion_tokens } , 0)), 0) AS answer_tokens
9831046 FROM workflow_runs wr
1047+ JOIN workflow_node_executions wne
1048+ ON wne.workflow_run_id = wr.id
9841049 JOIN workflow_agent_node_bindings wanb
985- ON wanb.tenant_id = :tenant_id
1050+ ON { binding_join }
9861051 AND wanb.agent_id = :agent_id
987- AND wanb.app_id = wr.app_id
988- AND wanb.workflow_id = wr.workflow_id
989- AND wanb.workflow_version = wr.version
9901052 { binding_filters }
991- JOIN workflow_node_executions wne
992- ON wne.workflow_run_id = wr.id
993- AND wne.node_id = wanb.node_id
994- WHERE wr.type != :chat_workflow_type{ run_date_filters }
1053+ JOIN apps source_app ON source_app.id = wanb.app_id AND source_app.tenant_id = wr.tenant_id
1054+ WHERE wr.tenant_id = :tenant_id AND wr.type != :chat_workflow_type{ run_date_filters }
9951055 GROUP BY wr.id, wr.created_by_role, wr.created_by, wr.created_at
9961056)
9971057SELECT
@@ -1085,24 +1145,23 @@ def _statistics_workflow_binding_filters_sql(source_filter: AgentSourceFilter) -
10851145 workflow_binding_filters .append ("wanb.node_id = :node_id" )
10861146 return f"AND { ' AND ' .join (workflow_binding_filters )} " if workflow_binding_filters else ""
10871147
1088- @ classmethod
1089- def _statistics_workflow_message_scope_sql ( cls , source_filter : AgentSourceFilter ) -> str :
1090- binding_filters = cls . _statistics_workflow_binding_filters_sql ( source_filter )
1148+ def _statistics_workflow_message_scope_sql ( self , source_filter : AgentSourceFilter ) -> str :
1149+ binding_filters = self . _statistics_workflow_binding_filters_sql ( source_filter )
1150+ binding_join = self . _statistics_workflow_node_binding_join_sql ( )
10911151 return f"""m.workflow_run_id IS NOT NULL
10921152 AND EXISTS (
10931153 SELECT 1
10941154 FROM workflow_runs wr
1155+ JOIN workflow_node_executions wne
1156+ ON wne.workflow_run_id = wr.id
10951157 JOIN workflow_agent_node_bindings wanb
1096- ON wanb.tenant_id = :tenant_id
1158+ ON { binding_join }
10971159 AND wanb.agent_id = :agent_id
1098- AND wanb.app_id = wr.app_id
1099- AND wanb.workflow_id = wr.workflow_id
1100- AND wanb.workflow_version = wr.version
11011160 { binding_filters }
1102- JOIN workflow_node_executions wne
1103- ON wne.workflow_run_id = wr.id
1104- AND wne.node_id = wanb.node_id
1161+ JOIN apps source_app ON source_app.id = wanb.app_id AND source_app.tenant_id = wr.tenant_id
11051162 WHERE wr.id = m.workflow_run_id
1163+ AND wr.app_id = m.app_id
1164+ AND wr.tenant_id = :tenant_id
11061165 AND wr.type = :chat_workflow_type
11071166 )"""
11081167
0 commit comments