Repository navigation
fix: Optimize knowledge base; AI-driven generation of tools and workflows #7920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| import threading | ||
|
|
||
| from django.test import SimpleTestCase | ||
|
|
||
| from application.workflow.common import WorkflowType, new_instance | ||
| from application.workflow.nodes import get_node_class | ||
| from application.workflow.nodes.loop_node.loop_node import LoopNodeSerializer | ||
| from application.workflow.workflow_manage import CallBack, WorkflowManage | ||
| from common.exception.app_exception import AppApiException | ||
|
|
||
|
|
||
| def _node(node_id, node_type, node_data=None, **properties): | ||
| return { | ||
| "id": node_id, | ||
| "type": node_type, | ||
| "x": 0, | ||
| "y": 0, | ||
| "properties": {"stepName": node_id, "node_data": node_data or {}, **properties}, | ||
| } | ||
|
|
||
|
|
||
| def _edge(source, target): | ||
| return { | ||
| "id": f"{source}-{target}", | ||
| "type": "app-edge", | ||
| "sourceNodeId": source, | ||
| "targetNodeId": target, | ||
| "sourceAnchorId": f"{source}_right", | ||
| "targetAnchorId": f"{target}_left", | ||
| } | ||
|
|
||
|
|
||
| def _loop_start(*variables): | ||
| return _node( | ||
| "loop-start-node", | ||
| "loop-start-node", | ||
| stepName="循环开始", | ||
| config={"fields": [{"label": "下标", "value": "index"}, {"label": "循环元素", "value": "item"}]}, | ||
| loop_input_field_list=[{"field": field, "label": field} for field in variables], | ||
| ) | ||
|
|
||
|
|
||
| def _assign(node_id, *variables): | ||
| return _node( | ||
| node_id, | ||
| "variable-assign-node", | ||
| { | ||
| "variable_list": [ | ||
| {"name": ".".join(fields), "fields": fields, "source": "custom", "type": "string", "value": value} | ||
| for fields, value in variables | ||
| ] | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| def _loop(node_id, number, body_nodes, body_edges): | ||
| return _node( | ||
| node_id, | ||
| "loop-node", | ||
| {"loop_type": "NUMBER", "number": number, "loop_body": {"nodes": body_nodes, "edges": body_edges}}, | ||
| ) | ||
|
|
||
|
|
||
| def _run(loop_node): | ||
| """以循环节点作为起始节点跑完一个根工作流,返回根 WorkflowManage""" | ||
| workflow = new_instance({"nodes": [loop_node], "edges": []}) | ||
| finished = threading.Event() | ||
| errors = [] | ||
|
|
||
| def on_complete(_manage, error): | ||
| errors.append(error) | ||
| finished.set() | ||
|
|
||
| def get_start_node(wf, manage): | ||
| node = wf.get_node(loop_node["id"]) | ||
| node_class = get_node_class(node.type, WorkflowType.APPLICATION) | ||
| return node_class(node, manage, lambda n: n.properties.get("node_data", {})) | ||
|
|
||
| def on_next(wf_manage, _content): | ||
| # 知识库工作流每收到一块输出就取一次详情,第一轮循环尚未结束时也必须可用 | ||
| wf_manage.get_details() | ||
|
|
||
| manage = WorkflowManage(workflow, {}, WorkflowType.APPLICATION, CallBack(on_next, on_complete), get_start_node) | ||
| manage.run() | ||
| assert finished.wait(10), "workflow did not finish" | ||
| assert errors == [None], errors | ||
| return manage | ||
|
|
||
|
|
||
| class LoopVariableTests(SimpleTestCase): | ||
| def test_loop_variable_is_shared_across_iterations_and_readable_after_the_loop(self): | ||
| loop = _loop( | ||
| "loop1", | ||
| 3, | ||
| [ | ||
| _loop_start("total"), | ||
| _assign( | ||
| "assign", | ||
| (["loop", "total"], "{{loop.total}}{{循环开始.index}}"), | ||
| (["global", "last"], "{{循环变量.total}}"), | ||
| ), | ||
| ], | ||
| [_edge("loop-start-node", "assign")], | ||
| ) | ||
|
|
||
| manage = _run(loop) | ||
|
|
||
| # 循环变量存放在循环节点的上下文里,下游用 [循环节点ID, 变量名] 引用 | ||
| self.assertEqual(manage.get_reference_field("loop1", ["total"]), "012") | ||
| # 循环体内给全局变量赋值,写到根工作流 | ||
| self.assertEqual(manage.get_reference_field("global", ["last"]), "012") | ||
| self.assertNotIn("loop", manage.context["loop1"]["last_context"]) | ||
| self.assertNotIn("global", manage.context["loop1"]["last_context"]) | ||
|
|
||
| def test_nested_loop_writes_reach_the_owning_loop_and_the_root_workflow(self): | ||
| inner = _loop( | ||
| "inner", | ||
| 2, | ||
| [ | ||
| _loop_start("cnt"), | ||
| _assign( | ||
| "assign", | ||
| (["loop", "acc"], "{{loop.acc}}x"), | ||
| (["loop", "cnt"], "{{loop.cnt}}y"), | ||
| (["global", "g"], "{{loop.acc}}|{{loop.cnt}}"), | ||
| ), | ||
| ], | ||
| [_edge("loop-start-node", "assign")], | ||
| ) | ||
| outer = _loop("outer", 2, [_loop_start("acc"), inner], [_edge("loop-start-node", "inner")]) | ||
|
|
||
| manage = _run(outer) | ||
|
|
||
| # 内层循环修改外层循环声明的变量 | ||
| self.assertEqual(manage.get_reference_field("outer", ["acc"]), "xxxx") | ||
| # 内层循环自己的变量存放在外层循环体的上下文里,且每次进入内层循环都从空开始 | ||
| self.assertEqual(manage.context["outer"]["last_context"]["inner"]["cnt"], "yy") | ||
| # 两层嵌套里给全局变量赋值,依然写到根工作流 | ||
| self.assertEqual(manage.context["global"], {"g": "xxxx|yy"}) | ||
| self.assertNotIn("global", manage.context["outer"]["last_context"]) | ||
|
|
||
| def test_loop_variable_cannot_use_a_name_reserved_by_the_loop_node(self): | ||
| data = {"loop_type": "NUMBER", "number": 1, "loop_body": {"nodes": [_loop_start("last_context")], "edges": []}} | ||
|
|
||
| with self.assertRaises(AppApiException): | ||
| LoopNodeSerializer(data=data).is_valid(raise_exception=True) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are the issues and suggestions:
Correctness / Robustness
_runasserts fail inside a worker thread.assert errors == [None], errorsruns on the main thread afterfinished.wait(10), but ifon_completeis invoked with an exception,threading.Event.set()is still called — fine — however any assertion raised inon_complete/on_next(executed in the workflow thread) will be swallowed and only surface as a timeout, making failures hard to diagnose. Consider capturingsys.exc_info()in the callback and re-raising on the main thread.finished.wait(10)timeout is hard-coded and short. On slow CI machines nested loops can exceed 10s. Make it configurable (env var) or larger.Mutable default avoided via
node_data or {}— good, but note that passing an empty dict{}asnode_datawill be replaced by a fresh dict; this is intentional but subtle. Fine._assignsignature is fragile. It accepts(fields_list, value)pairs positionally; a typo like passing a string instead of a list will silently produce wrong field names (".".join("ab")). Add a guard/validation in the helper.on_nextcallswf_manage.get_details()but discards the result. This looks like it's meant to exercise detail retrieval mid-iteration; but if it's just for coverage, add anasserton its shape or a comment clarifying intent. As written it may hide regressions.errors.append(error)thenassert errors == [None]— order/single-element assumption. If callbacks can fire multiple times (e.g., partial + final), this will break. Assert onall(e is None ...)instead.get_start_nodeclosure readsloop_node["id"]from the outer scope. It works because_runonly ever handles one loop node, but the parameterwf/manageshadows expectations. Fine, but consider naming it_get_start_nodeor documenting that it ignores the passedloop_node.Test asserts on internal Chinese literals and exact concatenation order (
"012","xxxx|yy"). These are implementation-coupled: if iteration order or string concatenation semantics change, tests break even if behavior is still correct. Consider asserting divisibility/count properties where possible.Missing negative cases: no test for
WorkflowManagerunning simultaneously)AppApiExceptionself.assertNotIn("loop", manage.context["loop1"]["last_context"])and the analogous"global"checks rely on the exact key namelast_context. That's a stable contract? If not, extract the constant.Duplication between tests. The inner-loop body construction is nearly identical; a helper
_inner_loop_body(...)would reduce drift._loop_start(*variables)exposes bothindex/itemand per-variable fields, which is presumably a schema requirement, but the test doesn't clarify why both forms are used. Add a short docstring/comments.Style / Minor
No assertions that
on_nextactually fired — if the workflow emits zero outputs, the "detail available mid-loop" contract isn't tested. Track a counter and assert > 0.threading.Event+ list is not a race-free error channel — appending to a list from a worker and reading afterEvent.set()is safe in CPython due to GIL, but document it or use aqueue.Queue."NUMBER"literal appears three times; pull into a constant or reference the enum used by production code.x: 0, y: 0in_nodeare UI-only fields; irrelevant to the engine — fine but could be omitted if the schema allows.Error message
"workflow did not finish"— good; consider including elapsed time and last-seen state for debugging.Optimization (minor): the tests assume synchronous execution inside the workflow thread; if the engine ever parallelizes loop bodies, tests 13 & 6 need revisiting. Flag this assumption with a comment near
_run.