Files
ARIA-AGENT/aria-brain/prompts.py
T
duffyduck 07c761fc72 feat(brain): GPS-Variablen + near()-Helper + erweiterte Condition-Vars
ARIA kann jetzt GPS-basierte Watcher-Trigger anlegen (Blitzer-Warner-Use-Case),
plus erweiterte Time-, System- und Activity-Variablen.

bridge/aria_bridge.py
  _persist_state() schreibt atomar nach /shared/state/<key>.json.
  Bei jedem chat- und audio-Event:
    - location → /shared/state/location.json {lat, lon, ts_unix}
    - last_user_ts → /shared/state/activity.json
  Brain-Watcher lesen das fuer die GPS- und Activity-Variablen.

aria-brain/watcher.py — komplett ueberarbeitet
  Neue Variablen-Sets:
    GPS:       current_lat, current_lon, location_age_sec (-1 = nie gesehen)
    Zeit (+):  minute_of_hour, day_of_month, month, year, is_weekend, unix_timestamp
    System:    ram_free_mb (MemAvailable), cpu_load_1min (loadavg)
    Activity:  last_user_message_ago_sec
    Memory:    pinned_count (zusaetzlich zu memory_count)

  Neue Funktion fuer Conditions:
    near(lat, lon, radius_m)  Haversine-Distanz von current_lat/lon
                              zum Punkt. False wenn keine Position bekannt.

  Parser-Erweiterung:
    ast.Call jetzt erlaubt, ABER nur fuer direkte Funktionsnamen aus der
    Whitelist (_ALLOWED_FUNCTIONS = {"near"}). Keine Attribute-Access,
    keine Keywords, Args nur Constants/Names/UnaryOp.
  Selbsttest blockt korrekt:
    __import__("os")...           → "Funktionsaufruf nur ueber direkten Namen"
    memory_count.__class__         → "Verbotener Ausdruck: Attribute"
    (lambda: 1)()                  → "Funktionsaufruf nur ueber direkten Namen"

aria-brain/main.py
  /triggers/conditions liefert jetzt zusaetzlich {functions:[...]} mit
  Signaturen + Beschreibungen. current-Snapshot filtert callable() raus
  damit JSON serialisierbar bleibt.

aria-brain/prompts.py + agent.py
  build_triggers_section bekommt condition_funcs als 4tes Argument und
  listet die im System-Prompt unter "Verfuegbare Funktionen". Operatoren-
  Hinweis ergaenzt mit Beispielen + Regeln (keine Variablen in Funktions-
  Args, keine Schachtelung).

diagnostic/index.html
  Trigger-Create-Modal: Variablen-Info-Block zeigt jetzt sowohl Variablen
  (mit aktuellen Werten) als auch Funktionen (Signatur + Beschreibung).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:52:13 +02:00

181 lines
7.8 KiB
Python

