Erinnerungen & Routinen (A3): Wecker in MC2 + Hermes-Tools
- services/reminders.py: persistenter Store (/srv/models/mc2-reminders.json), Wecker-Loop feuert in Briefkasten (Lucy spricht) + Telegram; repeat daily/weekdays/weekly; naive Zeiten = Commander-TZ (Europe/Berlin, Box ist UTC); verpasste Einmal-Erinnerungen feuern verspätet nach (ehrlich markiert) - routers/reminders.py: GET/POST/DELETE /api/reminders - mcp_mc.py: reminder_create/list/delete (Guards, ISO aus Sofort-Kontext) - announce.notify_telegram als gemeinsamer Helper (sentry nutzt ihn jetzt) - requirements: tzdata (zoneinfo-Fallback, Windows-Dev) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+4
-2
@@ -18,8 +18,8 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from config import FRONTEND_DIST, VERSION
|
from config import FRONTEND_DIST, VERSION
|
||||||
from routers import agent, connect, gateway_proxy, health, maintenance, memory, models, routing, system, voice
|
from routers import agent, connect, gateway_proxy, health, maintenance, memory, models, reminders as reminders_router, routing, system, voice
|
||||||
from services import sentry, warmer
|
from services import reminders, sentry, warmer
|
||||||
|
|
||||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||||
# für alle Module (logging.getLogger(__name__)).
|
# für alle Module (logging.getLogger(__name__)).
|
||||||
@@ -39,6 +39,7 @@ async def lifespan(app: FastAPI):
|
|||||||
log.info("Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)", warmer.INTERVAL)
|
log.info("Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)", warmer.INTERVAL)
|
||||||
if sentry.ENABLED:
|
if sentry.ENABLED:
|
||||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||||
|
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
@@ -73,6 +74,7 @@ app.include_router(connect.router)
|
|||||||
app.include_router(memory.router)
|
app.include_router(memory.router)
|
||||||
app.include_router(agent.router)
|
app.include_router(agent.router)
|
||||||
app.include_router(voice.router) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab)
|
app.include_router(voice.router) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab)
|
||||||
|
app.include_router(reminders_router.router) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten
|
||||||
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
||||||
app.include_router(maintenance.router)
|
app.include_router(maintenance.router)
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ ruamel.yaml>=0.18
|
|||||||
psutil>=5.9
|
psutil>=5.9
|
||||||
huggingface_hub>=0.27
|
huggingface_hub>=0.27
|
||||||
mcp>=1.2.0
|
mcp>=1.2.0
|
||||||
|
tzdata>=2024.1
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""Erinnerungen & Routinen (A3) — dünner REST-Layer über services/reminders.py.
|
||||||
|
Konsument ist v.a. der Hermes-Agent via mcp_mc.py (reminder_create/list/delete);
|
||||||
|
LAN-only wie alle MC2-Endpoints."""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from services import reminders
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
|
class ReminderIn(BaseModel):
|
||||||
|
text: str # was angesagt werden soll
|
||||||
|
when: str # ISO-8601; ohne Offset = Commander-Zeitzone (Europe/Berlin)
|
||||||
|
repeat: str = "" # '' einmalig | daily | weekdays | weekly
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/reminders")
|
||||||
|
def list_reminders() -> dict:
|
||||||
|
return {"items": reminders.list_all()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reminders")
|
||||||
|
def create_reminder(body: ReminderIn) -> dict:
|
||||||
|
try:
|
||||||
|
return {"ok": True, "item": reminders.create(body.text, body.when, body.repeat)}
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(400, str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/reminders/{reminder_id}")
|
||||||
|
def delete_reminder(reminder_id: int) -> dict:
|
||||||
|
try:
|
||||||
|
return {"ok": True, "deleted": reminders.delete(reminder_id)}
|
||||||
|
except KeyError as exc:
|
||||||
|
raise HTTPException(404, str(exc.args[0]))
|
||||||
@@ -13,6 +13,8 @@ MAX_ITEMS Einträge, ein Lock für die FastAPI-Threadpool-Worker.
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -21,6 +23,8 @@ from config import MODELS_DIR
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
NOTIFY_SH = str(Path(__file__).resolve().parent.parent.parent / "deploy" / "notify.sh")
|
||||||
|
|
||||||
STORE_PATH = Path(os.environ.get("MC_ANNOUNCE_STORE", str(MODELS_DIR / "mc2-announce.json")))
|
STORE_PATH = Path(os.environ.get("MC_ANNOUNCE_STORE", str(MODELS_DIR / "mc2-announce.json")))
|
||||||
MAX_ITEMS = int(os.environ.get("MC_ANNOUNCE_MAX", "200"))
|
MAX_ITEMS = int(os.environ.get("MC_ANNOUNCE_MAX", "200"))
|
||||||
|
|
||||||
@@ -72,6 +76,19 @@ def add(text: str, subject: str = "", source: str = "", priority: str = "normal"
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def notify_telegram(subject: str, text: str) -> None:
|
||||||
|
"""Best-effort auch auf Telegram (User ist evtl. nicht am PC). MC_NOTIFY_NO_ANNOUNCE=1
|
||||||
|
verhindert, dass notify.sh die Meldung ZURÜCK in den Briefkasten spiegelt — der Absender
|
||||||
|
(Wächter/Erinnerung) hat sie dort schon selbst abgelegt. Auf Windows (Dev) ein No-op."""
|
||||||
|
if not (os.name == "posix" and shutil.which("bash")):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
subprocess.run(["bash", NOTIFY_SH, "-s", subject, text],
|
||||||
|
timeout=30, capture_output=True, env={**os.environ, "MC_NOTIFY_NO_ANNOUNCE": "1"})
|
||||||
|
except Exception:
|
||||||
|
log.warning("notify_telegram: notify.sh fehlgeschlagen", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
def list_after(after: int | None, limit: int = 20) -> dict:
|
def list_after(after: int | None, limit: int = 20) -> dict:
|
||||||
"""Einträge NACH Cursor `after` (aufsteigend). Ohne Cursor nur den aktuellen
|
"""Einträge NACH Cursor `after` (aufsteigend). Ohne Cursor nur den aktuellen
|
||||||
Stand liefern (latest) — so initialisiert Lucy ihren Cursor, ohne Altes nachzuplappern."""
|
Stand liefern (latest) — so initialisiert Lucy ihren Cursor, ohne Altes nachzuplappern."""
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
"""
|
||||||
|
Erinnerungen & Routinen (Lucy-Proaktivität, Faden A3).
|
||||||
|
|
||||||
|
„Lucy, erinner mich morgen um 9 an …" — der Hermes-Agent legt per Tool (mcp_mc.py:
|
||||||
|
reminder_create) einen Eintrag an; der Wecker-Loop hier feuert ihn zur Zeit in den
|
||||||
|
Melde-Briefkasten (announce.py → Lucy spricht) und parallel auf Telegram.
|
||||||
|
|
||||||
|
Bewusst OHNE Hermes-cron: der Wecker läuft deterministisch im MC2-Backend (systemd,
|
||||||
|
überlebt Gateway-Ausfälle) und bleibt in UNSEREN Schichten (Hermes-Quellcode/Config
|
||||||
|
bleibt Auto-Update-Kanal, Verdikt Faden 11c).
|
||||||
|
|
||||||
|
Zeiten: ISO-8601. Ohne Offset gilt die Zeitzone des Commanders (MC_LOCAL_TZ,
|
||||||
|
Default Europe/Berlin) — die Box selbst läuft auf UTC, naive Zeiten dürfen daher
|
||||||
|
NIE als Box-Lokalzeit interpretiert werden. Wiederholung: daily | weekdays | weekly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
from config import MODELS_DIR
|
||||||
|
from services import announce
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STORE_PATH = Path(os.environ.get("MC_REMINDERS_STORE", str(MODELS_DIR / "mc2-reminders.json")))
|
||||||
|
LOCAL_TZ = ZoneInfo(os.environ.get("MC_LOCAL_TZ", "Europe/Berlin"))
|
||||||
|
INTERVAL = int(os.environ.get("MC_REMINDERS_INTERVAL", "20")) # Wecker-Tick (Sekunden)
|
||||||
|
MAX_ITEMS = int(os.environ.get("MC_REMINDERS_MAX", "100"))
|
||||||
|
REPEATS = ("", "daily", "weekdays", "weekly")
|
||||||
|
LATE_NOTE_S = 600 # feuert >10 min zu spät (Box war aus) → ehrlich dazusagen
|
||||||
|
|
||||||
|
_lock = threading.Lock()
|
||||||
|
_state: dict | None = None # {"next_id": int, "items": [...]}
|
||||||
|
|
||||||
|
|
||||||
|
def _load() -> dict:
|
||||||
|
global _state
|
||||||
|
if _state is None:
|
||||||
|
try:
|
||||||
|
_state = json.loads(STORE_PATH.read_text(encoding="utf-8"))
|
||||||
|
assert isinstance(_state.get("next_id"), int) and isinstance(_state.get("items"), list)
|
||||||
|
except Exception:
|
||||||
|
_state = {"next_id": 1, "items": []}
|
||||||
|
return _state
|
||||||
|
|
||||||
|
|
||||||
|
def _save(state: dict) -> None:
|
||||||
|
try:
|
||||||
|
tmp = STORE_PATH.with_suffix(".tmp")
|
||||||
|
tmp.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
|
||||||
|
tmp.replace(STORE_PATH)
|
||||||
|
except OSError:
|
||||||
|
log.warning("reminders: Store %s nicht schreibbar", STORE_PATH, exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_when(when: str) -> datetime:
|
||||||
|
"""ISO-8601 → bewusste Zeit. Naive Angaben = Commander-Zeitzone (NICHT Box-UTC)."""
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(when.strip())
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("Zeit nicht lesbar — bitte ISO-8601 mit Datum UND Uhrzeit, z.B. 2026-07-04T09:00.")
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
dt = dt.replace(tzinfo=LOCAL_TZ)
|
||||||
|
return dt
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt(ts: float) -> str:
|
||||||
|
"""Menschlich lesbare Commander-Lokalzeit (fürs Tool-Echo/Listing)."""
|
||||||
|
return datetime.fromtimestamp(ts, LOCAL_TZ).strftime("%a %d.%m.%Y %H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
def _advance(ts: float, repeat: str) -> float:
|
||||||
|
"""Nächste Wiederholung NACH jetzt (holt verpasste Termine ohne Mehrfach-Feuern auf)."""
|
||||||
|
dt = datetime.fromtimestamp(ts, LOCAL_TZ)
|
||||||
|
now = time.time()
|
||||||
|
while dt.timestamp() <= now:
|
||||||
|
dt += timedelta(days=7 if repeat == "weekly" else 1)
|
||||||
|
if repeat == "weekdays":
|
||||||
|
while dt.weekday() > 4: # Sa/So überspringen
|
||||||
|
dt += timedelta(days=1)
|
||||||
|
return dt.timestamp()
|
||||||
|
|
||||||
|
|
||||||
|
def create(text: str, when: str, repeat: str = "") -> dict:
|
||||||
|
text = (text or "").strip()
|
||||||
|
if not text:
|
||||||
|
raise ValueError("Leerer Erinnerungs-Text.")
|
||||||
|
repeat = (repeat or "").strip().lower()
|
||||||
|
if repeat == "once":
|
||||||
|
repeat = ""
|
||||||
|
if repeat not in REPEATS:
|
||||||
|
raise ValueError("repeat muss leer, daily, weekdays oder weekly sein.")
|
||||||
|
ts = _parse_when(when).timestamp()
|
||||||
|
if repeat:
|
||||||
|
if ts <= time.time(): # Startpunkt vorbei → auf die nächste Wiederholung rücken (Uhrzeit bleibt exakt)
|
||||||
|
ts = _advance(ts, repeat)
|
||||||
|
elif ts <= time.time() - 60:
|
||||||
|
raise ValueError(f"Die Zeit liegt in der Vergangenheit ({_fmt(ts)}) — bitte neu umrechnen "
|
||||||
|
f"(jetzt ist {_fmt(time.time())}).")
|
||||||
|
with _lock:
|
||||||
|
state = _load()
|
||||||
|
if len(state["items"]) >= MAX_ITEMS:
|
||||||
|
raise ValueError(f"Zu viele offene Erinnerungen (max {MAX_ITEMS}).")
|
||||||
|
item = {"id": state["next_id"], "text": text[:500], "next_ts": ts,
|
||||||
|
"repeat": repeat, "created_ts": time.time()}
|
||||||
|
state["next_id"] += 1
|
||||||
|
state["items"].append(item)
|
||||||
|
_save(state)
|
||||||
|
log.info("reminder #%s angelegt: %s → %s%s", item["id"], _fmt(ts), text[:60],
|
||||||
|
f" (Routine {repeat})" if repeat else "")
|
||||||
|
return {**item, "when_local": _fmt(ts)}
|
||||||
|
|
||||||
|
|
||||||
|
def list_all() -> list[dict]:
|
||||||
|
with _lock:
|
||||||
|
items = sorted(_load()["items"], key=lambda i: i["next_ts"])
|
||||||
|
return [{**i, "when_local": _fmt(i["next_ts"])} for i in items]
|
||||||
|
|
||||||
|
|
||||||
|
def delete(reminder_id: int) -> dict:
|
||||||
|
with _lock:
|
||||||
|
state = _load()
|
||||||
|
for i, item in enumerate(state["items"]):
|
||||||
|
if item["id"] == reminder_id:
|
||||||
|
state["items"].pop(i)
|
||||||
|
_save(state)
|
||||||
|
return {**item, "when_local": _fmt(item["next_ts"])}
|
||||||
|
raise KeyError(f"Erinnerung {reminder_id} nicht gefunden.")
|
||||||
|
|
||||||
|
|
||||||
|
def _fire_due() -> None:
|
||||||
|
now = time.time()
|
||||||
|
with _lock:
|
||||||
|
state = _load()
|
||||||
|
due = [i for i in state["items"] if i["next_ts"] <= now]
|
||||||
|
if not due:
|
||||||
|
return
|
||||||
|
for item in due:
|
||||||
|
if item["repeat"]:
|
||||||
|
item["next_ts"] = _advance(item["next_ts"], item["repeat"])
|
||||||
|
else:
|
||||||
|
state["items"].remove(item)
|
||||||
|
_save(state)
|
||||||
|
for item in due:
|
||||||
|
late = now - item["next_ts"] > LATE_NOTE_S if not item["repeat"] else False
|
||||||
|
text = f"Erinnerung, Commander: {item['text']}"
|
||||||
|
if late:
|
||||||
|
text += f" (Eigentlich fällig um {_fmt(item['next_ts'])} — die Box war wohl aus.)"
|
||||||
|
announce.add(text, subject="[Erinnerung]", source="reminder")
|
||||||
|
announce.notify_telegram("[Erinnerung]", item["text"])
|
||||||
|
log.info("reminder #%s gefeuert%s", item["id"], " (Routine)" if item["repeat"] else "")
|
||||||
|
|
||||||
|
|
||||||
|
async def reminders_loop() -> None:
|
||||||
|
"""Wecker (Hintergrund-Task im MC2-Lifespan). Verpasste einmalige Erinnerungen
|
||||||
|
(Box war aus) feuern beim nächsten Tick nach — ehrlich als verspätet markiert."""
|
||||||
|
import asyncio
|
||||||
|
log.info("reminders: Wecker aktiv (Tick %ss, Zeitzone %s)", INTERVAL, LOCAL_TZ)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await asyncio.to_thread(_fire_due)
|
||||||
|
except Exception:
|
||||||
|
log.debug("reminders: Tick fehlgeschlagen", exc_info=True)
|
||||||
|
await asyncio.sleep(INTERVAL)
|
||||||
@@ -16,8 +16,6 @@ Braucht KEIN sudo, keine neuen Dienste — läuft als asyncio-Task im MC2-Backen
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -34,8 +32,6 @@ START_DELAY = int(os.environ.get("MC_SENTRY_START_DELAY", "90")) # Dienste nac
|
|||||||
FAIL_AFTER = int(os.environ.get("MC_SENTRY_FAIL_AFTER", "3")) # Fehl-Ticks bis Alarm (3×120s = 6 min)
|
FAIL_AFTER = int(os.environ.get("MC_SENTRY_FAIL_AFTER", "3")) # Fehl-Ticks bis Alarm (3×120s = 6 min)
|
||||||
REMIND_S = int(os.environ.get("MC_SENTRY_REMIND_S", "21600")) # Erinnerung bei Dauerproblem: 6 h
|
REMIND_S = int(os.environ.get("MC_SENTRY_REMIND_S", "21600")) # Erinnerung bei Dauerproblem: 6 h
|
||||||
DISK_ALARM_PCT = float(os.environ.get("MC_SENTRY_DISK_PCT", "90"))
|
DISK_ALARM_PCT = float(os.environ.get("MC_SENTRY_DISK_PCT", "90"))
|
||||||
NOTIFY_SH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
|
||||||
"deploy", "notify.sh")
|
|
||||||
|
|
||||||
|
|
||||||
def _reach(url: str, path: str = "/health") -> bool:
|
def _reach(url: str, path: str = "/health") -> bool:
|
||||||
@@ -102,16 +98,9 @@ _watches: dict[str, _Watch] = {name: _Watch() for name in CHECKS}
|
|||||||
|
|
||||||
|
|
||||||
def _notify_telegram(subject: str, text: str) -> None:
|
def _notify_telegram(subject: str, text: str) -> None:
|
||||||
"""Best-effort auch auf Telegram (User ist evtl. nicht am PC). notify.sh spiegelt
|
"""Telegram-Direktweg (gemeinsamer Helper in announce.py); Briefkasten-Eintrag
|
||||||
seinerseits in den Briefkasten — als Quelle 'sentry' markierte Einträge legt der
|
legt der Wächter selbst ab."""
|
||||||
Wächter aber schon selbst ab, darum hier der Direktweg NUR für Telegram."""
|
announce.notify_telegram(subject, text + " (Diese Meldung kam auch an Lucy.)")
|
||||||
if not (os.name == "posix" and shutil.which("bash")):
|
|
||||||
return # Dev auf Windows: kein hermes/notify
|
|
||||||
try:
|
|
||||||
subprocess.run(["bash", NOTIFY_SH, "-s", subject, text + " (Diese Meldung kam auch an Lucy.)"],
|
|
||||||
timeout=30, capture_output=True, env={**os.environ, "MC_NOTIFY_NO_ANNOUNCE": "1"})
|
|
||||||
except Exception:
|
|
||||||
log.warning("sentry: notify.sh fehlgeschlagen", exc_info=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _tick() -> None:
|
def _tick() -> None:
|
||||||
|
|||||||
@@ -273,6 +273,46 @@ def apply_update(kind: str) -> str:
|
|||||||
return json.dumps(_post(routes[kind]), ensure_ascii=False, indent=2)
|
return json.dumps(_post(routes[kind]), ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Erinnerungen & Routinen (A3): die Box sagt dem Commander zur Zeit aktiv Bescheid ─────────
|
||||||
|
@mcp.tool()
|
||||||
|
def reminder_create(text: str, when: str, repeat: str = "") -> str:
|
||||||
|
"""Legt eine Erinnerung an: zur angegebenen Zeit sagt die Box dem Commander den Text AKTIV
|
||||||
|
an (Lucy spricht + Telegram). NUR nutzen, wenn der Commander ausdrücklich erinnert werden
|
||||||
|
will ('erinner mich', 'sag mir um', 'jeden Morgen').
|
||||||
|
when = ISO-8601 mit Datum UND Uhrzeit in der Lokalzeit des Commanders, z.B. 2026-07-04T09:00.
|
||||||
|
Relative Angaben ('in 20 Minuten', 'morgen früh') selbst aus der aktuellen Zeit im Kontext
|
||||||
|
umrechnen. repeat: '' einmalig | daily (täglich) | weekdays (Mo–Fr) | weekly (wöchentlich).
|
||||||
|
text = die Ansage selbst, kurz und direkt (ohne 'Erinnerung:' davor — das ergänzt die Box)."""
|
||||||
|
try:
|
||||||
|
item = _post("/api/reminders", {"text": text, "when": when, "repeat": repeat})["item"]
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
return f"Fehlgeschlagen: {e.response.json().get('detail', e.response.text[:200])}"
|
||||||
|
rep = {"daily": " (täglich)", "weekdays": " (werktags)", "weekly": " (wöchentlich)"}.get(item["repeat"], "")
|
||||||
|
return f"Erinnerung #{item['id']} angelegt: {item['when_local']}{rep} — „{item['text']}“"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def reminder_list() -> str:
|
||||||
|
"""Zeigt alle anstehenden Erinnerungen/Routinen (Nummer, Zeit, Text). Vor dem Löschen
|
||||||
|
hiermit die Nummer ermitteln."""
|
||||||
|
items = _get("/api/reminders")["items"]
|
||||||
|
if not items:
|
||||||
|
return "Keine Erinnerungen angelegt."
|
||||||
|
rep = {"daily": " · täglich", "weekdays": " · werktags", "weekly": " · wöchentlich"}
|
||||||
|
return "\n".join(f"#{i['id']} · {i['when_local']}{rep.get(i['repeat'], '')} — {i['text']}" for i in items)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def reminder_delete(reminder_id: int) -> str:
|
||||||
|
"""Löscht eine Erinnerung/Routine endgültig (Nummer aus reminder_list). NUR auf
|
||||||
|
ausdrücklichen Wunsch des Commanders."""
|
||||||
|
try:
|
||||||
|
gone = _delete(f"/api/reminders/{reminder_id}")["deleted"]
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
return f"Fehlgeschlagen: {e.response.json().get('detail', e.response.text[:200])}"
|
||||||
|
return f"Erinnerung #{gone['id']} gelöscht ({gone['when_local']} — „{gone['text']}“)."
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def service_logs(service: str, lines: int = 100) -> str:
|
def service_logs(service: str, lines: int = 100) -> str:
|
||||||
"""Liest die letzten Log-Zeilen eines Dienstes (Diagnose). service z.B. hermes-gateway,
|
"""Liest die letzten Log-Zeilen eines Dienstes (Diagnose). service z.B. hermes-gateway,
|
||||||
|
|||||||
Reference in New Issue
Block a user