Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-20) — DMN-Style Decision Tables](#whats-new-2026-06-20--dmn-style-decision-tables)
- [What's new (2026-06-20) — Saga / Compensating Rollback](#whats-new-2026-06-20--saga--compensating-rollback)
- [What's new (2026-06-20) — JSONPath Querying](#whats-new-2026-06-20--jsonpath-querying)
- [What's new (2026-06-20) — Multi-Channel Webhook Notifications](#whats-new-2026-06-20--multi-channel-webhook-notifications)
Expand Down Expand Up @@ -105,6 +106,12 @@

---

## What's new (2026-06-20) — DMN-Style Decision Tables

Externalize branching into reviewable rule tables. Full reference: [`docs/source/Eng/doc/new_features/v53_features_doc.rst`](docs/source/Eng/doc/new_features/v53_features_doc.rst).

- **`evaluate_table` / `DecisionTable`** (`AC_decision_table`, `ac_decision_table`): replaces nested `AC_if_var` chains with rows of `conditions -> outputs` and a hit policy (`UNIQUE`/`FIRST`/`PRIORITY`/`COLLECT`). Cell conditions are wildcard / literal / `{op, value}` using the executor's standard comparators (reused, not duplicated). Pure-stdlib, fully testable; the DMN way to keep business rules data-driven.

## What's new (2026-06-20) — Saga / Compensating Rollback

Undo completed steps when a later one fails. Full reference: [`docs/source/Eng/doc/new_features/v52_features_doc.rst`](docs/source/Eng/doc/new_features/v52_features_doc.rst).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-20) — DMN 式决策表](#本次更新-2026-06-20--dmn-式决策表)
- [本次更新 (2026-06-20) — Saga / 补偿回滚](#本次更新-2026-06-20--saga--补偿回滚)
- [本次更新 (2026-06-20) — JSONPath 查询](#本次更新-2026-06-20--jsonpath-查询)
- [本次更新 (2026-06-20) — 多通道 Webhook 通知](#本次更新-2026-06-20--多通道-webhook-通知)
Expand Down Expand Up @@ -104,6 +105,12 @@

---

## 本次更新 (2026-06-20) — DMN 式决策表

将分支外部化为可审查的规则表。完整参考:[`docs/source/Zh/doc/new_features/v53_features_doc.rst`](../docs/source/Zh/doc/new_features/v53_features_doc.rst)。

- **`evaluate_table` / `DecisionTable`**(`AC_decision_table`、`ac_decision_table`):以一列列的 `conditions -> outputs` 加命中政策(`UNIQUE`/`FIRST`/`PRIORITY`/`COLLECT`)取代嵌套 `AC_if_var` 链。单元格条件为通配符 / 字面值 / `{op, value}`,使用执行器标准比较子(重用,不重复)。纯标准库、可完整测试;DMN 让业务规则数据驱动的方式。

## 本次更新 (2026-06-20) — Saga / 补偿回滚

后续步骤失败时回滚已完成步骤。完整参考:[`docs/source/Zh/doc/new_features/v52_features_doc.rst`](../docs/source/Zh/doc/new_features/v52_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-20) — DMN 式決策表](#本次更新-2026-06-20--dmn-式決策表)
- [本次更新 (2026-06-20) — Saga / 補償回溯](#本次更新-2026-06-20--saga--補償回溯)
- [本次更新 (2026-06-20) — JSONPath 查詢](#本次更新-2026-06-20--jsonpath-查詢)
- [本次更新 (2026-06-20) — 多通道 Webhook 通知](#本次更新-2026-06-20--多通道-webhook-通知)
Expand Down Expand Up @@ -104,6 +105,12 @@

---

## 本次更新 (2026-06-20) — DMN 式決策表

將分支外部化為可審查的規則表。完整參考:[`docs/source/Zh/doc/new_features/v53_features_doc.rst`](../docs/source/Zh/doc/new_features/v53_features_doc.rst)。

- **`evaluate_table` / `DecisionTable`**(`AC_decision_table`、`ac_decision_table`):以一列列的 `conditions -> outputs` 加命中政策(`UNIQUE`/`FIRST`/`PRIORITY`/`COLLECT`)取代巢狀 `AC_if_var` 鏈。儲存格條件為萬用字元 / 字面值 / `{op, value}`,使用執行器標準比較子(重用,不重複)。純標準函式庫、可完整測試;DMN 讓商業規則資料驅動的方式。

## 本次更新 (2026-06-20) — Saga / 補償回溯

後續步驟失敗時復原已完成步驟。完整參考:[`docs/source/Zh/doc/new_features/v52_features_doc.rst`](../docs/source/Zh/doc/new_features/v52_features_doc.rst)。
Expand Down
55 changes: 55 additions & 0 deletions docs/source/Eng/doc/new_features/v53_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
DMN-Style Decision Tables
=========================

Nested ``AC_if_var`` chains get unreadable fast. A decision table externalizes
branching into rows of ``conditions -> outputs`` evaluated by a **hit policy** —
the DMN way to keep business rules data-driven and reviewable.

Each cell condition is a wildcard (``None`` / ``"-"`` / ``"*"``), a literal
(equality), or ``{"op": "ge", "value": 18}`` using the project's standard
comparators (``eq/ne/lt/le/gt/ge/contains/startswith/endswith`` — reused from the
executor, not duplicated). Pure standard library; imports no ``PySide6``.

Hit policies
------------

================ ===================================================
Policy Behavior
================ ===================================================
``UNIQUE`` Exactly one rule may match (raises if more do).
``FIRST`` The first matching rule wins.
``PRIORITY`` Same as FIRST — first match in rule order.
``COLLECT`` All matching rules' outputs (a list).
================ ===================================================

Headless API
------------

.. code-block:: python

from je_auto_control import evaluate_table

spec = {
"inputs": ["age", "country"],
"hit_policy": "FIRST",
"rules": [
{"conditions": {"age": {"op": "lt", "value": 18}},
"outputs": {"tier": "minor"}},
{"conditions": {"age": {"op": "ge", "value": 18}, "country": "US"},
"outputs": {"tier": "us-adult"}},
{"conditions": {"age": {"op": "ge", "value": 18}},
"outputs": {"tier": "adult"}},
],
}
evaluate_table(spec, {"age": 30, "country": "DE"}) # -> {"tier": "adult"}

``evaluate_table`` returns the matched outputs dict (or ``{}`` if none) for
single-hit policies, and a list for ``COLLECT``. The ``DecisionTable`` class
(``from_dict`` / ``evaluate``) is available for reuse.

Executor command
----------------

``AC_decision_table`` takes ``spec`` and ``context`` (each a dict or JSON
string) and returns ``{result}``. The same operation is exposed as the MCP tool
``ac_decision_table`` and as a Script Builder command under **Flow**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v50_features_doc
doc/new_features/v51_features_doc
doc/new_features/v52_features_doc
doc/new_features/v53_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
53 changes: 53 additions & 0 deletions docs/source/Zh/doc/new_features/v53_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
DMN 式決策表
============

巢狀的 ``AC_if_var`` 鏈很快就難以閱讀。決策表將分支外部化為一列列的 ``conditions ->
outputs``,並以**命中政策(hit policy)**評估 —— 這是 DMN 讓商業規則維持資料驅動且易於審
查的方式。

每個儲存格條件為萬用字元(``None`` / ``"-"`` / ``"*"``)、字面值(相等)或 ``{"op": "ge",
"value": 18}``,使用本專案標準比較子(``eq/ne/lt/le/gt/ge/contains/startswith/endswith``
—— 重用自執行器,不重複)。純標準函式庫;不匯入 ``PySide6``。

命中政策
--------

================ ===================================================
政策 行為
================ ===================================================
``UNIQUE`` 僅可有一條規則命中(多於一條則拋例外)。
``FIRST`` 第一條命中的規則勝出。
``PRIORITY`` 同 FIRST —— 依規則順序取第一個命中。
``COLLECT`` 所有命中規則的 outputs(清單)。
================ ===================================================

無頭 API
--------

.. code-block:: python

from je_auto_control import evaluate_table

spec = {
"inputs": ["age", "country"],
"hit_policy": "FIRST",
"rules": [
{"conditions": {"age": {"op": "lt", "value": 18}},
"outputs": {"tier": "minor"}},
{"conditions": {"age": {"op": "ge", "value": 18}, "country": "US"},
"outputs": {"tier": "us-adult"}},
{"conditions": {"age": {"op": "ge", "value": 18}},
"outputs": {"tier": "adult"}},
],
}
evaluate_table(spec, {"age": 30, "country": "DE"}) # -> {"tier": "adult"}

``evaluate_table`` 對單一命中政策回傳命中的 outputs dict(若無則 ``{}``),``COLLECT`` 則
回傳清單。``DecisionTable`` 類別(``from_dict`` / ``evaluate``)亦可重用。

執行器指令
----------

``AC_decision_table`` 接受 ``spec`` 與 ``context``(各為 dict 或 JSON 字串),回傳
``{result}``。相同操作亦提供為 MCP 工具 ``ac_decision_table``,以及 Script Builder 中
**Flow** 分類下的指令。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v50_features_doc
doc/new_features/v51_features_doc
doc/new_features/v52_features_doc
doc/new_features/v53_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
5 changes: 5 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@
)
# Saga orchestrator: multi-step flow with compensating rollback
from je_auto_control.utils.saga import Saga, SagaResult, run_saga
# DMN-style decision tables (rules + hit policy)
from je_auto_control.utils.decision_table import (
DecisionTable, Rule, evaluate_table,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -746,6 +750,7 @@ def start_autocontrol_gui(*args, **kwargs):
"WebhookChannel", "WebhookResult", "notify_webhook", "set_default_poster",
"json_extract", "json_query", "json_query_one",
"Saga", "SagaResult", "run_saga",
"DecisionTable", "Rule", "evaluate_table",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
11 changes: 11 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,17 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
),
description="Run steps; on failure undo completed steps LIFO.",
))
specs.append(CommandSpec(
"AC_decision_table", "Flow", "Decision Table (DMN)",
fields=(
FieldSpec("spec", FieldType.STRING,
placeholder='{"inputs": ["age"], "hit_policy": "FIRST", '
'"rules": [...]}'),
FieldSpec("context", FieldType.STRING,
placeholder='{"age": 30}'),
),
description="Evaluate inputs against a rule table (hit policy).",
))
specs.append(CommandSpec(
"AC_generate_sop", "Report", "Generate SOP Document",
fields=(
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/decision_table/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""DMN-style decision tables: rules + hit policy for externalized branching."""
from je_auto_control.utils.decision_table.decision_table import (
DecisionTable, Rule, evaluate_table,
)

__all__ = ["DecisionTable", "Rule", "evaluate_table"]
97 changes: 97 additions & 0 deletions je_auto_control/utils/decision_table/decision_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Evaluate inputs against a table of rules (a DMN-style decision table).

Nested ``AC_if_var`` chains get unreadable fast; a decision table externalizes
branching into rows of ``conditions -> outputs`` with a *hit policy*. Each cell
condition is a wildcard (``None`` / ``"-"`` / ``"*"``), a literal (equality), or
``{"op": "ge", "value": 18}`` using the project's standard comparators
(``eq/ne/lt/le/gt/ge/contains/startswith/endswith`` — reused from the executor,
not duplicated).

Hit policies: ``UNIQUE`` (exactly one rule may match, else error), ``FIRST`` /
``PRIORITY`` (first matching rule wins), ``COLLECT`` (all matches). Pure standard
library; imports no ``PySide6``.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List, Mapping

HIT_UNIQUE = "UNIQUE"
HIT_FIRST = "FIRST"
HIT_PRIORITY = "PRIORITY"
HIT_COLLECT = "COLLECT"
_WILDCARDS = (None, "", "-", "*")


@dataclass
class Rule:
"""One table row: per-input conditions and the outputs to return."""

conditions: Dict[str, Any] = field(default_factory=dict)
outputs: Dict[str, Any] = field(default_factory=dict)


def _comparators() -> Dict[str, Any]:
from je_auto_control.utils.executor.flow_control import _COMPARATORS
return _COMPARATORS


def _cell_matches(value: Any, condition: Any) -> bool:
if condition in _WILDCARDS:
return True
if isinstance(condition, dict) and "op" in condition:
comparator = _comparators().get(condition["op"])
if comparator is None:
raise ValueError(f"unknown decision op: {condition['op']!r}")
try:
return comparator(value, condition.get("value"))
except TypeError:
return False
return value == condition


def _rule_matches(rule: Rule, context: Mapping[str, Any]) -> bool:
return all(_cell_matches(context.get(name), cond)
for name, cond in rule.conditions.items())


class DecisionTable:
"""A list of rules evaluated against an input context by a hit policy."""

def __init__(self, inputs: List[str], rules: List[Rule],
hit_policy: str = HIT_UNIQUE) -> None:
"""``inputs`` documents the input names; ``hit_policy`` selects matches."""
self.inputs = list(inputs)
self.rules = list(rules)
self.hit_policy = hit_policy.upper()

@classmethod
def from_dict(cls, spec: Mapping[str, Any]) -> "DecisionTable":
"""Build a table from a JSON-style ``{inputs, hit_policy, rules}`` spec."""
rules = [Rule(dict(r.get("conditions", {})), dict(r.get("outputs", {})))
for r in spec.get("rules", [])]
return cls(spec.get("inputs", []), rules,
spec.get("hit_policy", HIT_UNIQUE))

def evaluate(self, context: Mapping[str, Any]) -> List[Dict[str, Any]]:
"""Return the outputs of matching rules per the hit policy."""
matches = [dict(rule.outputs) for rule in self.rules
if _rule_matches(rule, context)]
if self.hit_policy == HIT_UNIQUE and len(matches) > 1:
raise ValueError(
f"UNIQUE hit policy matched {len(matches)} rules")
if self.hit_policy in (HIT_FIRST, HIT_PRIORITY, HIT_UNIQUE):
return matches[:1]
return matches


def evaluate_table(spec: Mapping[str, Any],
context: Mapping[str, Any]) -> Any:
"""Evaluate a table spec against a context.

Returns a list for ``COLLECT``; otherwise the single matched outputs dict
(or ``{}`` when nothing matches).
"""
table = DecisionTable.from_dict(spec)
matches = table.evaluate(context)
if table.hit_policy == HIT_COLLECT:
return matches
return matches[0] if matches else {}
12 changes: 12 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3328,6 +3328,17 @@ def _run_saga(steps: Any) -> Dict[str, Any]:
"failed_step": result.failed_step, "error": result.error}


def _decision_table(spec: Any, context: Any) -> Dict[str, Any]:
"""Adapter: evaluate a DMN-style decision table against a context."""
import json
from je_auto_control.utils.decision_table import evaluate_table
if isinstance(spec, str):
spec = json.loads(spec)
if isinstance(context, str):
context = json.loads(context)
return {"result": evaluate_table(spec, context)}


class Executor:
"""
Executor
Expand Down Expand Up @@ -3611,6 +3622,7 @@ def __init__(self):
"AC_json_query": _json_query,
"AC_json_extract": _json_extract,
"AC_run_saga": _run_saga,
"AC_decision_table": _decision_table,
"AC_a11y_record_start": _a11y_record_start,
"AC_a11y_record_stop": _a11y_record_stop,
"AC_a11y_record_events": _a11y_record_events,
Expand Down
20 changes: 19 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3244,6 +3244,24 @@ def saga_tools() -> List[MCPTool]:
]


def decision_table_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_decision_table",
description=("Evaluate a DMN-style decision table 'spec' "
"({inputs, hit_policy: UNIQUE/FIRST/PRIORITY/COLLECT, "
"rules:[{conditions, outputs}]}) against a 'context'. "
"Conditions are wildcard/literal/{op,value}. Returns "
"{result} (outputs dict, or list for COLLECT)."),
input_schema=schema(
{"spec": {"type": "object"}, "context": {"type": "object"}},
["spec", "context"]),
handler=h.decision_table,
annotations=READ_ONLY,
),
]


def unattended_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -4307,7 +4325,7 @@ def media_assert_tools() -> List[MCPTool]:
video_report_tools, fuzzy_tools, artifact_store_tools, image_dedup_tools,
locale_tools, voice_tools, coordinate_space_tools, loop_guard_tools,
process_mining_tools, asset_tools, events_tools, notify_channel_tools,
jsonpath_tools, saga_tools,
jsonpath_tools, saga_tools, decision_table_tools,
screen_record_tools,
process_and_shell_tools, remote_desktop_tools, gamepad_tools,
usb_passthrough_tools, assertion_tools, data_source_tools,
Expand Down
Loading
Loading