From 955075e3fda09e6f8c8320ec5b9cf41d0b5c30d3 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 13 May 2026 22:30:50 +0000 Subject: [PATCH] =?UTF-8?q?feat(mac-worker):=20LaunchAgent=20f=C3=BCr=20Da?= =?UTF-8?q?uerbetrieb=20auf=20dem=20Mac?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - com.voiceagent.worker.plist.template — User-LaunchAgent mit RunAtLoad, KeepAlive(Crashed=true), ProcessType=Interactive für MLX/Metal-Zugriff - install-launchagent.sh — generiert plist mit Pfaden, lädt via launchctl, prüft Health-Endpoint; unterstützt install/uninstall/status - READMEs: Abschnitt "Worker als Dienst" mit Verwaltungs-Befehlen --- README.md | 6 +- mac-worker/README.md | 44 +++++++- .../com.voiceagent.worker.plist.template | 54 +++++++++ mac-worker/deploy/install-launchagent.sh | 103 ++++++++++++++++++ 4 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 mac-worker/deploy/com.voiceagent.worker.plist.template create mode 100755 mac-worker/deploy/install-launchagent.sh diff --git a/README.md b/README.md index 869fbf1..1872417 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,12 @@ launchctl setenv OLLAMA_HOST "0.0.0.0:11434" brew services restart ollama ollama pull llama3.1:8b -# Worker starten (Vordergrund — für dauerhaften Betrieb LaunchAgent nutzen) +# Worker starten (Vordergrund — für Test) uvicorn app.main:app --host 0.0.0.0 --port 8080 + +# Für Dauerbetrieb: als LaunchAgent registrieren +# (startet bei Login, restart nach Crash, Logs in ~/Library/Logs/) +./deploy/install-launchagent.sh ``` Test vom LXC aus: diff --git a/mac-worker/README.md b/mac-worker/README.md index c54e882..6196d77 100644 --- a/mac-worker/README.md +++ b/mac-worker/README.md @@ -28,7 +28,7 @@ launchctl setenv OLLAMA_HOST "0.0.0.0:11434" brew services restart ollama ``` -## Worker starten +## Worker starten — Vordergrund (Test) ```bash uvicorn app.main:app --host 0.0.0.0 --port 8080 @@ -39,6 +39,48 @@ Health-Check vom LXC: curl http://:8080/health ``` +## Worker als Dienst (LaunchAgent — empfohlen für Dauerbetrieb) + +Der Worker soll dauerhaft laufen, nach Login automatisch starten und bei Crash automatisch neu starten. Dafür ein **LaunchAgent** (kein LaunchDaemon — MLX/Metal-GPU-Zugriff braucht User-Context). + +**Installation** (einmalig, nachdem `.venv` und `.env` eingerichtet sind): + +```bash +cd ~/voice-agent/mac-worker +./deploy/install-launchagent.sh +``` + +Das Skript: + +1. prüft, ob `.venv/bin/uvicorn` existiert +2. legt `.env` aus Vorlage an, falls fehlt +3. generiert `~/Library/LaunchAgents/com.voiceagent.worker.plist` mit den passenden Pfaden +4. lädt den Agent (`launchctl load -w …`) +5. testet `http://127.0.0.1:8080/health` + +**Verwaltung:** + +| Aktion | Befehl | +|---|---| +| Status + Logs anzeigen | `./deploy/install-launchagent.sh status` | +| Entfernen | `./deploy/install-launchagent.sh uninstall` | +| Update (neue Version pullen) | `git pull && pip install -r requirements.txt && ./deploy/install-launchagent.sh` | +| Stoppen | `launchctl unload -w ~/Library/LaunchAgents/com.voiceagent.worker.plist` | +| Starten | `launchctl load -w ~/Library/LaunchAgents/com.voiceagent.worker.plist` | +| Live-Logs | `tail -f ~/Library/Logs/voice-agent-worker.{out,err}.log` | + +**Eigenschaften des Agents:** + +- `RunAtLoad` — startet beim Login +- `KeepAlive` mit `Crashed=true`, `SuccessfulExit=false` — Restart nach Crash, kein Loop bei sauberem Exit +- `ThrottleInterval=10` — frühestens 10 s nach Restart erneut starten +- Logs in `~/Library/Logs/voice-agent-worker.{out,err}.log` + +**Wichtig:** der Agent läuft nur, wenn der Mac-User eingeloggt ist. Für headless-Server-Betrieb (Mac Mini ohne Login) entweder: + +- Auto-Login aktivieren (System-Settings → Users), oder +- in einen LaunchDaemon umbauen — dann fällt allerdings die Apple-Neural-Engine-Beschleunigung weg, MLX läuft nur auf der CPU. + ## API-Endpunkte | Methode | Pfad | Zweck | diff --git a/mac-worker/deploy/com.voiceagent.worker.plist.template b/mac-worker/deploy/com.voiceagent.worker.plist.template new file mode 100644 index 0000000..1956f86 --- /dev/null +++ b/mac-worker/deploy/com.voiceagent.worker.plist.template @@ -0,0 +1,54 @@ + + + + + Label + com.voiceagent.worker + + WorkingDirectory + __WORKDIR__ + + ProgramArguments + + __WORKDIR__/.venv/bin/uvicorn + app.main:app + --host + 0.0.0.0 + --port + 8080 + + + EnvironmentVariables + + PATH + __WORKDIR__/.venv/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin + HOME + __HOME__ + PYTHONUNBUFFERED + 1 + + + RunAtLoad + + + KeepAlive + + SuccessfulExit + + Crashed + + + + ThrottleInterval + 10 + + ProcessType + Interactive + + StandardOutPath + __HOME__/Library/Logs/voice-agent-worker.out.log + + StandardErrorPath + __HOME__/Library/Logs/voice-agent-worker.err.log + + diff --git a/mac-worker/deploy/install-launchagent.sh b/mac-worker/deploy/install-launchagent.sh new file mode 100755 index 0000000..acb8e6b --- /dev/null +++ b/mac-worker/deploy/install-launchagent.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Installiert den Voice-Agent Mac-Worker als LaunchAgent. +# Idempotent — kann zum Update wiederholt laufen. +# +# Nutzung: +# ./install-launchagent.sh # installieren / aktualisieren +# ./install-launchagent.sh uninstall # entfernen +# ./install-launchagent.sh status # Status + letzte Logs +set -euo pipefail + +LABEL="com.voiceagent.worker" +PLIST_DST="$HOME/Library/LaunchAgents/${LABEL}.plist" +LOG_DIR="$HOME/Library/Logs" + +# Verzeichnis dieses Skripts → Repo-Root/mac-worker +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKDIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TEMPLATE="$SCRIPT_DIR/com.voiceagent.worker.plist.template" + +cmd="${1:-install}" + +uninstall() { + if [ -f "$PLIST_DST" ]; then + echo "[*] Stoppe und entlade LaunchAgent…" + launchctl unload "$PLIST_DST" 2>/dev/null || true + rm -f "$PLIST_DST" + echo "[✓] $PLIST_DST entfernt." + else + echo "[i] Kein LaunchAgent installiert ($PLIST_DST nicht vorhanden)." + fi +} + +show_status() { + echo "── launchctl list | grep $LABEL ─────────────────────────" + launchctl list | grep "$LABEL" || echo "(nicht geladen)" + echo + echo "── Health-Check ─────────────────────────────────────────" + curl -fsS http://127.0.0.1:8080/health 2>/dev/null && echo || echo "(nicht erreichbar)" + echo + echo "── stderr.log (letzte 30 Zeilen) ────────────────────────" + tail -n 30 "$LOG_DIR/voice-agent-worker.err.log" 2>/dev/null || echo "(keine Logs)" +} + +install_agent() { + # Vorbedingungen prüfen + if [ ! -x "$WORKDIR/.venv/bin/uvicorn" ]; then + echo "FEHLER: $WORKDIR/.venv/bin/uvicorn nicht gefunden." >&2 + echo "Bitte zuerst die Python-Umgebung einrichten:" >&2 + echo " cd $WORKDIR && python3.11 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt" >&2 + exit 1 + fi + + if [ ! -f "$WORKDIR/.env" ]; then + echo "WARNUNG: $WORKDIR/.env fehlt — kopiere aus .env.example." + cp "$WORKDIR/.env.example" "$WORKDIR/.env" + fi + + mkdir -p "$LOG_DIR" "$HOME/Library/LaunchAgents" + + echo "[1/4] Generiere plist mit WORKDIR=$WORKDIR" + sed \ + -e "s|__WORKDIR__|$WORKDIR|g" \ + -e "s|__HOME__|$HOME|g" \ + "$TEMPLATE" > "$PLIST_DST" + + echo "[2/4] Falls bereits geladen: vorher entladen" + launchctl unload "$PLIST_DST" 2>/dev/null || true + + echo "[3/4] Lade LaunchAgent" + launchctl load -w "$PLIST_DST" + + echo "[4/4] Warte 2 s und prüfe Health-Endpoint" + sleep 2 + if curl -fsS http://127.0.0.1:8080/health >/dev/null; then + echo + echo "[✓] Voice-Agent Mac-Worker läuft." + curl -s http://127.0.0.1:8080/health | python3 -m json.tool 2>/dev/null || true + else + echo + echo "[!] Health-Check fehlgeschlagen. Logs:" + echo " $LOG_DIR/voice-agent-worker.err.log" + echo " $LOG_DIR/voice-agent-worker.out.log" + tail -n 20 "$LOG_DIR/voice-agent-worker.err.log" 2>/dev/null || true + exit 1 + fi + + echo + echo "── Verwaltung ───────────────────────────────────────────" + echo " Stoppen / Starten: launchctl unload|load -w $PLIST_DST" + echo " Status: $0 status" + echo " Entfernen: $0 uninstall" + echo " Logs (live): tail -f $LOG_DIR/voice-agent-worker.{out,err}.log" +} + +case "$cmd" in + install) install_agent ;; + uninstall) uninstall ;; + status) show_status ;; + *) + echo "Nutzung: $0 [install|uninstall|status]" >&2 + exit 1 + ;; +esac