Skip to content

Commit bd279de

Browse files
committed
fix: The form carried in the loop node cannot retrieve historical data
1 parent 9dc63b6 commit bd279de

8 files changed

Lines changed: 120 additions & 83 deletions

File tree

‎apps/application/serializers/common.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,28 @@ def set_record(self, tool_record):
121121
)
122122

123123

124+
def load_debug_workflow_context(chat_record_id):
125+
"""
126+
按记录 id 解析历史工作流 context:优先 Redis 调试缓存 DEBUG_WORKFLOW_CONTEXT,其次 DB ChatRecord.workflow_context。
127+
属业务层逻辑(依赖 ChatRecord),供基于 ChatRecord 的续跑场景(应用对话、子应用节点)复用,
128+
作为 WorkflowManage.from_context 的 get_context 回调传入;引擎本身不关心 context 来源。
129+
"""
130+
try:
131+
cache_key = Cache_Version.DEBUG_WORKFLOW_CONTEXT.get_key(chat_record_id=str(chat_record_id))
132+
context_data = cache.get(cache_key)
133+
if not context_data:
134+
chat_record = ChatRecord.objects.filter(id=chat_record_id).first()
135+
if not chat_record or not chat_record.workflow_context:
136+
return None
137+
context_data = chat_record.workflow_context
138+
return context_data
139+
except Exception:
140+
import traceback
141+
142+
traceback.print_exc()
143+
return None
144+
145+
124146
def resolve_chat_user(chat_user_id, chat_user_type, asker=None):
125147
"""
126148
根据对话用户 id / 类型解析出对话用户信息。

‎apps/application/workflow/i_node.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
import time
11+
import traceback
1112
from enum import Enum
1213
from typing import Optional, Type, Callable
1314

@@ -99,6 +100,7 @@ def run(self):
99100
except CancelledException:
100101
self.complete(Status.CANCELLED)
101102
except Exception as e:
103+
traceback.print_exc()
102104
self.complete(Status.FAIL, error=e)
103105

104106
def _run(self):
Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# coding=utf-8
22
"""
3-
@project: MaxKB
4-
@Author:虎虎虎
5-
@file: loop_workflow_manage.py
6-
@date:2026/7/2 10:00
7-
@desc:
3+
@project: MaxKB
4+
@Author:虎虎虎
5+
@file: loop_workflow_manage.py
6+
@date:2026/7/2 10:00
7+
@desc:
88
"""
9+
910
from typing import Dict, Optional, Callable
1011

1112
from application.workflow.common import Workflow, WorkflowType, Node
@@ -14,21 +15,20 @@
1415

1516

1617
class LoopWorkFlowManage(WorkflowManage):
17-
18-
def __init__(self,
19-
workflow: Workflow,
20-
parameters: Dict,
21-
workflow_type: WorkflowType,
22-
call_back: CallBack,
23-
get_start_node: Callable[[Workflow, WorkflowManage], INode],
24-
parent_workflow_manage: WorkflowManage,
25-
loop_context: Dict = None):
18+
def __init__(
19+
self,
20+
workflow: Workflow,
21+
parameters: Dict,
22+
workflow_type: WorkflowType,
23+
call_back: CallBack,
24+
get_start_node: Callable[[Workflow, WorkflowManage], INode],
25+
parent_workflow_manage: WorkflowManage,
26+
):
2627
self.parent_workflow_manage = parent_workflow_manage
27-
self.loop_context = loop_context or {}
2828
super().__init__(workflow, parameters, workflow_type, call_back, get_start_node)
2929

3030
def get_parameters(self):
31-
return {**self.parameters, **self.loop_context}
31+
return self.parameters
3232

3333
def get_parent_context(self, node_id, key):
3434
return self.parent_workflow_manage.get_context(node_id, key)
@@ -38,7 +38,8 @@ def generate_prompt(self, prompt):
3838
prompt = self.parent_workflow_manage.workflow.reset_prompt(prompt)
3939
context = {**self.context, **self.parent_workflow_manage.context}
4040
from langchain_core.prompts import PromptTemplate
41-
prompt_template = PromptTemplate.from_template(prompt, template_format='jinja2')
41+
42+
prompt_template = PromptTemplate.from_template(prompt, template_format="jinja2")
4243
return prompt_template.format(context=context)
4344

4445
def get_reference_field(self, node_id, fields):
@@ -55,3 +56,28 @@ def get_reference_field(self, node_id, fields):
5556

5657
# 从父工作流获取
5758
return self.parent_workflow_manage.get_reference_field(node_id, fields)
59+
60+
@classmethod
61+
def from_context(
62+
cls, get_context, workflow, parameters, workflow_type, call_back, get_start_node, parent_workflow_manage=None
63+
):
64+
try:
65+
context = get_context()
66+
67+
instance = cls(
68+
workflow=workflow,
69+
parameters=parameters,
70+
workflow_type=workflow_type,
71+
call_back=call_back,
72+
get_start_node=get_start_node,
73+
parent_workflow_manage=parent_workflow_manage,
74+
)
75+
if context:
76+
instance.context = context
77+
78+
return instance
79+
except Exception:
80+
import traceback
81+
82+
traceback.print_exc()
83+
return None

‎apps/application/workflow/nodes/application_node/application_node.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,10 @@ def get_start_node_fn(wf, wm):
213213

214214
# 表单提交:从历史 context 恢复子应用;否则全新运行
215215
if is_submit:
216+
from application.serializers.common import load_debug_workflow_context
217+
216218
sub_manage = WorkflowManage.from_context(
217-
chat_record_id=sub_chat_record_id,
219+
get_context=lambda: load_debug_workflow_context(sub_chat_record_id),
218220
workflow=sub_workflow,
219221
parameters=sub_parameters,
220222
workflow_type=WorkflowType.APPLICATION,

‎apps/application/workflow/nodes/loop_node/loop_node.py‎

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,8 @@
1414

1515
from application.workflow.common import WorkflowType, new_instance
1616
from application.workflow.i_node import INode, Signal
17-
18-
from application.workflow.message.struct.content import NodeInfo, Position
19-
from application.workflow.message.struct.text_content import TextContent
17+
from application.workflow.message.struct.content import Position
2018
from application.workflow.status import Status
21-
2219
from common.exception.app_exception import AppApiException
2320

2421
MAX_LOOP_COUNT = 500
@@ -61,6 +58,8 @@ class LoopNode(INode):
6158
serializer_class = LoopNodeSerializer
6259
supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
6360
type = "loop-node"
61+
_workflow_params = None
62+
_iterator = None
6463

6564
def _run(self):
6665
self.execute()
@@ -81,7 +80,7 @@ def execute(self):
8180
if loop_type == "ARRAY" and isinstance(array, list) and len(array) >= 2:
8281
array = self.workflow_manage.get_reference_field(array[0], array[1:])
8382

84-
self.write_context("params", {"loop_type": loop_type, "array": array, "number": number})
83+
self.data["params"] = {"loop_type": loop_type, "array": array, "number": number}
8584

8685
# 根据 start_index 构建迭代器
8786
if loop_type == "ARRAY":
@@ -90,12 +89,8 @@ def execute(self):
9089
iterator = _generate_while_loop(number or MAX_LOOP_COUNT, start_index=start_index)
9190
else:
9291
iterator = _generate_loop_number(number, start_index=start_index)
93-
94-
self._loop_node_data = self.get_context("loop_node_data") or []
95-
self._loop_answer_data = self.get_context("loop_answer_data") or []
96-
self._answer_text = self.get_context("answer") or ""
9792
self._workflow_params = workflow_params
98-
self._loop_body = loop_body
93+
self.data["loop_body"] = loop_body
9994
self._iterator = iterator
10095

10196
self._run_next()
@@ -104,35 +99,31 @@ def _run_next(self):
10499
try:
105100
item, index = next(self._iterator)
106101
except StopIteration:
107-
self.write_context("answer", self._answer_text)
108-
self.write_context("run_time", time.time() - self.data.get("start_time", time.time()))
102+
self.data["run_time"] = time.time() - self.data.get("start_time", time.time())
109103
self.complete(Status.SUCCESS)
110104
return
111-
loop_context = {"index": index, "item": item}
112-
workflow = new_instance(self._loop_body, self.get_workflow_type())
105+
workflow = new_instance(self.data["loop_body"], self.get_workflow_type())
113106

114107
chunk_list = []
115108

116109
def on_next(wf_manage, content):
117110
chunk_list.append(content)
118-
if hasattr(content, "content"):
119-
self._answer_text += content.content
120111
content.position = Position(self.get_node_id(), index, content.position)
121112
self.write(content)
122113

123114
def on_complete(wf_manage, error):
124115
loop_details_list = self.data.setdefault("loop_details_list", [])
125116
loop_details_list.append(wf_manage.get_details())
126-
self._loop_node_data.append(wf_manage.context)
127-
self._loop_answer_data.append([c.to_dict() for c in chunk_list])
128-
self.write_context("loop_node_data", self._loop_node_data)
129-
self.write_context("loop_answer_data", self._loop_answer_data)
130117
self.write_context("index", index)
131118
self.write_context("item", item)
119+
last_context = self.workflow_manage.get_context(self.node.id, "last_context")
120+
if last_context:
121+
self.write_context("last_context", {**last_context, **wf_manage.context})
122+
else:
123+
self.write_context("last_context", wf_manage.context)
132124

133125
if wf_manage.signal == Signal.BREAK or wf_manage.signal == Signal.FORM:
134-
self.write_context("answer", self._answer_text)
135-
self.write_context("run_time", time.time() - self.data.get("start_time", time.time()))
126+
self.data["run_time"] = time.time() - self.data.get("start_time", time.time())
136127
self.complete(Status.SUCCESS)
137128
return
138129

@@ -173,40 +164,41 @@ def get_start_node_fn(wf, wf_manage):
173164
start_node = wf.get_node("loop-start-node")
174165
return loop_start_class(start_node, wf_manage, lambda n: n.properties.get("node_data", {}))
175166

167+
def get_context():
168+
last_context = self.workflow_manage.get_context(self.node.id, "last_context") or {}
169+
if last_context:
170+
return last_context
171+
return {}
172+
176173
# 构建子工作流参数,第一次迭代传入 child_position
177174
loop_workflow_params = dict(self._workflow_params)
178175
if child_position:
179176
loop_workflow_params["position"] = child_position
180177
else:
181178
loop_workflow_params.pop("position", None)
182-
183-
loop_manage = LoopWorkFlowManage(
179+
loop_workflow_params["index"] = index
180+
loop_workflow_params["item"] = item
181+
loop_manage = LoopWorkFlowManage.from_context(
184182
workflow=workflow,
185183
parameters=loop_workflow_params,
186184
workflow_type=self.get_workflow_type(),
187185
call_back=call_back,
188186
get_start_node=get_start_node_fn,
189187
parent_workflow_manage=self.workflow_manage,
190-
loop_context=loop_context,
188+
get_context=get_context,
191189
)
192-
loop_manage.start_node.workflow_manage = loop_manage
193190
loop_manage.run()
194191

195192
def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
196193
details = super().get_details(index, position, old_details, **kwargs)
197194
details.update(
198-
{
199-
"params": self.get_context("params"),
200-
"index": self.get_context("index"),
201-
"item": self.get_context("item"),
202-
"answer": self.get_context("answer"),
203-
}
195+
{"params": self.data.get("params"), "index": self.get_context("index"), "item": self.get_context("item")}
204196
)
205197
loop_details = []
206198
position_index = 0
207199
loop_position_index = 0
208200
if old_details and position:
209-
for index, item in enumerate(old_details.get("iteration_details") or []):
201+
for index, item in enumerate(old_details.get("children") or []):
210202
loop_position_index = index
211203
loop_details.append(item)
212204
current_details = loop_details[loop_position_index]

‎apps/application/workflow/nodes/loop_start_node/loop_start_node.py‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,18 @@ class LoopStartNode(INode):
2323
type = "loop-start-node"
2424

2525
def execute(self):
26-
loop_context = getattr(self.workflow_manage, "loop_context", {})
27-
index = loop_context.get("index", 0)
28-
item = loop_context.get("item", None)
29-
30-
self.write_context("index", index)
31-
self.write_context("item", item)
26+
loop = self.workflow_manage.context.get("loop")
27+
if loop is None:
28+
self.write_context("loop", {})
29+
parameters = self.workflow_manage.get_parameters()
30+
if parameters is not None:
31+
index = parameters.get("index", 0)
32+
item = parameters.get("item", 0)
33+
self.write_context("index", index)
34+
self.write_context("item", item)
35+
else:
36+
self.write_context("index", 0)
37+
self.write_context("item", 0)
3238

3339
def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
3440
details = super().get_details(index, position, old_details, **kwargs)

‎apps/application/workflow/workflow_manage.py‎

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -246,40 +246,27 @@ def get_reference_field(self, node_id, fields):
246246
return obj
247247

248248
@classmethod
249-
def from_context(cls, chat_record_id, workflow, parameters, workflow_type, call_back, get_start_node):
250-
"""从历史 context 恢复 WorkflowManage"""
251-
from application.models import ChatRecord
252-
from django.core.cache import cache
253-
from common.constants.cache_version import Cache_Version
254-
249+
def from_context(cls, get_context, workflow, parameters, workflow_type, call_back, get_start_node):
250+
"""
251+
恢复 WorkflowManage:调用 get_context() 拿到历史 context 并用它重建实例。
252+
context 从何而来(DB、缓存或其它)由调用方通过 get_context 决定,引擎不关心其业务来源;
253+
get_context 返回空或抛异常则返回 None,调用方可据此回退为全新执行。
254+
"""
255255
try:
256-
context_data = None
257-
258-
# 先从 Redis 查(调试模式)
259-
cache_key = Cache_Version.DEBUG_WORKFLOW_CONTEXT.get_key(chat_record_id=str(chat_record_id))
260-
context_data = cache.get(cache_key)
261-
262-
# Redis 没有,从数据库查
263-
if not context_data:
264-
chat_record = ChatRecord.objects.filter(id=chat_record_id).first()
265-
if not chat_record or not chat_record.workflow_context:
266-
return None
267-
context_data = chat_record.workflow_context
268-
269-
# 创建 WorkflowManage 实例
256+
context = get_context()
257+
if not context:
258+
return None
270259
instance = cls(
271260
workflow=workflow,
272261
parameters=parameters,
273262
workflow_type=workflow_type,
274263
call_back=call_back,
275264
get_start_node=get_start_node,
276265
)
277-
278266
# 恢复全局 context
279-
instance.context = context_data
280-
267+
instance.context = context
281268
return instance
282-
except Exception as e:
269+
except Exception:
283270
import traceback
284271

285272
traceback.print_exc()

‎apps/chat/serializers/chat.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
)
3737
from application.serializers.application import ApplicationOperateSerializer
3838
from application.serializers.application_chat import ChatCountSerializer
39-
from application.serializers.common import resolve_chat_user, resolve_chat_user_group
39+
from application.serializers.common import load_debug_workflow_context, resolve_chat_user, resolve_chat_user_group
4040
from chat.serializers.chat_history import ChatHistory
4141
from application.workflow.common import WorkflowType, new_instance
4242
from application.workflow.message.aggregator import AggregationManager
@@ -356,7 +356,7 @@ def get_start_node_fn(wf, wm):
356356
# Form 提交(有 position 和 chat_record_id):从历史 context 恢复
357357
if position and chat_record_id:
358358
work_flow_manage = WorkflowManage.from_context(
359-
chat_record_id=chat_record_id,
359+
get_context=lambda: load_debug_workflow_context(chat_record_id),
360360
workflow=workflow,
361361
parameters=parameters,
362362
workflow_type=WorkflowType.APPLICATION,

0 commit comments

Comments
 (0)