Add spoken tool progress acknowledgements

This commit is contained in:
Mikei386
2026-08-23 13:54:39 +02:00
parent 4ae8ab948e
commit 727827ac32
12 changed files with 219 additions and 5 deletions
@@ -0,0 +1,182 @@
"""
title: MikeAI Spoken Tool Status
author: MikeAI
version: 1.0.0
description: Plays one short, local acknowledgement when a real tool call starts.
"""
from __future__ import annotations
import json
import threading
from pydantic import BaseModel
class Filter:
class Valves(BaseModel):
priority: int = 80
enabled: bool = True
_announced: set[str] = set()
_lock = threading.Lock()
def __init__(self):
self.valves = self.Valves()
self.toggle = False
@staticmethod
def _key(metadata: dict | None) -> str:
if isinstance(metadata, dict):
value = metadata.get("message_id") or metadata.get("id") or metadata.get("chat_id")
if value:
return str(value)
return "current"
@staticmethod
def _tool_names(event: dict) -> list[str]:
names: list[str] = []
candidates = []
direct = event.get("tool_calls")
if direct:
candidates.append(direct)
for choice in event.get("choices") or []:
if not isinstance(choice, dict):
continue
for container_name in ("delta", "message"):
container = choice.get(container_name)
if isinstance(container, dict) and container.get("tool_calls"):
candidates.append(container["tool_calls"])
for candidate in candidates:
calls = candidate if isinstance(candidate, list) else [candidate]
for call in calls:
if not isinstance(call, dict):
continue
function = call.get("function")
if isinstance(function, dict) and function.get("name"):
names.append(str(function["name"]))
elif call.get("name"):
names.append(str(call["name"]))
return names
@staticmethod
def _category(names: list[str]) -> str:
joined = " ".join(names).lower().replace("-", "_")
if any(token in joined for token in ("local_web", "web_search", "web_compare", "web_research")):
return "web"
if any(token in joined for token in ("unraid", "mua_")):
return "unraid"
if any(token in joined for token in ("homeassistant", "home_assistant", "hass_")):
return "homeassistant"
if any(token in joined for token in ("sonarr", "radarr", "arr_")):
return "media"
return "generic"
async def inlet(self, body: dict, __metadata__: dict = None, **kwargs) -> dict:
if self.valves.enabled:
key = self._key(__metadata__)
with self._lock:
self._announced.discard(key)
return body
async def stream(
self,
event: dict,
__metadata__: dict = None,
__event_emitter__=None,
**kwargs,
) -> dict:
if not self.valves.enabled or __event_emitter__ is None or not isinstance(event, dict):
return event
names = self._tool_names(event)
if not names:
return event
key = self._key(__metadata__)
with self._lock:
if key in self._announced:
return event
self._announced.add(key)
if len(self._announced) > 1024:
self._announced.clear()
self._announced.add(key)
category = self._category(names)
javascript = f"""
(() => {{
const category = {json.dumps(category)};
const key = {json.dumps(key)};
const allowed = ['web', 'unraid', 'homeassistant', 'media', 'generic'];
const safeCategory = allowed.includes(category) ? category : 'generic';
const timers = window.__mikeAIToolStatusTimers ||= new Map();
if (timers.has(key)) return;
const timer = window.setTimeout(async () => {{
timers.delete(key);
let enabled = false;
try {{
const local = JSON.parse(localStorage.getItem('settings') || '{{}}');
enabled = (local.ui || local).responseAutoPlayback === true;
const token = localStorage.getItem('token');
if (token) {{
const response = await fetch('/api/v1/users/user/settings', {{
headers: {{ Authorization: `Bearer ${{token}}` }},
cache: 'no-store'
}});
if (response.ok) {{
const settings = await response.json();
enabled = (settings.ui || settings).responseAutoPlayback === true;
}}
}}
}} catch (_) {{}}
if (!enabled) return;
try {{
const audio = new Audio(`/static/tool-status/${{safeCategory}}.mp3?status=v1`);
audio.preload = 'auto';
audio.volume = 0.92;
await audio.play();
}} catch (_) {{}}
}}, 850);
timers.set(key, timer);
}})();
""".strip()
try:
await __event_emitter__(
{"type": "execute", "data": {"code": javascript}}
)
except Exception:
pass
return event
async def outlet(
self,
body: dict,
__metadata__: dict = None,
__event_emitter__=None,
**kwargs,
) -> dict:
key = self._key(__metadata__)
with self._lock:
self._announced.discard(key)
if __event_emitter__ is not None:
javascript = f"""
(() => {{
const key = {json.dumps(key)};
const timers = window.__mikeAIToolStatusTimers;
const timer = timers && timers.get(key);
if (timer !== undefined) {{
window.clearTimeout(timer);
timers.delete(key);
}}
}})();
""".strip()
try:
await __event_emitter__(
{"type": "execute", "data": {"code": javascript}}
)
except Exception:
pass
return body
+10 -4
View File
@@ -9,7 +9,7 @@ ACTION_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/actions" && pwd)
die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
for file in reasoning_default_off.py thinking.py stability_guard.py secret_redaction.py local_performance_metrics.py; do
for file in reasoning_default_off.py thinking.py stability_guard.py secret_redaction.py spoken_tool_status.py local_performance_metrics.py; do
[[ -s $FILTER_DIR/$file ]] || die "Filterdatei fehlt: $file"
done
[[ -s $ACTION_DIR/quick_actions.py ]] || die "Actiondatei fehlt: quick_actions.py"
@@ -58,10 +58,11 @@ if not {"key", "value", "updated_at"} <= config_columns:
owner = requested_owner
if not owner:
existing = con.execute(
"select user_id from function where id in (?, ?, ?, ?, ?, ?) order by id limit 1",
"select user_id from function where id in (?, ?, ?, ?, ?, ?, ?) order by id limit 1",
(
"reasoning_default_off", "thinking", "stability_guard",
"secret_redaction", "local_performance_metrics", "quick_actions",
"secret_redaction", "spoken_tool_status", "local_performance_metrics",
"quick_actions",
),
).fetchone()
if existing:
@@ -80,6 +81,11 @@ functions = [
("thinking", "Thinking", "filter", 20, filter_dir, ""),
("stability_guard", "MikeAI Stability Guard", "filter", 30, filter_dir, ""),
("secret_redaction", "MikeAI Secret Redaction", "filter", 40, filter_dir, ""),
(
"spoken_tool_status", "MikeAI Spoken Tool Status", "filter", 80, filter_dir,
"Spielt bei aktiver automatischer Sprachausgabe einmalig eine kurze lokale Ansage, "
"sobald ein echtes Werkzeug gestartet wird.",
),
("local_performance_metrics", "MikeAI Local Performance Metrics", "filter", 90, filter_dir, ""),
(
"quick_actions", "MikeAI Quick Actions", "action", 100, action_dir,
@@ -222,7 +228,7 @@ with con:
)
print(
"OpenWebUI konfiguriert: Default Off=10, Thinking=20, "
"Stability Guard=30, Secret Redaction=40, Local Metrics=90, "
"Stability Guard=30, Secret Redaction=40, Spoken Tool Status=80, Local Metrics=90, "
"Quick Actions=100, Folgefragen=aus, Piper-TTS=aktiv, Werkzeugwahl=optimiert"
)
PY
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.