Add spoken tool progress acknowledgements
This commit is contained in:
@@ -19,6 +19,7 @@ logs/
|
||||
*.log
|
||||
*.wav
|
||||
*.mp3
|
||||
!platform/openwebui/theme/tool-status/*.mp3
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
|
||||
@@ -71,6 +71,9 @@ Dark-Theme namens **Midnight Aurora**. CSS und Start-Loader liegen unter
|
||||
`platform/openwebui/theme/` und werden schreibgeschützt in den Container
|
||||
eingebunden. Der Hintergrund bewegt sich bewusst langsam; Browser mit aktivierter
|
||||
Option „Bewegung reduzieren“ erhalten automatisch eine unbewegte Variante.
|
||||
Kurze Werkzeugbestätigungen werden ebenfalls lokal aus statischen Clips
|
||||
abgespielt. Sie laufen nur bei aktivierter automatischer Sprachausgabe, kosten
|
||||
keine Modell-Tokens und verraten dem Modell keine zusätzlichen Daten.
|
||||
|
||||
## Dokumentation
|
||||
|
||||
|
||||
@@ -710,6 +710,7 @@ services:
|
||||
- ./platform/openwebui/theme/custom.css:/app/build/static/custom.css:ro
|
||||
- ./platform/openwebui/theme/loader.js:/app/build/static/loader.js:ro
|
||||
- ./platform/openwebui/theme/midnight-aurora.svg:/app/build/static/midnight-aurora.svg:ro
|
||||
- ./platform/openwebui/theme/tool-status:/app/build/static/tool-status:ro
|
||||
environment:
|
||||
WEBUI_SECRET_KEY: "${WEBUI_SECRET_KEY:?WEBUI_SECRET_KEY is required}"
|
||||
DEFAULT_MODELS: mikeai-medium
|
||||
|
||||
@@ -138,6 +138,13 @@ dem internen Router und dessen aktuellem Schlüssel. Das ist erforderlich, weil
|
||||
persistente Providerwerte nach einer Schlüsselrotation Vorrang vor den
|
||||
Container-Umgebungsvariablen haben.
|
||||
|
||||
Der Filterinstaller richtet außerdem die lokalen gesprochenen
|
||||
Werkzeugbestätigungen ein. Die vorgerenderten Ansagen werden über
|
||||
`/static/tool-status/` read-only ausgeliefert und nur bei aktivierter
|
||||
automatischer Sprachausgabe abgespielt. Zur Abnahme eine Websuche und eine
|
||||
Unraid-Abfrage auslösen: Vor längeren Aufrufen muss genau eine kurze passende
|
||||
Ansage kommen; bei ausgeschalteter automatischer Sprachausgabe bleibt sie aus.
|
||||
|
||||
Der Modellinstaller setzt außerdem für den ermittelten OpenWebUI-Benutzer das
|
||||
native Chat-Hintergrundbild `/static/midnight-aurora.svg`. Das Bild wird durch
|
||||
Compose read-only eingebunden. `custom.css` verändert bewusst nicht mehr die
|
||||
|
||||
+15
-1
@@ -16,7 +16,15 @@ Reihenfolge ist absichtlich festgelegt:
|
||||
Passwörter, JWTs und private Schlüssel aus Tool-Ergebnissen, bevor sie das
|
||||
Modell erreichen, sowie aus fertigen Modellantworten. Nutzereingaben und
|
||||
Authentifizierungswege werden nicht verändert.
|
||||
5. `MikeAI Local Performance Metrics`, Priorität 90: erfasst nach Abschluss
|
||||
5. `MikeAI Spoken Tool Status`, Priorität 80: erkennt den ersten echten
|
||||
Werkzeugaufruf eines Antwortlaufs und löst im Browser genau eine kurze,
|
||||
passende Ansage für Web, Unraid, Home Assistant, Medienverwaltung oder
|
||||
sonstige Werkzeuge aus. Die fünf MP3-Clips sind vorgerendert und liegen
|
||||
unter `platform/openwebui/theme/tool-status/`; dadurch blockiert die Ansage
|
||||
weder XTTS noch das Werkzeug. Sie wird nur abgespielt, wenn der Benutzer in
|
||||
OpenWebUI die automatische Sprachausgabe aktiviert hat. Endet ein sehr
|
||||
schneller Lauf innerhalb der kurzen Wartezeit, wird die Ansage verworfen.
|
||||
6. `MikeAI Local Performance Metrics`, Priorität 90: erfasst nach Abschluss
|
||||
ausschließlich technische Zahlen wie Laufzeit, Tokenzähler, Token/s und
|
||||
Tool-Anzahl. Nutzer-, Chat- und Nachrichten-IDs sowie sämtliche Textinhalte
|
||||
werden weder geschrieben noch gehasht gespeichert.
|
||||
@@ -70,6 +78,12 @@ Die rotierende, inhaltsfreie Metrikdatei liegt im persistenten
|
||||
OpenWebUI-Volume unter `mike-ai-request-metrics.jsonl` (maximal 5 MiB plus eine
|
||||
Rotation). Sie darf für Benchmarks ausgewertet werden, ohne Chats auszulesen.
|
||||
|
||||
Die Werkzeugansagen enthalten weder Prompt- noch Ergebnistext und werden nicht
|
||||
in die Unterhaltung oder den Modellkontext geschrieben. Der lokale
|
||||
OpenWebUI-Browser-Event prüft die persönliche Einstellung
|
||||
`responseAutoPlayback` über denselben Browser-Login. Ein Fehler, eine
|
||||
Browser-Autoplay-Sperre oder ein fehlender Clip bleibt folgenlos für den Chat.
|
||||
|
||||
## Aktionen an Modellantworten
|
||||
|
||||
Die globale Action `MikeAI Quick Actions` ergänzt die Nachrichtenleiste um:
|
||||
|
||||
@@ -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
|
||||
@@ -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.
Binary file not shown.
Reference in New Issue
Block a user