Files
Alfredv2/alfred/app/services/dona_unified_router_v4.py
T
2026-11-02 08:50:05 +01:00

138 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Dona V4: conservative fast routing for dashboard *requests* (voice or text).
No direct shell calls, no side effects on Proxmox/HA/Spotify. Reuses Dona V3
`build_dashboard()` and its existing read-only providers. Complex queries,
missing UI sockets and any provider error fall through to Hermes unchanged.
"""
from __future__ import annotations
import logging
import os
import re
import threading
import time
from typing import Callable
_LOG = logging.getLogger('dona.router.v4')
_LOCK = threading.Lock()
_EMITTER: Callable | None = None
_FALSE = {'0', 'false', 'off', 'no', 'disabled'}
def configure_emitter(fn):
"""Called once by app.py after socketio is constructed."""
global _EMITTER
if not callable(fn):
raise TypeError('Un émetteur Socket.IO callable est nécessaire')
with _LOCK:
_EMITTER = fn
def enabled():
return os.environ.get('DONA_FAST_DASHBOARDS_V4', 'on').strip().lower() not in _FALSE
# Only an explicit presentation request. No guess from vague words such as
# "proxmox" or "température" alone. These aren't commands to control devices.
_VIEW_RE = re.compile(
r'\b(tableaux?\s+(?:de\s+)?bord|tableaux?|dashboards?|vue\s+d.ensemble)\b',
re.I,
)
_COMPLEX_RE = re.compile(
r'\b(?:analy[sz]\w*|pourquoi|compar\w*|recommand\w*|optimis\w*|'
r'pr[eé]di\w*|histor\w*|tendance\w*|migr\w*|sauvegard\w*|'
r'backup\w*|restaur\w*|red[eé]marr\w*|arr[eê]t\w*|'
r'allum\w*|[eé]tein\w*|cr[eé]\w*|supprim\w*|modifi\w*|'
r'configure\w*|change\w*|envoie\w*|partage\w*|notifie\w*|'
r'enregistre\w*|planifi\w*|automati\w*|graphique\s+sur\s+\d+|'
r'sur\s+\d+\s+(?:jours?|heures?|semaines?|mois|ans)|'
r'\b(?:ceph|stockage|r[eé]seau)\b)\b', re.I,
)
_DOMAINS = {
'proxmox': re.compile(r'\bproxmox\b', re.I),
'spotify': re.compile(r'\bspotify\b', re.I),
'lights': re.compile(r'\b(lumi[eè]res?|[eé]clairages?|lampes?)\b', re.I),
'temperatures': re.compile(r'\b(temp[eé]ratures?|thermom[eè]tres?)\b', re.I),
}
_OTHER = re.compile(
r'\b(wazuh|comfyui|paperless|gmail|youtube|courriel|email|'
r'calendrier|agenda|m[eé]t[eé]o|cam[eé]ra|sonnette|visages?|'
r'reconnaissance\s+faciale|kanban)\b', re.I,
)
def route(prompt):
"""Return a source label or None; deliberate false negatives are safe."""
if not enabled() or not isinstance(prompt, str) or len(prompt.strip()) > 500:
return None
q = prompt.casefold().strip()
if not _VIEW_RE.search(q) or _COMPLEX_RE.search(q) or _OTHER.search(q):
return None
found = [key for key, regex in _DOMAINS.items() if regex.search(q)]
if len(found) != 1:
return None
# Proxmox V3 has an additional explicit standard-query classifier.
if found[0] == 'proxmox':
from services.dona_proxmox_direct import is_standard_proxmox
if not is_standard_proxmox(prompt):
return None
return found[0]
class _FallbackToHermes(Exception):
pass
def _no_hermes(_prompt):
"""Prohibit an accidental second Hermes invocation in direct mode."""
raise _FallbackToHermes()
def try_route(prompt):
"""Return spoken/text confirmation when a live board was emitted, else None.
Fallback is intentionally the original ask_hermes path. Session state is
untouched by direct-only read operations.
"""
source = route(prompt)
if source is None:
return None
with _LOCK:
emitter = _EMITTER
if emitter is None:
return None
start = time.monotonic()
try:
from services.dona_dynamic import build_dashboard
board = build_dashboard(prompt, _no_hermes)
if not isinstance(board, dict) or board.get('schema') != 'dona.dashboard.v1':
raise _FallbackToHermes()
if not isinstance(board.get('cards'), list):
raise _FallbackToHermes()
actual = str(board.get('origin', '')).lower()
allowed = {
'proxmox': {'proxmox'},
'spotify': {'spotify'},
'lights': {'lights', 'homeassistant'},
'temperatures': {'homeassistant'},
}
if actual not in allowed[source]:
raise _FallbackToHermes()
# Socket.IO callbacks may raise, in which case normal Hermes fallback.
emitter({'type': 'dona_dynamic', 'dashboard': board,
'dona_fast_v4': True})
except Exception as exc:
# No prompt, payload, tokens or tool data in logs.
_LOG.warning('[DONA-V4] route=%s outcome=fallback reason=%s elapsed_ms=%.1f',
source, type(exc).__name__, (time.monotonic()-start)*1000)
return None
_LOG.warning('[DONA-V4] route=%s outcome=direct elapsed_ms=%.1f cards=%d',
source, (time.monotonic()-start)*1000, len(board['cards']))
return {
'proxmox': 'J’affiche le tableau de bord Proxmox.',
'spotify': 'J’affiche le tableau de bord Spotify.',
'lights': 'J’affiche le tableau de bord des lumières.',
'temperatures': 'J’affiche le tableau de bord des températures.',
}[source]