"""
System-Prompt-Bau aus Memory-Punkten.
Strategie:
1. Alle pinned Punkte (Hot Memory) — gruppiert nach Type — in den
System-Prompt schreiben. IMMER drin.
2. Top-K semantisch aehnliche Punkte (Cold Memory) zur aktuellen
User-Nachricht — als "Moeglicherweise relevant" eingehaengt.
3. Aktive Skills als kompakte Liste (nur Name + Description) — damit
ARIA weiss was sie hat.
Phase B Punkt 1: nur Hot-Memory-Bau, Skills + Cold-Search kommen
mit dem Conversation-Loop in spaeteren Phasen.
"""
from __future__ import annotations
from typing import List
from memory import MemoryPoint
TYPE_HEADINGS = {
"identity": "## Wer du bist",
"rule": "## Sicherheitsregeln & Prinzipien",
"preference": "## Benutzer-Praeferenzen",
"tool": "## Tool-Freigaben",
"skill": "## Deine Skills",
}
def build_hot_memory_section(pinned: List[MemoryPoint]) -> str:
"""Baue den 'IMMER-im-Prompt'-Block aus pinned Punkten."""
grouped: dict[str, List[MemoryPoint]] = {}
for p in pinned:
grouped.setdefault(p.type, []).append(p)
parts: List[str] = []
# Sortier-Reihenfolge: identity → rule → preference → tool → skill → Rest
order = ["identity", "rule", "preference", "tool", "skill"]
for t in order:
items = grouped.pop(t, [])
if not items:
continue
parts.append(TYPE_HEADINGS.get(t, f"## {t}"))
for p in items:
parts.append(f"### {p.title}")
parts.append(p.content.strip())
parts.append("")
# uebrige Types (falls jemand was anderes als pinned markiert)
for t, items in grouped.items():
parts.append(f"## {t}")
for p in items:
parts.append(f"### {p.title}")
parts.append(p.content.strip())
parts.append("")
return "\n".join(parts).strip()
def build_cold_memory_section(matches: List[MemoryPoint]) -> str:
"""Baue 'Moeglicherweise relevant'-Block aus Search-Treffern."""
if not matches:
return ""
lines = ["## Moeglicherweise relevant (aus Gedaechtnis)"]
for p in matches:
score = f" [score={p.score:.2f}]" if p.score is not None else ""
lines.append(f"- **{p.title}**{score}")
lines.append(f" {p.content.strip()}")
return "\n".join(lines)
def build_skills_section(skills: List[dict]) -> str:
"""Listet alle Skills (aktiv + deaktiviert) damit ARIA weiss was es gibt
und keine doppelt baut. Plus klare Schwelle wann ein Skill sich lohnt."""
lines = ["## Deine Skills"]
if skills:
for s in skills:
active = s.get("active", True)
marker = "" if active else " [DEAKTIVIERT — kann nicht aufgerufen werden]"
lines.append(f"- **{s.get('name', '?')}**{marker}{s.get('description', '(ohne Beschreibung)')}")
lines.append("")
lines.append("Wenn ein vorhandener Skill zur Aufgabe passt: nutze ihn via Tool-Call.")
else:
lines.append("(noch keine Skills vorhanden)")
lines.append("")
lines.append("### Wann lohnt sich ein neuer Skill?")
lines.append("")
lines.append("**Skills sind IMMER Python** — eigene venv pro Skill mit den noetigen "
"pip-Paketen. Kein apt im Skill, kein systemweiter Install. Python deckt "
"in der Regel alles ab (yt-dlp, requests, pypdf, pillow, openpyxl, "
"static-ffmpeg, beautifulsoup4, …). Falls etwas WIRKLICH nur via apt geht: "
"Stefan fragen ob es ins Brain-Dockerfile soll.")
lines.append("")
lines.append("**Harte Regel — IMMER Skill anlegen wenn:** die Loesung erfordert eine "
"pip-Library. Begruendung: Brain-Container hat keinen persistenten State "
"ausser /data/skills/. Ohne Skill wuerde der Install bei jedem "
"Container-Restart wiederholt.")
lines.append("")
lines.append("**Sonst — Skill nur wenn alle vier zutreffen:**")
lines.append("")
lines.append("1. **Wiederkehrend** — die Aufgabe wird realistisch nochmal gestellt. "
"Einmal-Faelle (\"wie spaet ist es jetzt\") kein Skill.")
lines.append("2. **Nicht-trivial** — mehrere Schritte. Ein einzelner Shell-Befehl "
"(`date`, `hostname`, `ls`) ist KEIN Skill — das macht Bash direkt.")
lines.append("3. **Parametrisierbar** — der Skill nimmt Eingaben (URL, Datei, Suchbegriff) "
"und gibt ein nuetzliches Ergebnis zurueck.")
lines.append("4. **Wiederverwendbar als ganzes** — Stefan wuerde es zukuenftig per Name "
"ansprechen (\"mach mir den YouTube zu MP3\") statt jedes Mal zu erklaeren.")
lines.append("")
lines.append("Wenn nichts installiert werden muss UND nicht alle vier zutreffen: einfach "
"die Aufgabe loesen ohne Skill anzulegen. Stefan kann jederzeit sagen "
"'bau daraus einen Skill'.")
return "\n".join(lines)
def build_triggers_section(
triggers: List[dict],
condition_vars: List[dict],
condition_funcs: List[dict] | None = None,
) -> str:
"""Triggers (passive Aufweck-Quellen) + verfuegbare Condition-Variablen + Funktionen."""
lines = ["## Trigger (passive Aufweck-Quellen)"]
lines.append("")
lines.append("Trigger sind ANDERS als Skills: das System ruft DICH wenn ein Event passiert. "
"Du legst sie an wenn Stefan sagt 'erinner mich an X' oder 'sag bescheid wenn Y'.")
lines.append("")
if triggers:
lines.append("### Aktuelle Trigger")
for t in triggers:
active = t.get("active", True)
mark = "" if active else " [INAKTIV]"
if t["type"] == "timer":
lines.append(f"- **{t['name']}**{mark} (timer) feuert {t.get('fires_at')}: \"{t.get('message','')[:80]}\"")
elif t["type"] == "watcher":
lines.append(f"- **{t['name']}**{mark} (watcher) cond=`{t.get('condition')}`: \"{t.get('message','')[:80]}\"")
lines.append("")
lines.append("### Verfuegbare Condition-Variablen (fuer Watcher)")
for v in condition_vars:
lines.append(f"- `{v['name']}` ({v['type']}) — {v['desc']}")
if condition_funcs:
lines.append("")
lines.append("### Verfuegbare Funktionen in Conditions")
for fn in condition_funcs:
lines.append(f"- `{fn['signature']}` — {fn['desc']}")
lines.append("")
lines.append("Operatoren in Conditions: `<` `>` `<=` `>=` `==` `!=` `and` `or` `not`. "
"Beispiele: `disk_free_gb < 5 and hour_of_day >= 8`, "
"`day_of_week == \"mon\"`, `near(53.123, 7.456, 500)`. "
"Funktionen nur mit Konstanten als Argumenten (keine Variablen, "
"keine geschachtelten Funktionen).")
lines.append("")
lines.append("### Wann welcher Typ?")
lines.append("- **Timer** fuer einmalige Erinnerungen mit konkreter Zeit ('in 10min', 'um 14:30').")
lines.append("- **Watcher** fuer 'wenn X passiert' (Disk voll, bestimmte Tageszeit, GPS-Naehe).")
lines.append("- ARIA legt Trigger NUR auf Stefan-Wunsch an, nicht eigenmaechtig.")
return "\n".join(lines)
def build_system_prompt(
pinned: List[MemoryPoint],
cold: List[MemoryPoint] | None = None,
skills: List[dict] | None = None,
triggers: List[dict] | None = None,
condition_vars: List[dict] | None = None,
condition_funcs: List[dict] | None = None,
) -> str:
"""Kompletter System-Prompt: Hot + Cold + Skills + Triggers."""
parts = [build_hot_memory_section(pinned)]
if skills:
parts.append("")
parts.append(build_skills_section(skills))
if condition_vars:
parts.append("")
parts.append(build_triggers_section(triggers or [], condition_vars, condition_funcs))
if cold:
parts.append("")
parts.append(build_cold_memory_section(cold))
return "\n".join(parts).strip()