File size: 3,869 Bytes
ab62348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""Deterministic text-only router registered as ``router-rules/v1``.

Only the question and visible option text are inspected.  Source metadata,
board categories, images, and conversation state are intentionally absent
from this API.
"""

from __future__ import annotations

import hashlib
import json
import re
from collections.abc import Mapping, Sequence
from typing import Any


RULES_VERSION = "router-rules/v1"
SCREEN_GEOMETRY = "screen_geometry"
DEFAULT = "default"
ROUTES = (SCREEN_GEOMETRY, DEFAULT)

GUI_TARGET_CUES = (
    "marker", "click", "tap", "button", "icon", "menu", "toolbar", "tab",
    "window", "dialog", "element", "on screen", "screenshot", "cursor", "link",
    "field",
)
GEOMETRY_CUES = (
    "angle", "triangle", "circle", "radius", "diameter", "perimeter", "area of",
    "length of", "parallel", "perpendicular", "polygon", "quadrilateral", "chord",
    "tangent", "arc", "degrees", "°", "find x", "find y", r"\triangle",
)
LITERAL_CUES = ("°", r"\triangle")

RULE_DEFINITION = {
    "version": RULES_VERSION,
    "input": "question+option-text-only",
    "case_sensitive": False,
    "matching": {
        "word_and_phrase_cues": "unicode-word-boundaries",
        "literal_cues": list(LITERAL_CUES),
    },
    "screen_geometry": {
        "gui_target_cues": list(GUI_TARGET_CUES),
        "geometry_cues": list(GEOMETRY_CUES),
    },
    "fallback": DEFAULT,
    "ambiguous": DEFAULT,
}


def _canonical_bytes(value: Any) -> bytes:
    return json.dumps(
        value, ensure_ascii=False, sort_keys=True, separators=(",", ":"),
        allow_nan=False,
    ).encode("utf-8")


RULES_SHA256 = hashlib.sha256(_canonical_bytes(RULE_DEFINITION)).hexdigest()


def _pattern(cue: str) -> re.Pattern[str]:
    # Every non-literal registered cue consists of words separated by spaces.
    # Lookarounds make phrases boundary-aware at both ends without ASCII-only
    # ``\b`` behaviour ("tablet" must not match "tab", for example).
    return re.compile(r"(?<!\w)" + re.escape(cue) + r"(?!\w)", re.IGNORECASE)


_WORD_PATTERNS = tuple(
    _pattern(cue)
    for cue in (*GUI_TARGET_CUES, *GEOMETRY_CUES)
    if cue not in LITERAL_CUES
)


def _option_text(option: Any) -> str | None:
    if isinstance(option, str):
        return option
    if isinstance(option, Mapping):
        # These are the two visible fields accepted by the benchmark and
        # /v1/systemone contracts.  Metadata fields are deliberately ignored.
        values = []
        for key in ("name", "description", "text"):
            value = option.get(key)
            if value is not None:
                if not isinstance(value, str):
                    return None
                values.append(value)
        return " ".join(values) if values else None
    return None


def route(question: str, options: Sequence[Any]) -> str:
    """Return the registered route, failing ambiguous/malformed inputs closed.

    Matching is case-insensitive.  Word cues and multi-word phrases require
    word boundaries; ``°`` and ``\\triangle`` are matched literally.
    """
    if not isinstance(question, str) or isinstance(options, (str, bytes)):
        return DEFAULT
    try:
        option_values = list(options)
    except TypeError:
        return DEFAULT
    visible = []
    for option in option_values:
        value = _option_text(option)
        if value is None:
            return DEFAULT
        visible.append(value)
    text = "\n".join((question, *visible)).casefold()
    if any(cue.casefold() in text for cue in LITERAL_CUES):
        return SCREEN_GEOMETRY
    if any(pattern.search(text) for pattern in _WORD_PATTERNS):
        return SCREEN_GEOMETRY
    return DEFAULT


__all__ = [
    "DEFAULT", "GEOMETRY_CUES", "GUI_TARGET_CUES", "LITERAL_CUES", "ROUTES",
    "RULES_SHA256", "RULES_VERSION", "RULE_DEFINITION", "SCREEN_GEOMETRY", "route",
]