diff --git a/apps/application/workflow/compare/__init__.py b/apps/application/workflow/compare/__init__.py new file mode 100644 index 00000000000..ce0c430e1ad --- /dev/null +++ b/apps/application/workflow/compare/__init__.py @@ -0,0 +1,83 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2024/6/7 14:43 + @desc: +""" +from typing import List + +from .contain_compare import ContainCompare +from .end_with import EndWithCompare +from .equal_compare import EqualCompare +from .ge_compare import GECompare +from .gt_compare import GTCompare +from .is_not_null_compare import IsNotNullCompare +from .is_not_true import IsNotTrueCompare +from .is_null_compare import IsNullCompare +from .is_true import IsTrueCompare +from .le_compare import LECompare +from .len_equal_compare import LenEqualCompare +from .len_ge_compare import LenGECompare +from .len_gt_compare import LenGTCompare +from .len_le_compare import LenLECompare +from .len_lt_compare import LenLTCompare +from .lt_compare import LTCompare +from .not_contain_compare import NotContainCompare +from .not_equal_compare import NotEqualCompare +from .regex_compare import RegexCompare +from .start_with import StartWithCompare +from .wildcard_compare import WildcardCompare + +_compare_handler_dict = { + 'is_null': IsNullCompare(), + 'is_not_null': IsNotNullCompare(), + 'contain': ContainCompare(), + 'not_contain': NotContainCompare(), + 'eq': EqualCompare(), + 'not_eq': NotEqualCompare(), + 'ge': GECompare(), + 'gt': GTCompare(), + 'le': LECompare(), + 'lt': LTCompare(), + 'len_eq': LenEqualCompare(), + 'len_ge': LenGECompare(), + 'len_gt': LenGTCompare(), + 'len_le': LenLECompare(), + 'len_lt': LenLTCompare(), + 'is_true': IsTrueCompare(), + 'is_not_true': IsNotTrueCompare(), + 'start_with': StartWithCompare(), + 'end_with': EndWithCompare(), + 'regex': RegexCompare(), + 'wildcard': WildcardCompare(), +} + + +def _compare(source_value, compare, target_value): + compare_handler = _compare_handler_dict.get(compare) + if compare_handler is None: + raise RuntimeError(f"Unknown compare handler '{compare}'") + return compare_handler.compare(source_value, compare, target_value) + + +def _assertion(workflow_manage, field_list: List[str], compare: str, value): + try: + value = workflow_manage.generate_prompt(value) + except Exception: + pass + field_value = None + try: + field_value = workflow_manage.get_reference_field(field_list[0], field_list[1:]) + except Exception: + pass + return _compare(field_value, compare, value) + + +def do_assertion(workflow_manage, condition, condition_list): + b = False if condition == 'and' else True + for row in condition_list: + if _assertion(workflow_manage, row.get('field'), row.get('compare'), row.get('value')) is b: + return b + return not b diff --git a/apps/application/workflow/compare/compare.py b/apps/application/workflow/compare/compare.py new file mode 100644 index 00000000000..62eb4a7b910 --- /dev/null +++ b/apps/application/workflow/compare/compare.py @@ -0,0 +1,15 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: compare.py + @date:2024/6/7 14:37 + @desc: +""" +from abc import abstractmethod + +class Compare: + + @abstractmethod + def compare(self, source_value, compare, target_value): + pass diff --git a/apps/application/workflow/compare/contain_compare.py b/apps/application/workflow/compare/contain_compare.py new file mode 100644 index 00000000000..cd50d543a3a --- /dev/null +++ b/apps/application/workflow/compare/contain_compare.py @@ -0,0 +1,25 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: contain_compare.py + @date:2024/6/11 10:02 + @desc: +""" +from .compare import Compare + + +class ContainCompare(Compare): + + def compare(self, source_value, compare, target_value): + target_value = str(target_value) + + if isinstance(source_value, str): + return target_value in source_value + elif isinstance(source_value, list): + for item in source_value: + if str(item) == target_value: + return True + return False + else: + return target_value in str(source_value) diff --git a/apps/application/workflow/compare/end_with.py b/apps/application/workflow/compare/end_with.py new file mode 100644 index 00000000000..eae7e3a8a15 --- /dev/null +++ b/apps/application/workflow/compare/end_with.py @@ -0,0 +1,16 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: start_with.py + @date:2025/10/20 10:37 + @desc: +""" +from .compare import Compare + + +class EndWithCompare(Compare): + + def compare(self, source_value, compare, target_value): + source_value = str(source_value) + return source_value.endswith(str(target_value)) diff --git a/apps/application/workflow/compare/equal_compare.py b/apps/application/workflow/compare/equal_compare.py new file mode 100644 index 00000000000..dad0cffa9fa --- /dev/null +++ b/apps/application/workflow/compare/equal_compare.py @@ -0,0 +1,15 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: equal_compare.py + @date:2024/6/7 14:44 + @desc: +""" +from .compare import Compare + + +class EqualCompare(Compare): + + def compare(self, source_value, compare, target_value): + return str(source_value) == str(target_value) diff --git a/apps/application/workflow/compare/ge_compare.py b/apps/application/workflow/compare/ge_compare.py new file mode 100644 index 00000000000..e1cf2e7aac0 --- /dev/null +++ b/apps/application/workflow/compare/ge_compare.py @@ -0,0 +1,25 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: lt_compare.py + @date:2024/6/11 9:52 + @desc: 大于比较器 +""" +from .compare import Compare + + +class GECompare(Compare): + + def compare(self, source_value, compare, target_value): + if source_value is None: + return target_value is None + + try: + return float(source_value) >= float(target_value) + except Exception: + try: + return str(source_value) >= str(target_value) + except Exception: + pass + return False diff --git a/apps/application/workflow/compare/gt_compare.py b/apps/application/workflow/compare/gt_compare.py new file mode 100644 index 00000000000..fab86c4bc8c --- /dev/null +++ b/apps/application/workflow/compare/gt_compare.py @@ -0,0 +1,25 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: lt_compare.py + @date:2024/6/11 9:52 + @desc: 大于比较器 +""" +from .compare import Compare + + +class GTCompare(Compare): + + def compare(self, source_value, compare, target_value): + if source_value is None: + return False + + try: + return float(source_value) > float(target_value) + except Exception: + try: + return str(source_value) > str(target_value) + except Exception: + pass + return False diff --git a/apps/application/workflow/compare/is_not_null_compare.py b/apps/application/workflow/compare/is_not_null_compare.py new file mode 100644 index 00000000000..37fd4a72ea4 --- /dev/null +++ b/apps/application/workflow/compare/is_not_null_compare.py @@ -0,0 +1,18 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: is_not_null_compare.py + @date:2024/6/28 10:45 + @desc: +""" +from .compare import Compare + + +class IsNotNullCompare(Compare): + + def compare(self, source_value, compare, target_value): + try: + return source_value is not None and len(source_value) > 0 + except Exception: + return True diff --git a/apps/application/workflow/compare/is_not_true.py b/apps/application/workflow/compare/is_not_true.py new file mode 100644 index 00000000000..fabeec2cc41 --- /dev/null +++ b/apps/application/workflow/compare/is_not_true.py @@ -0,0 +1,18 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: is_not_true.py + @date:2025/4/7 13:44 + @desc: +""" +from .compare import Compare + + +class IsNotTrueCompare(Compare): + + def compare(self, source_value, compare, target_value): + try: + return source_value is False + except Exception: + return False diff --git a/apps/application/workflow/compare/is_null_compare.py b/apps/application/workflow/compare/is_null_compare.py new file mode 100644 index 00000000000..240403ea605 --- /dev/null +++ b/apps/application/workflow/compare/is_null_compare.py @@ -0,0 +1,18 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: is_null_compare.py + @date:2024/6/28 10:45 + @desc: +""" +from .compare import Compare + + +class IsNullCompare(Compare): + + def compare(self, source_value, compare, target_value): + try: + return source_value is None or len(source_value) == 0 + except Exception: + return False diff --git a/apps/application/workflow/compare/is_true.py b/apps/application/workflow/compare/is_true.py new file mode 100644 index 00000000000..8cb4a45a2a5 --- /dev/null +++ b/apps/application/workflow/compare/is_true.py @@ -0,0 +1,18 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: IsTrue.py + @date:2025/4/7 13:38 + @desc: +""" +from .compare import Compare + + +class IsTrueCompare(Compare): + + def compare(self, source_value, compare, target_value): + try: + return source_value is True + except Exception: + return False diff --git a/apps/application/workflow/compare/le_compare.py b/apps/application/workflow/compare/le_compare.py new file mode 100644 index 00000000000..0ebdb394857 --- /dev/null +++ b/apps/application/workflow/compare/le_compare.py @@ -0,0 +1,25 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: lt_compare.py + @date:2024/6/11 9:52 + @desc: 小于比较器 +""" +from .compare import Compare + + +class LECompare(Compare): + + def compare(self, source_value, compare, target_value): + if source_value is None: + return target_value is None + + try: + return float(source_value) <= float(target_value) + except Exception: + try: + return str(source_value) <= str(target_value) + except Exception: + pass + return False diff --git a/apps/application/workflow/compare/len_equal_compare.py b/apps/application/workflow/compare/len_equal_compare.py new file mode 100644 index 00000000000..98a5314a292 --- /dev/null +++ b/apps/application/workflow/compare/len_equal_compare.py @@ -0,0 +1,18 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: equal_compare.py + @date:2024/6/7 14:44 + @desc: +""" +from .compare import Compare + + +class LenEqualCompare(Compare): + + def compare(self, source_value, compare, target_value): + try: + return len(source_value) == int(target_value) + except Exception as e: + return False diff --git a/apps/application/workflow/compare/len_ge_compare.py b/apps/application/workflow/compare/len_ge_compare.py new file mode 100644 index 00000000000..06dd566cf24 --- /dev/null +++ b/apps/application/workflow/compare/len_ge_compare.py @@ -0,0 +1,18 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: lt_compare.py + @date:2024/6/11 9:52 + @desc: 大于比较器 +""" +from .compare import Compare + + +class LenGECompare(Compare): + + def compare(self, source_value, compare, target_value): + try: + return len(source_value) >= int(target_value) + except Exception: + return False diff --git a/apps/application/workflow/compare/len_gt_compare.py b/apps/application/workflow/compare/len_gt_compare.py new file mode 100644 index 00000000000..fae2668e0ba --- /dev/null +++ b/apps/application/workflow/compare/len_gt_compare.py @@ -0,0 +1,18 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: lt_compare.py + @date:2024/6/11 9:52 + @desc: 大于比较器 +""" +from .compare import Compare + + +class LenGTCompare(Compare): + + def compare(self, source_value, compare, target_value): + try: + return len(source_value) > int(target_value) + except Exception: + return False diff --git a/apps/application/workflow/compare/len_le_compare.py b/apps/application/workflow/compare/len_le_compare.py new file mode 100644 index 00000000000..41b9ee9f709 --- /dev/null +++ b/apps/application/workflow/compare/len_le_compare.py @@ -0,0 +1,18 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: lt_compare.py + @date:2024/6/11 9:52 + @desc: 小于比较器 +""" +from .compare import Compare + + +class LenLECompare(Compare): + + def compare(self, source_value, compare, target_value): + try: + return len(source_value) <= int(target_value) + except Exception: + return False diff --git a/apps/application/workflow/compare/len_lt_compare.py b/apps/application/workflow/compare/len_lt_compare.py new file mode 100644 index 00000000000..4a9b11654ca --- /dev/null +++ b/apps/application/workflow/compare/len_lt_compare.py @@ -0,0 +1,18 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: lt_compare.py + @date:2024/6/11 9:52 + @desc: 小于比较器 +""" +from .compare import Compare + + +class LenLTCompare(Compare): + + def compare(self, source_value, compare, target_value): + try: + return len(source_value) < int(target_value) + except Exception: + return False diff --git a/apps/application/workflow/compare/lt_compare.py b/apps/application/workflow/compare/lt_compare.py new file mode 100644 index 00000000000..ecf8d549bfd --- /dev/null +++ b/apps/application/workflow/compare/lt_compare.py @@ -0,0 +1,25 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: lt_compare.py + @date:2024/6/11 9:52 + @desc: 小于比较器 +""" +from .compare import Compare + + +class LTCompare(Compare): + + def compare(self, source_value, compare, target_value): + if source_value is None: + return False + + try: + return float(source_value) < float(target_value) + except Exception: + try: + return str(source_value) < str(target_value) + except Exception: + pass + return False diff --git a/apps/application/workflow/compare/not_contain_compare.py b/apps/application/workflow/compare/not_contain_compare.py new file mode 100644 index 00000000000..99194e70364 --- /dev/null +++ b/apps/application/workflow/compare/not_contain_compare.py @@ -0,0 +1,25 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: contain_compare.py + @date:2024/6/11 10:02 + @desc: +""" +from .compare import Compare + + +class NotContainCompare(Compare): + + def compare(self, source_value, compare, target_value): + target_value = str(target_value) + + if isinstance(source_value, str): + return target_value not in source_value + elif isinstance(source_value, list): + for item in source_value: + if str(item) == target_value: + return False + return True + else: + return target_value not in str(source_value) diff --git a/apps/application/workflow/compare/not_equal_compare.py b/apps/application/workflow/compare/not_equal_compare.py new file mode 100644 index 00000000000..f53057ebfa9 --- /dev/null +++ b/apps/application/workflow/compare/not_equal_compare.py @@ -0,0 +1,15 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:wangliang181230 + @file: not_equal_compare.py + @date:2026/3/17 9:41 + @desc: +""" +from .compare import Compare + + +class NotEqualCompare(Compare): + + def compare(self, source_value, compare, target_value): + return str(source_value) != str(target_value) diff --git a/apps/application/workflow/compare/regex_compare.py b/apps/application/workflow/compare/regex_compare.py new file mode 100644 index 00000000000..613300e6589 --- /dev/null +++ b/apps/application/workflow/compare/regex_compare.py @@ -0,0 +1,35 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:wangliang181230 + @file: regex_compare.py + @date:2026/3/30 12:11 + @desc: +""" +import re + +from .compare import Compare +from common.cache.mem_cache import MemCache + +match_cache = MemCache('regex', { + 'TIMEOUT': 3600, # 缓存有效期为 1 小时 + 'OPTIONS': { + 'MAX_ENTRIES': 500, # 最多缓存 500 个条目 + 'CULL_FREQUENCY': 10, # 达到上限时,删除约 1/10 的缓存 + }, +}) + + +def compile_and_cache(regex): + match = match_cache.get(regex) + if not match: + match = re.compile(regex).fullmatch + match_cache.set(regex, match) + return match + + +class RegexCompare(Compare): + + def compare(self, source_value, compare, target_value): + match = compile_and_cache(str(target_value)) + return bool(match(str(source_value))) diff --git a/apps/application/workflow/compare/start_with.py b/apps/application/workflow/compare/start_with.py new file mode 100644 index 00000000000..054ea9bd6cb --- /dev/null +++ b/apps/application/workflow/compare/start_with.py @@ -0,0 +1,16 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: start_with.py + @date:2025/10/20 10:37 + @desc: +""" +from .compare import Compare + + +class StartWithCompare(Compare): + + def compare(self, source_value, compare, target_value): + source_value = str(source_value) + return source_value.startswith(str(target_value)) diff --git a/apps/application/workflow/compare/wildcard_compare.py b/apps/application/workflow/compare/wildcard_compare.py new file mode 100644 index 00000000000..43c903a9360 --- /dev/null +++ b/apps/application/workflow/compare/wildcard_compare.py @@ -0,0 +1,38 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:wangliang181230 + @file: wildcard_compare.py + @date:2026/3/30 12:11 + @desc: +""" +import fnmatch +import re + +from .compare import Compare +from common.cache.mem_cache import MemCache + + +match_cache = MemCache('wildcard_to_regex', { + 'TIMEOUT': 3600, # 缓存有效期为 1 小时 + 'OPTIONS': { + 'MAX_ENTRIES': 500, # 最多缓存 500 个条目 + 'CULL_FREQUENCY': 10, # 达到上限时,删除约 1/10 的缓存 + }, +}) + + +def translate_and_compile_and_cache(wildcard): + match = match_cache.get(wildcard) + if not match: + regex = fnmatch.translate(wildcard) + match = re.compile(regex).match + match_cache.set(wildcard, match) + return match + +class WildcardCompare(Compare): + + def compare(self, source_value, compare, target_value): + # 转成正则,性能更高 + match = translate_and_compile_and_cache(str(target_value)) + return bool(match(str(source_value))) diff --git a/apps/application/workflow/nodes/condition_node/__init__.py b/apps/application/workflow/nodes/condition_node/__init__.py new file mode 100644 index 00000000000..c1c44f4f7ca --- /dev/null +++ b/apps/application/workflow/nodes/condition_node/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎虎 + @file: __init__.py + @date:2026/7/2 10:00 + @desc: +""" +from .condition_node import ConditionNode diff --git a/apps/application/workflow/nodes/condition_node/condition_node.py b/apps/application/workflow/nodes/condition_node/condition_node.py new file mode 100644 index 00000000000..5738b0adf2c --- /dev/null +++ b/apps/application/workflow/nodes/condition_node/condition_node.py @@ -0,0 +1,65 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎虎 + @file: condition_node.py + @date:2026/7/2 10:00 + @desc: +""" +from typing import List + +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from application.workflow.compare import do_assertion +from application.workflow.common import WorkflowType +from application.workflow.i_node import INode +from application.workflow.status import Status + + +class ConditionSerializer(serializers.Serializer): + compare = serializers.CharField(required=True, label=_("Comparator")) + value = serializers.CharField(required=True, label=_("value")) + field = serializers.ListField(required=True, label=_("Fields")) + + +class ConditionBranchSerializer(serializers.Serializer): + id = serializers.CharField(required=True, label=_("Branch id")) + type = serializers.CharField(required=True, label=_("Branch Type")) + condition = serializers.CharField(required=True, label=_("Condition or|and")) + conditions = ConditionSerializer(many=True) + + +class ConditionNodeSerializer(serializers.Serializer): + branch = ConditionBranchSerializer(many=True) + + +class ConditionNode(INode): + serializer_class = ConditionNodeSerializer + supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL] + type = 'condition-node' + + def execute(self): + node_params = self.get_parameters() + branch_list = node_params.get('branch', []) + branch = self._evaluate_branches(branch_list) + branch_id = branch.get('id') + branch_name = branch.get('type') + + self.write_context('branch_id', branch_id) + self.write_context('branch_name', branch_name) + + self.complete(Status.SUCCESS, [self.branch_anchor(branch_id)]) + + def _evaluate_branches(self, branch_list: List): + for branch in branch_list: + if self._branch_assertion(branch): + return branch + return branch_list[-1] if branch_list else {} + + def _branch_assertion(self, branch): + return do_assertion( + self.workflow_manage, + branch.get('condition'), + branch.get('conditions') + ) diff --git a/apps/application/workflow/workflow_manage.py b/apps/application/workflow/workflow_manage.py index b70b720e069..70f4461a1d2 100644 --- a/apps/application/workflow/workflow_manage.py +++ b/apps/application/workflow/workflow_manage.py @@ -13,8 +13,8 @@ from langchain_core.prompts import PromptTemplate -from application.flow.i_step_node import INode from application.workflow.common import Workflow, WorkflowType, Node, get_node_parameters +from application.workflow.i_node import INode from application.workflow.message.struct.content import Content from application.workflow.nodes import get_node_class from application.workflow.status import Status