Add spoken tool progress acknowledgements
This commit is contained in:
@@ -19,6 +19,7 @@ logs/
|
|||||||
*.log
|
*.log
|
||||||
*.wav
|
*.wav
|
||||||
*.mp3
|
*.mp3
|
||||||
|
!platform/openwebui/theme/tool-status/*.mp3
|
||||||
*.png
|
*.png
|
||||||
*.jpg
|
*.jpg
|
||||||
*.jpeg
|
*.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
|
`platform/openwebui/theme/` und werden schreibgeschützt in den Container
|
||||||
eingebunden. Der Hintergrund bewegt sich bewusst langsam; Browser mit aktivierter
|
eingebunden. Der Hintergrund bewegt sich bewusst langsam; Browser mit aktivierter
|
||||||
Option „Bewegung reduzieren“ erhalten automatisch eine unbewegte Variante.
|
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
|
## Dokumentation
|
||||||
|
|
||||||
|
|||||||
@@ -710,6 +710,7 @@ services:
|
|||||||
- ./platform/openwebui/theme/custom.css:/app/build/static/custom.css:ro
|
- ./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/loader.js:/app/build/static/loader.js:ro
|
||||||
- ./platform/openwebui/theme/midnight-aurora.svg:/app/build/static/midnight-aurora.svg: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:
|
environment:
|
||||||
WEBUI_SECRET_KEY: "${WEBUI_SECRET_KEY:?WEBUI_SECRET_KEY is required}"
|
WEBUI_SECRET_KEY: "${WEBUI_SECRET_KEY:?WEBUI_SECRET_KEY is required}"
|
||||||
DEFAULT_MODELS: mikeai-medium
|
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
|
persistente Providerwerte nach einer Schlüsselrotation Vorrang vor den
|
||||||
Container-Umgebungsvariablen haben.
|
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
|
Der Modellinstaller setzt außerdem für den ermittelten OpenWebUI-Benutzer das
|
||||||
native Chat-Hintergrundbild `/static/midnight-aurora.svg`. Das Bild wird durch
|
native Chat-Hintergrundbild `/static/midnight-aurora.svg`. Das Bild wird durch
|
||||||
Compose read-only eingebunden. `custom.css` verändert bewusst nicht mehr die
|
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
|
Passwörter, JWTs und private Schlüssel aus Tool-Ergebnissen, bevor sie das
|
||||||
Modell erreichen, sowie aus fertigen Modellantworten. Nutzereingaben und
|
Modell erreichen, sowie aus fertigen Modellantworten. Nutzereingaben und
|
||||||
Authentifizierungswege werden nicht verändert.
|
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
|
ausschließlich technische Zahlen wie Laufzeit, Tokenzähler, Token/s und
|
||||||
Tool-Anzahl. Nutzer-, Chat- und Nachrichten-IDs sowie sämtliche Textinhalte
|
Tool-Anzahl. Nutzer-, Chat- und Nachrichten-IDs sowie sämtliche Textinhalte
|
||||||
werden weder geschrieben noch gehasht gespeichert.
|
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
|
OpenWebUI-Volume unter `mike-ai-request-metrics.jsonl` (maximal 5 MiB plus eine
|
||||||
Rotation). Sie darf für Benchmarks ausgewertet werden, ohne Chats auszulesen.
|
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
|
## Aktionen an Modellantworten
|
||||||
|
|
||||||
Die globale Action `MikeAI Quick Actions` ergänzt die Nachrichtenleiste um:
|
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; }
|
die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; }
|
||||||
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
|
[[ $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"
|
[[ -s $FILTER_DIR/$file ]] || die "Filterdatei fehlt: $file"
|
||||||
done
|
done
|
||||||
[[ -s $ACTION_DIR/quick_actions.py ]] || die "Actiondatei fehlt: quick_actions.py"
|
[[ -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
|
owner = requested_owner
|
||||||
if not owner:
|
if not owner:
|
||||||
existing = con.execute(
|
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",
|
"reasoning_default_off", "thinking", "stability_guard",
|
||||||
"secret_redaction", "local_performance_metrics", "quick_actions",
|
"secret_redaction", "spoken_tool_status", "local_performance_metrics",
|
||||||
|
"quick_actions",
|
||||||
),
|
),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if existing:
|
if existing:
|
||||||
@@ -80,6 +81,11 @@ functions = [
|
|||||||
("thinking", "Thinking", "filter", 20, filter_dir, ""),
|
("thinking", "Thinking", "filter", 20, filter_dir, ""),
|
||||||
("stability_guard", "MikeAI Stability Guard", "filter", 30, filter_dir, ""),
|
("stability_guard", "MikeAI Stability Guard", "filter", 30, filter_dir, ""),
|
||||||
("secret_redaction", "MikeAI Secret Redaction", "filter", 40, 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, ""),
|
("local_performance_metrics", "MikeAI Local Performance Metrics", "filter", 90, filter_dir, ""),
|
||||||
(
|
(
|
||||||
"quick_actions", "MikeAI Quick Actions", "action", 100, action_dir,
|
"quick_actions", "MikeAI Quick Actions", "action", 100, action_dir,
|
||||||
@@ -222,7 +228,7 @@ with con:
|
|||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
"OpenWebUI konfiguriert: Default Off=10, Thinking=20, "
|
"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"
|
"Quick Actions=100, Folgefragen=aus, Piper-TTS=aktiv, Werkzeugwahl=optimiert"
|
||||||
)
|
)
|
||||||
PY
|
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