Skip to content

Commit a2bf011

Browse files
authored
Merge pull request #248 from Integration-Automation/feat/fuzzy-match-batch
Add fuzzy string matching and dedupe
2 parents 4fe0e48 + 81394ca commit a2bf011

16 files changed

Lines changed: 451 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
## Table of Contents
1515

16+
- [What's new (2026-06-20) — Fuzzy String Matching & Dedupe](#whats-new-2026-06-20--fuzzy-string-matching--dedupe)
1617
- [What's new (2026-06-19) — Video Step-Overlay Report](#whats-new-2026-06-19--video-step-overlay-report)
1718
- [What's new (2026-06-19) — Agent Observability (GenAI OpenTelemetry Spans)](#whats-new-2026-06-19--agent-observability-genai-opentelemetry-spans)
1819
- [What's new (2026-06-19) — Compliance Control Report (SOC2 / ISO 27001)](#whats-new-2026-06-19--compliance-control-report-soc2--iso-27001)
@@ -92,6 +93,12 @@
9293

9394
---
9495

96+
## What's new (2026-06-20) — Fuzzy String Matching & Dedupe
97+
98+
Match noisy OCR/UI text robustly. Full reference: [`docs/source/Eng/doc/new_features/v40_features_doc.rst`](docs/source/Eng/doc/new_features/v40_features_doc.rst).
99+
100+
- **`fuzzy_ratio` / `fuzzy_best_match` / `fuzzy_matches` / `fuzzy_dedupe`** (`AC_fuzzy_ratio` / `AC_fuzzy_best_match` / `AC_fuzzy_dedupe`, `ac_*`): score similarity (0..1), pick the closest candidate from a list, or collapse near-duplicates — so a flow can act on "the button that *looks like* Submit" rather than an exact label. The default backend is stdlib `difflib` (**zero extra deps**); the optional `[fuzzy]` extra adds `rapidfuzz` for speed, with scores normalised either way. `ignore_case` and `score_cutoff` supported.
101+
95102
## What's new (2026-06-19) — Video Step-Overlay Report
96103

97104
Caption screenshots into a walkthrough video. Full reference: [`docs/source/Eng/doc/new_features/v39_features_doc.rst`](docs/source/Eng/doc/new_features/v39_features_doc.rst).

README/README_zh-CN.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目录
1414

15+
- [本次更新 (2026-06-20) — 模糊字符串匹配与去重](#本次更新-2026-06-20--模糊字符串匹配与去重)
1516
- [本次更新 (2026-06-19) — 视频步骤叠加报告](#本次更新-2026-06-19--视频步骤叠加报告)
1617
- [本次更新 (2026-06-19) — Agent 可观测性(GenAI OpenTelemetry Spans)](#本次更新-2026-06-19--agent-可观测性genai-opentelemetry-spans)
1718
- [本次更新 (2026-06-19) — 合规控制报告(SOC2 / ISO 27001)](#本次更新-2026-06-19--合规控制报告soc2--iso-27001)
@@ -91,6 +92,12 @@
9192

9293
---
9394

95+
## 本次更新 (2026-06-20) — 模糊字符串匹配与去重
96+
97+
稳健匹配含噪声的 OCR/UI 文本。完整参考:[`docs/source/Zh/doc/new_features/v40_features_doc.rst`](../docs/source/Zh/doc/new_features/v40_features_doc.rst)
98+
99+
- **`fuzzy_ratio` / `fuzzy_best_match` / `fuzzy_matches` / `fuzzy_dedupe`**(`AC_fuzzy_ratio` / `AC_fuzzy_best_match` / `AC_fuzzy_dedupe``ac_*`):为相似度评分(0..1)、从列表挑最接近的候选,或收合近似重复 —— 让流程可针对「*看起来像* Submit 的按钮」动作,而非精确标签。默认后端为标准库 `difflib`(**无额外依赖**);可选的 `[fuzzy]` extra 加入 `rapidfuzz` 以加速,两者分数皆归一化。支持 `ignore_case``score_cutoff`
100+
94101
## 本次更新 (2026-06-19) — 视频步骤叠加报告
95102

96103
将屏幕截图加上字幕制成走查视频。完整参考:[`docs/source/Zh/doc/new_features/v39_features_doc.rst`](../docs/source/Zh/doc/new_features/v39_features_doc.rst)

README/README_zh-TW.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目錄
1414

15+
- [本次更新 (2026-06-20) — 模糊字串比對與去重](#本次更新-2026-06-20--模糊字串比對與去重)
1516
- [本次更新 (2026-06-19) — 影片步驟疊加報告](#本次更新-2026-06-19--影片步驟疊加報告)
1617
- [本次更新 (2026-06-19) — Agent 可觀測性(GenAI OpenTelemetry Spans)](#本次更新-2026-06-19--agent-可觀測性genai-opentelemetry-spans)
1718
- [本次更新 (2026-06-19) — 合規控制報告(SOC2 / ISO 27001)](#本次更新-2026-06-19--合規控制報告soc2--iso-27001)
@@ -91,6 +92,12 @@
9192

9293
---
9394

95+
## 本次更新 (2026-06-20) — 模糊字串比對與去重
96+
97+
穩健比對含雜訊的 OCR/UI 文字。完整參考:[`docs/source/Zh/doc/new_features/v40_features_doc.rst`](../docs/source/Zh/doc/new_features/v40_features_doc.rst)
98+
99+
- **`fuzzy_ratio` / `fuzzy_best_match` / `fuzzy_matches` / `fuzzy_dedupe`**(`AC_fuzzy_ratio` / `AC_fuzzy_best_match` / `AC_fuzzy_dedupe``ac_*`):為相似度評分(0..1)、從清單挑最接近的候選,或收合近似重複 —— 讓流程可針對「*看起來像* Submit 的按鈕」動作,而非精確標籤。預設後端為標準函式庫 `difflib`(**無額外相依**);選用的 `[fuzzy]` extra 加入 `rapidfuzz` 以加速,兩者分數皆正規化。支援 `ignore_case``score_cutoff`
100+
94101
## 本次更新 (2026-06-19) — 影片步驟疊加報告
95102

96103
將螢幕截圖加上字幕製成走查影片。完整參考:[`docs/source/Zh/doc/new_features/v39_features_doc.rst`](../docs/source/Zh/doc/new_features/v39_features_doc.rst)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
Fuzzy String Matching & Dedupe
2+
==============================
3+
4+
Exact string comparison is brittle when text comes from OCR or shifting UI copy.
5+
These helpers score similarity, pick the best candidate from a list, and collapse
6+
near-duplicates — so a flow can act on "the button that *looks like* Submit"
7+
rather than an exact label.
8+
9+
The default backend is the standard library :mod:`difflib`, so the feature works
10+
with **zero extra dependencies**. If the optional ``rapidfuzz`` package is
11+
installed (``pip install je_auto_control[fuzzy]``) it is used instead for speed;
12+
scores are normalised to ``0.0..1.0`` either way, so callers never depend on
13+
which backend ran. ``BACKEND`` names the active one. Imports no ``PySide6``.
14+
15+
Headless API
16+
------------
17+
18+
.. code-block:: python
19+
20+
from je_auto_control import (
21+
fuzzy_ratio, fuzzy_best_match, fuzzy_matches, fuzzy_dedupe)
22+
23+
fuzzy_ratio("Sumbit", "Submit") # ~0.83 (case-insensitive default)
24+
25+
fuzzy_best_match("Sve", ["Cancel", "Save", "Submit"])
26+
# -> ("Save", 0.86, 1) (choice, score, index) — or None below score_cutoff
27+
28+
fuzzy_matches("login", ["login", "logon", "logout"], limit=2)
29+
# -> [("login", 1.0, 0), ("logon", 0.8, 1)] sorted best-first
30+
31+
fuzzy_dedupe(["Invoice", "invoice ", "Receipt"], threshold=0.85)
32+
# -> ["Invoice", "Receipt"] near-duplicates collapse, first kept
33+
34+
All functions take ``ignore_case`` (default ``True``); ``fuzzy_best_match`` /
35+
``fuzzy_matches`` take ``score_cutoff`` to drop weak candidates.
36+
37+
Executor commands
38+
-----------------
39+
40+
================================ ===================================================
41+
Command Effect
42+
================================ ===================================================
43+
``AC_fuzzy_ratio`` ``{score}`` similarity between two strings.
44+
``AC_fuzzy_best_match`` ``{match, score, index}`` (or null) from choices.
45+
``AC_fuzzy_dedupe`` ``{unique}`` with near-duplicates collapsed.
46+
================================ ===================================================
47+
48+
``choices`` / ``items`` accept a list or a JSON-string list (so the visual
49+
builder works). The same operations are exposed as MCP tools (``ac_fuzzy_ratio``
50+
/ ``ac_fuzzy_best_match`` / ``ac_fuzzy_dedupe``) and as Script Builder commands
51+
under **Data**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Comprehensive guides for all AutoControl features.
6262
doc/new_features/v37_features_doc
6363
doc/new_features/v38_features_doc
6464
doc/new_features/v39_features_doc
65+
doc/new_features/v40_features_doc
6566
doc/ocr_backends/ocr_backends_doc
6667
doc/observability/observability_doc
6768
doc/operations_layer/operations_layer_doc
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
模糊字串比對與去重
2+
==================
3+
4+
當文字來自 OCR 或時常變動的 UI 文案時,精確字串比對很脆弱。這些輔助函式為相似度評分、
5+
從清單中挑出最佳候選,並收合近似重複項 —— 讓流程可以針對「*看起來像* Submit 的按鈕」
6+
動作,而非精確標籤。
7+
8+
預設後端為標準函式庫 :mod:`difflib`,因此本功能**無需任何額外相依**即可運作。若安裝了
9+
選用的 ``rapidfuzz`` 套件(``pip install je_auto_control[fuzzy]``)則改用其以加速;無論
10+
何者,分數皆正規化為 ``0.0..1.0``,故呼叫端永不依賴實際執行的後端。``BACKEND`` 標示目
11+
前作用中的後端。不匯入 ``PySide6``。
12+
13+
無頭 API
14+
--------
15+
16+
.. code-block:: python
17+
18+
from je_auto_control import (
19+
fuzzy_ratio, fuzzy_best_match, fuzzy_matches, fuzzy_dedupe)
20+
21+
fuzzy_ratio("Sumbit", "Submit") # ~0.83(預設不分大小寫)
22+
23+
fuzzy_best_match("Sve", ["Cancel", "Save", "Submit"])
24+
# -> ("Save", 0.86, 1) (choice, score, index) —— 低於 score_cutoff 則為 None
25+
26+
fuzzy_matches("login", ["login", "logon", "logout"], limit=2)
27+
# -> [("login", 1.0, 0), ("logon", 0.8, 1)] 由高分至低分排序
28+
29+
fuzzy_dedupe(["Invoice", "invoice ", "Receipt"], threshold=0.85)
30+
# -> ["Invoice", "Receipt"] 近似重複收合,保留第一個
31+
32+
所有函式皆接受 ``ignore_case``(預設 ``True``);``fuzzy_best_match`` /
33+
``fuzzy_matches`` 接受 ``score_cutoff`` 以濾除弱候選。
34+
35+
執行器指令
36+
----------
37+
38+
================================ ===================================================
39+
指令 效果
40+
================================ ===================================================
41+
``AC_fuzzy_ratio`` 兩字串相似度的 ``{score}``。
42+
``AC_fuzzy_best_match`` 從候選中取 ``{match, score, index}``(或 null)。
43+
``AC_fuzzy_dedupe`` 收合近似重複後的 ``{unique}``。
44+
================================ ===================================================
45+
46+
``choices`` / ``items`` 接受清單或 JSON 字串清單(因此視覺化建構器可用)。相同操作亦提供
47+
為 MCP 工具(``ac_fuzzy_ratio`` / ``ac_fuzzy_best_match`` / ``ac_fuzzy_dedupe``),以及
48+
Script Builder 中 **Data** 分類下的指令。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ AutoControl 所有功能的完整使用指南。
6262
doc/new_features/v37_features_doc
6363
doc/new_features/v38_features_doc
6464
doc/new_features/v39_features_doc
65+
doc/new_features/v40_features_doc
6566
doc/ocr_backends/ocr_backends_doc
6667
doc/observability/observability_doc
6768
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@
230230
from je_auto_control.utils.video_report import (
231231
VideoStep, build_overlay_plan, render_overlay_frame, write_step_video,
232232
)
233+
# Fuzzy string matching / dedupe (difflib default, optional rapidfuzz)
234+
from je_auto_control.utils.fuzzy import (
235+
fuzzy_best_match, fuzzy_dedupe, fuzzy_matches, fuzzy_ratio,
236+
)
233237
# Background popup/interrupt watchdog (unattended automation)
234238
from je_auto_control.utils.watchdog import (
235239
PopupWatchdog, WatchdogRule, default_popup_watchdog,
@@ -676,6 +680,7 @@ def start_autocontrol_gui(*args, **kwargs):
676680
"AgentTrace", "default_trace", "reset_trace",
677681
"VideoStep", "build_overlay_plan", "render_overlay_frame",
678682
"write_step_video",
683+
"fuzzy_best_match", "fuzzy_dedupe", "fuzzy_matches", "fuzzy_ratio",
679684
# MCP server
680685
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
681686
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,41 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
867867
),
868868
description="Render captioned screenshots into a walkthrough video.",
869869
))
870+
specs.append(CommandSpec(
871+
"AC_fuzzy_ratio", "Data", "Fuzzy: Similarity Ratio",
872+
fields=(
873+
FieldSpec("left", FieldType.STRING),
874+
FieldSpec("right", FieldType.STRING),
875+
FieldSpec("ignore_case", FieldType.BOOL, optional=True,
876+
default=True),
877+
),
878+
description="Similarity score (0..1) between two strings.",
879+
))
880+
specs.append(CommandSpec(
881+
"AC_fuzzy_best_match", "Data", "Fuzzy: Best Match",
882+
fields=(
883+
FieldSpec("query", FieldType.STRING),
884+
FieldSpec("choices", FieldType.STRING,
885+
placeholder='["Save", "Cancel", "Submit"]'),
886+
FieldSpec("score_cutoff", FieldType.FLOAT, optional=True,
887+
default=0.0),
888+
FieldSpec("ignore_case", FieldType.BOOL, optional=True,
889+
default=True),
890+
),
891+
description="Best fuzzy match of query within choices (JSON list).",
892+
))
893+
specs.append(CommandSpec(
894+
"AC_fuzzy_dedupe", "Data", "Fuzzy: Dedupe",
895+
fields=(
896+
FieldSpec("items", FieldType.STRING,
897+
placeholder='["foo", "foo ", "bar"]'),
898+
FieldSpec("threshold", FieldType.FLOAT, optional=True,
899+
default=0.9),
900+
FieldSpec("ignore_case", FieldType.BOOL, optional=True,
901+
default=True),
902+
),
903+
description="Collapse near-duplicate strings (JSON list).",
904+
))
870905
specs.append(CommandSpec(
871906
"AC_generate_sop", "Report", "Generate SOP Document",
872907
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3067,6 +3067,37 @@ def _write_step_video(steps: Any, output: str, fps: int = 10,
30673067
seconds_per_step=seconds_per_step)
30683068

30693069

3070+
def _coerce_list(value: Any) -> List[Any]:
3071+
import json
3072+
return json.loads(value) if isinstance(value, str) else list(value)
3073+
3074+
3075+
def _fuzzy_ratio(left: Any, right: Any,
3076+
ignore_case: bool = True) -> Dict[str, Any]:
3077+
"""Adapter: similarity score (0..1) between two values."""
3078+
from je_auto_control.utils.fuzzy import fuzzy_ratio
3079+
return {"score": fuzzy_ratio(left, right, ignore_case=ignore_case)}
3080+
3081+
3082+
def _fuzzy_best_match(query: Any, choices: Any, score_cutoff: float = 0.0,
3083+
ignore_case: bool = True) -> Dict[str, Any]:
3084+
"""Adapter: best fuzzy match from choices, or a null match."""
3085+
from je_auto_control.utils.fuzzy import fuzzy_best_match
3086+
best = fuzzy_best_match(query, _coerce_list(choices),
3087+
score_cutoff=score_cutoff, ignore_case=ignore_case)
3088+
if best is None:
3089+
return {"match": None, "score": 0.0, "index": -1}
3090+
return {"match": best[0], "score": best[1], "index": best[2]}
3091+
3092+
3093+
def _fuzzy_dedupe(items: Any, threshold: float = 0.9,
3094+
ignore_case: bool = True) -> Dict[str, Any]:
3095+
"""Adapter: drop near-duplicate items, keeping the first of each cluster."""
3096+
from je_auto_control.utils.fuzzy import fuzzy_dedupe
3097+
return {"unique": fuzzy_dedupe(_coerce_list(items), threshold=threshold,
3098+
ignore_case=ignore_case)}
3099+
3100+
30703101
class Executor:
30713102
"""
30723103
Executor
@@ -3319,6 +3350,9 @@ def __init__(self):
33193350
"AC_trace_export": _trace_export,
33203351
"AC_trace_reset": _trace_reset,
33213352
"AC_write_step_video": _write_step_video,
3353+
"AC_fuzzy_ratio": _fuzzy_ratio,
3354+
"AC_fuzzy_best_match": _fuzzy_best_match,
3355+
"AC_fuzzy_dedupe": _fuzzy_dedupe,
33223356
"AC_a11y_record_start": _a11y_record_start,
33233357
"AC_a11y_record_stop": _a11y_record_stop,
33243358
"AC_a11y_record_events": _a11y_record_events,

0 commit comments

Comments
 (0)