Files
iris_x_hermes/gateway-plugin/pickers.py
T
ARIA b8e756c3dd
CI / Gateway plugin tests (pull_request) Successful in 4m59s
CI / Kotlin tests (android host + desktop) (pull_request) Successful in 7m5s
Split adapter.py monolith into focused modules; restore Ruff complexity defaults (issue #12)
adapter.py was a 3,493-line monolith. Split it into focused modules with
clear separation of responsibilities, bringing it down to ~857 lines:

- Module-level helpers: hooks, classify, pickers, commands, setup,
  defaults, secrets
- Frame-handler mixins: inbound, tool_frames, push_frames, media_frames,
  picker_frames, channel_frames, query_frames
- mixin_base: IrisAdapterBase (declaration-only base for shared attrs)
- adapter.py now holds only IrisAdapter (the composition of the 7 mixins
  + BasePlatformAdapter), register(), and test-facing re-exports

The mixins come before BasePlatformAdapter in the MRO so their methods
override the base; super() calls (e.g. send_image) still resolve to
BasePlatformAdapter. No circular imports; dispatch.py and http_server.py
(instance-method callers) are unaffected.

Ruff complexity ceilings (PLR0911/0912/0913/0915) restored to Ruff's
built-in defaults (12/50/6/5) instead of "just above the current maxima",
which ratchets the bar down as code grows. The existing genuinely-complex
functions (frame builders mirroring the wire schema, the QR matrix builder,
the dispatch table) carry an explicit `# noqa: PLR09xx` marking them as
reviewed, frozen exceptions; new code is held to the default ceilings.

All 125 tests green (94 test_android + 31 test_android_http); no new ruff
errors introduced.
2026-08-24 20:55:00 +02:00

84 lines
3.2 KiB
Python

"""Choice-picker callbacks: clarify + exec approval.
Build the ``on_choice_selected`` closures the adapter stores in
``_pending_pickers``; a ``picker.select`` from the app runs the closure and
delivers its reply text as a normal message.
"""
def _clarify_is_multi(clarify_id: str) -> bool:
"""True when the pending clarify [clarify_id] allows multiple selections.
The flag lives on the gateway's pending entry; a missing/expired entry (or
any lookup error) is treated as single-select.
"""
try:
from tools import clarify_gateway as _cg
with _cg._lock:
_entry = _cg._entries.get(clarify_id)
return bool(_entry and getattr(_entry, "multi_select", False))
except Exception:
return False
def _clarify_picker_callback(clarify_id: str, choices: list[str]):
"""Build the ``on_choice_selected`` callback for a clarify picker.
Option values are positional (``c0``..``cN``) plus an ``other`` sentinel;
the closure maps them back to the real choice strings. A real pick resolves
the clarify (the agent then continues and replies); "Other" flips the entry
to text-capture so the next typed message is the answer. An unmappable value
also flips to text so a clarify never dead-ends.
"""
async def on_choice_selected(chat_id: str, value: str) -> str | None:
from tools.clarify_gateway import mark_awaiting_text, resolve_gateway_clarify
if value == "other":
mark_awaiting_text(clarify_id)
return "✏️ Type your answer:"
try:
idx = int(value[1:]) if value.startswith("c") else -1
except ValueError:
idx = -1
if 0 <= idx < len(choices):
resolve_gateway_clarify(clarify_id, choices[idx])
return None
mark_awaiting_text(clarify_id)
return "✏️ Type your answer:"
return on_choice_selected
# The four exec-approval outcomes hermes understands (tools.approval).
_APPROVAL_CHOICES = ("once", "session", "always", "deny")
def _approval_picker_callback(session_key: str):
"""Build the ``on_choice_selected`` callback for an exec-approval picker.
The option values are the raw hermes approval outcomes (``once`` /
``session`` / ``always`` / ``deny``); a tap resolves the waiting agent
thread via ``resolve_gateway_approval`` (the same primitive the text
``/approve`` / ``/deny`` handlers use) and returns a short confirmation
label, which the picker handler delivers as a normal message. An unknown
value is treated as a deny so a stray tap never approves a command.
"""
async def on_choice_selected(chat_id: str, value: str) -> str | None:
from tools.approval import resolve_gateway_approval
choice = value if value in _APPROVAL_CHOICES else "deny"
count = resolve_gateway_approval(session_key, choice)
label = {
"once": "✅ Approved once",
"session": "✅ Approved for this session",
"always": "✅ Approved permanently",
"deny": "❌ Denied",
}[choice]
if not count:
label = "⌛ Approval expired — no command was waiting."
return label
return on_choice_selected