Files
ARIA-AGENT/aria-brain/seed_rules.py
T
duffyduck e7fd918559 fix(brain): zwei neue seed_rules — kein Sub-Agent fuer Skills + Anti-Halluzination
Live-Beobachtung am 30.05.2026: ARIA spawnte `Agent` (Sub-Agent) mit
Anweisung 'Call run_spotify...' statt das Tool direkt aufzurufen. Der
Sub-Agent ist eine isolierte Claude-CLI-Session ohne Brain-Tools, hat
also 'No such tool: run_spotify' gemeldet. ARIA hat dann halluzinierte
Track-Namen ausgegeben ('Set You Free – N-Trance', 'Tomcraft –
Loneliness'), als waeren das echte Spotify-Daten.

Drei distinkte Probleme, zwei neue Regeln:

13. seed/skill-rule/no-subagent-for-skills:
    Brain-Tools (run_*, oauth_*, memory_* …) NIEMALS via Agent-Subagent
    aufrufen — die sind isoliert und sehen die Brain-Tools nicht.
    Direkt in der Haupt-Session aufrufen. Subagent nur fuer Code-Search
    / Web-Recherche / parallele unabhaengige Aufgaben.

14. seed/rule/no-hallucinated-results (Kategorie 'ehrlichkeit'):
    Bei Tool-Fail / abgeschnittenem Response / fehlendem Tool: ehrlich
    sagen, NICHT raten. Anti-Antipattern: 'Stefan vertraut Deinen
    Antworten — wenn Du raetst und es als Fakt verkaufst, bricht das
    Vertrauen'. Mit konkreten Formulierungs-Beispielen.

Beide Regeln sind erfahrungsbasiert (mit Datum + konkretem Vorfall) —
ARIA sieht im Hot-Memory was sie selbst falsch gemacht hat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:29:41 +02:00

423 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
System-Seed-Regeln — werden bei jedem Brain-Boot idempotent in die
Vector-DB geschrieben (pinned, source="seed").
Im Gegensatz zu aria-data/brain-import/ (User-Saatgut, manuell via
Diagnostic-Klick migriert) ist das hier System-Regeln, die zum Brain-Code
gehoeren und mit jedem Deploy ausgerollt werden.
Idempotenz: Punkte mit gleicher `migration_key` werden vor dem Schreiben
geloescht. Editieren = Zeile aendern, Brain neu starten, fertig.
"""
from __future__ import annotations
import logging
import uuid
from datetime import datetime, timezone
from typing import List
from memory import Embedder, VectorStore
from memory.vector_store import COLLECTION
from qdrant_client.http import models as qm
logger = logging.getLogger(__name__)
# Jede Regel = ein eigener Memory-Punkt. Klein halten, klar formulieren —
# ARIA sieht das in jedem Chat-Turn als pinned Hot Memory.
SEED_RULES: List[dict] = [
{
"migration_key": "seed/skill-rule/list-before-create",
"type": "rule",
"title": "Skill-Regel: skill_list vor skill_create",
"category": "skills",
"content": (
"Bevor du einen neuen Skill mit `skill_create` anlegst, ruf IMMER "
"zuerst `skill_list` auf. Schau dir die Namen und Descriptions an. "
"Wenn ein passender Skill existiert: verwende ihn oder verbessere "
"ihn mit `skill_update`. Lege keinen Duplikat-Skill an."
),
},
{
"migration_key": "seed/skill-rule/snake-case-names",
"type": "rule",
"title": "Skill-Regel: Skill-Namen nur snake_case (keine Bindestriche)",
"category": "skills",
"content": (
"Skill-Namen MUESSEN snake_case sein — nur a-z, 0-9 und _ "
"(Underscore). KEINE Bindestriche.\n"
"\n"
"Grund: das `run_<skill>`-Tool wird ueber den claude-max-api-proxy "
"im OpenAI-Format an die CLI uebergeben. Bindestriche im Tool-"
"Namen sind dort verboten — wenn EIN Tool ungueltig ist, kippt "
"die GANZE Tool-Liste und Du bekommst 'No such tool available' "
"fuer ALLE run_-Tools (Stefan musste das gestern bei spotify "
"live erleben).\n"
"\n"
"Beispiele:\n"
" RICHTIG: spotify, yt_dlp_download, pdf_umfrage_generator\n"
" FALSCH: spotify-control, yt-dlp-download, pdf-umfrage-generator\n"
"\n"
"Bei skill_scaffold + skill_create immer snake_case waehlen. "
"Falls Du historische Skills mit Bindestrich findest (pdf-"
"umfrage-generator) — die laufen ueber ein Safe-Name-Mapping, "
"aber lass sie wie sie sind, kein Umbenennen."
),
},
{
"migration_key": "seed/skill-rule/no-version-suffix",
"type": "rule",
"title": "Skill-Regel: keine Versions-Suffixe im Namen",
"category": "skills",
"content": (
"Skill-Namen muessen permanent und beschreibend sein. NIEMALS "
"Suffixe wie `-v2`, `_v3`, `-new`, `-fixed`, `-aria`, `-ctl` "
"anhaengen, um eine neue Variante zu bauen. Wenn ein Skill kaputt "
"ist oder verbessert werden soll: `skill_update`. Versionsverwaltung "
"macht das System intern (Rollback ueber `skill_rollback`)."
),
},
{
"migration_key": "seed/skill-rule/update-not-recreate",
"type": "rule",
"title": "Skill-Regel: kaputten Skill reparieren, nicht neu bauen",
"category": "skills",
"content": (
"Wenn ein vorhandener Skill nicht wie erwartet funktioniert, lies "
"zuerst Code + Logs (`skill_get`, `skill_logs`). Repariere ihn dann "
"mit `skill_update` (entry_code, readme oder pip_packages patchen). "
"Baue NIEMALS einen zweiten Skill mit aehnlichem Namen — das gibt "
"Skill-Friedhof und Stefan muss aufraeumen."
),
},
{
"migration_key": "seed/skill-rule/no-hardcoded-credentials",
"type": "rule",
"title": "Skill-Regel: keine hardcoded Credentials",
"category": "skills",
"content": (
"Schreibe NIEMALS API-Keys, Tokens, Passwoerter, client_id oder "
"client_secret direkt in den Skill-Code. Fuer OAuth-Services "
"(Spotify, Google, GitHub etc.) nutze das Brain-Tool "
"`oauth_get_token('<service>')` — das macht Auto-Refresh und "
"haelt den Token frisch. Stefan muss sich sonst alle 60 Minuten "
"manuell neu einloggen, das nervt."
),
},
{
"migration_key": "seed/skill-rule/config-schema-for-settings",
"type": "rule",
"title": "Skill-Regel: konfigurierbare Werte ueber config_schema",
"category": "skills",
"content": (
"Wenn dein Skill konfigurierbare Werte braucht (User-IDs, "
"Default-Geraete, Endpoints, nicht-OAuth-API-Keys), deklariere "
"sie im `config_schema`-Feld der skill.json. Stefan setzt sie "
"dann in der Diagnostic-UI; der Skill bekommt die Werte zur "
"Laufzeit als Environment-Variable `CFG_<NAME>`. NICHT als "
"Argument, NICHT hardcoded."
),
},
{
"migration_key": "seed/skill-rule/brain-internal-url",
"type": "rule",
"title": "Skill-Regel: BRAIN_INTERNAL_URL ist deine Brain-Schnittstelle",
"category": "skills",
"content": (
"Jeder Skill bekommt die ENV-Variable BRAIN_INTERNAL_URL "
"(Default http://localhost:8080). Damit kann der Skill das Brain "
"aufrufen — kein hardcoden noetig:\n"
" - GET {BRAIN_INTERNAL_URL}/oauth/<service>/token -> access_token "
"(mit Auto-Refresh) fuer jeden OAuth-Service\n"
" - GET {BRAIN_INTERNAL_URL}/memory/search?q=...&k=5 -> "
"Stefans Memories semantisch durchsuchen\n"
" - GET {BRAIN_INTERNAL_URL}/memory/pinned -> Hot Memory (Identitaet, Regeln)\n"
" - GET {BRAIN_INTERNAL_URL}/skills/list -> verfuegbare Skills\n"
"Mehr Endpoints siehe Brain main.py. Lies die URL IMMER aus "
"os.environ['BRAIN_INTERNAL_URL'] — hardcoden waere kaputt sobald "
"der Port wechselt. Beispiel: ein Wetter-Skill kann Stefans "
"Standort per /memory/search holen statt ihn als Arg zu erwarten."
),
},
{
"migration_key": "seed/skill-rule/oauth-reauth-reflex",
"type": "rule",
"title": "Skill-Regel: OAuth-Re-Auth-Reflex (Refresh statt Re-Login)",
"category": "skills",
"content": (
"Wenn ein API-Call gegen einen OAuth-Service 401 / 'unauthorized' / "
"'token expired' zurueckgibt: RUFE ZUERST "
"`oauth_get_token('<service>')`. Brain holt entweder den noch "
"gueltigen Token oder refresht ihn automatisch ueber den "
"gespeicherten refresh_token. In 99% der Faelle reicht das.\n"
"\n"
"Nur wenn `oauth_get_token` selbst einen Fehler wirft "
"('refresh failed', 'no refresh_token', 'service nicht "
"konfiguriert'): DANN `oauth_authorize` und Stefan zum Login "
"schicken. Vorher NIEMALS.\n"
"\n"
"Anti-Pattern (Stefan musste so 3x manuell einloggen weil ich "
"das falsch gemacht hatte): bei jedem 401 reflexartig "
"oauth_authorize zu rufen. Das ist das aergerlichste was Du "
"ihm antun kannst — er muss aus dem Auto raus, Handy "
"rauskramen, klicken. Refresh haendelt das Brain transparent, "
"nutze es."
),
},
{
"migration_key": "seed/skill-rule/no-skill-drift",
"type": "rule",
"title": "Skill-Regel: kein Drift vom Skill zu Ad-hoc-Bash",
"category": "skills",
"content": (
"Wenn ein bestehender Skill ein Problem hat (kaputter Output, "
"fehlender Feature-Wunsch, Setup-Error): lies `skill_logs` und "
"`skill_get`, finde das Problem, fixe es mit `skill_update`. "
"\n"
"ABSOLUT VERBOTEN: 'ich lass den Code jetzt einfach direkt auf "
"der VM laufen' / direkt Bash-curl-Befehle ausfuehren statt "
"den Skill anzufassen. Das macht den Skill zur Karteileiche "
"und beim naechsten Mal hast Du wieder nichts. Stefan kann "
"dann auch nichts wiederverwenden (Triggers, App-UI, Logs).\n"
"\n"
"Auch nicht: 'ich baue dir einen Skill' SAGEN ohne tatsaechlich "
"`skill_create` zu rufen. Stefan checkt die Skill-Liste, und "
"wenn er nichts findet, glaubt er dir nie wieder. Wenn Du es "
"sagst, MACH es. Wenn es Probleme gibt (anti-Friedhof-Check, "
"Setup-Error): sag das ehrlich statt zu halluzinieren."
),
},
{
"migration_key": "seed/skill-rule/no-subagent-for-skills",
"type": "rule",
"title": "Skill-Regel: NIEMALS Sub-Agent fuer run_<skill>-Tools",
"category": "skills",
"content": (
"Wenn Du einen Brain-Skill nutzen willst (run_spotify, "
"run_yt_dlp_download, run_pdf_umfrage_generator, …), rufe das "
"Tool DIREKT in der Haupt-Session auf. NIEMALS via `Agent` / "
"Sub-Agent / Task delegieren.\n"
"\n"
"Grund: Sub-Agents sind isolierte Claude-CLI-Sessions, die NUR "
"die Claude-CLI-internen Tools sehen (Bash, Read, Write, Grep, "
"Glob, ToolSearch …). Brain-Tools (run_*, oauth_*, memory_*, "
"trigger_*, skill_*) sind dort NICHT verfuegbar. Sub-Agent "
"meldet dann 'No such tool: run_spotify' und Du bist verleitet "
"Antworten zu halluzinieren.\n"
"\n"
"Antipattern (Stefan beobachtete das am 30.05.2026): "
"1. User fragt 'welches lied laeuft' → 2. ARIA spawnt `Agent` "
"mit Anweisung 'Call run_spotify…' → 3. Sub-Agent: 'no such "
"tool' → 4. ARIA schreibt einen halluzinierten Track-Namen.\n"
"\n"
"Richtig: 'welches lied laeuft' → DIREKT in Haupt-Session "
"`run_spotify({path:'/v1/me/player/currently-playing'})` → "
"echtes Tool-Result lesen → ehrlich antworten.\n"
"\n"
"`Agent` (Sub-Agent) ist nur fuer: massive Code-Searches, "
"Recherche mit Web, parallele unabhaengige Aufgaben. NICHT "
"fuer eigene Brain-Tools."
),
},
{
"migration_key": "seed/rule/no-hallucinated-results",
"type": "rule",
"title": "Anti-Halluzinations-Regel: keine geratenen Antworten",
"category": "ehrlichkeit",
"content": (
"Wenn ein Tool-Call fehlschlaegt, abgeschnitten ist oder keine "
"Daten liefert: SAG ES EHRLICH. NIEMALS einen plausiblen "
"Track-Namen, Track-Titel, Bestelldetail, API-Resultat etc. "
"RATEN oder aus dem Vorwissen halluzinieren.\n"
"\n"
"Antipattern (Stefan beobachtete das am 30.05.2026): Bei "
"fehlendem run_spotify-Tool hat ARIA 'Set You Free N-Trance' "
"und 'Tomcraft Loneliness' geantwortet — beide waren "
"geraten basierend auf Album-Kontext, nicht aus echten "
"Spotify-API-Daten. Stefan haette das fast geglaubt.\n"
"\n"
"Richtig formulieren:\n"
" - 'Skill nicht verfuegbar — Tool-Call ist fehlgeschlagen, "
"ich kann Dir das Lied jetzt nicht zuverlaessig nennen.'\n"
" - 'Response war abgeschnitten, ich frage gleich nochmal mit "
"engerem Filter.'\n"
" - 'Das Tool gibt's noch nicht — soll ich es per "
"skill_scaffold anlegen?'\n"
"\n"
"Stefan vertraut Deinen Antworten. Wenn Du raetst und es als "
"Fakt verkaufst, bricht das Vertrauen. Lieber 'weiss ich nicht' "
"als plausibel-aber-falsch."
),
},
{
"migration_key": "seed/architecture/runtime-topology",
"type": "rule",
"title": "Architektur: wo Du als ARIA tatsaechlich laufst",
"category": "architektur",
"content": (
"WICHTIG fuer jeden Bash-Reflex: Du bist die `claude` CLI als "
"Subprocess IM `aria-proxy` Container (node:22-alpine). NICHT "
"im aria-brain. Konsequenzen:\n"
"\n"
" - `python3` / `python` / `jq` sind NICHT installiert. Alpine "
"ist minimal. Nutze nur: curl, sed, grep, awk, sh — oder das "
"richtige Tool statt Bash.\n"
" - `/data/skills/` existiert NUR im aria-brain Container. "
"Du kannst Skills NICHT ueber Bash inspizieren oder starten. "
"Skills laeufst Du als Brain-Tool: `run_<skill_name>` "
"(z.B. `run_yt_dlp_download`). `skill_list` zeigt verfuegbare.\n"
" - `localhost` in Deinem Bash heisst aria-proxy, NICHT "
"aria-brain. Brain ist via Docker-Net erreichbar als "
"`http://aria-brain:8080` (oder Alias `http://brain:8080`). "
"ABER: in 99% der Faelle willst Du das gar nicht — nutze die "
"Brain-Tools direkt (`oauth_get_token`, `memory_search`, …), "
"die sind eine Tool-Call-Ebene hoeher und schneller.\n"
" - `BRAIN_INTERNAL_URL` ist NUR in laufenden Skills gesetzt, "
"NICHT in Deinem Bash-Env. Wenn Du `env | grep BRAIN` machst "
"und nichts findest: das ist normal, Du bist hier nicht in "
"einem Skill.\n"
"\n"
"Was Du DOCH von hier aus kannst:\n"
" - Per `ssh aria@host` zur VM-Host wechseln — der ed25519-"
"Key liegt unter /root/.ssh/. Dort bist Du `aria` mit sudo "
"und voller Linux-Power. Fuer Pentest, Admin, komplexe Tasks "
"der richtige Weg.\n"
" - Externe APIs direkt anpingen (Spotify, GitHub etc.) — "
"curl reicht. Token holst Du Dir per Brain-Tool "
"`oauth_get_token('<service>')` und packst ihn in den curl-"
"Header. Aber: das ist Ad-hoc. Fuer wiederkehrendes baust Du "
"einen Skill (siehe no-skill-drift Regel).\n"
"\n"
"Anti-Pattern (47 Sekunden Stefan-Lebenszeit, am 29.05.2026): "
"12 Bash-Versuche mit python3/python/jq/lokales /data/skills "
"→ alles fehlte. Erst nach 9 Tries kapiert dass `localhost` "
"der falsche Host ist. Bei jedem Bash-Call gegen 'lokale' "
"Brain-Resources: erst denken, sonst Brain-Tool nehmen."
),
},
{
"migration_key": "seed/skill-rule/scaffold-reflex",
"type": "rule",
"title": "Skill-Regel: skill_scaffold-Reflex (mit Cross-Session-Counter)",
"category": "skills",
"content": (
"Brain trackt server-side wie oft Du in den letzten 24h dieselbe "
"externe API per Bash-curl angerufen hast (Cross-Session-Counter, "
"siehe '## API-Heuristik'-Block im System-Prompt). \n"
"\n"
"AUTO-SCAFFOLD: Brain legt fuer wiederkehrende Hosts mit "
"bekanntem Template (Spotify, GitHub, OpenAI, OpenWeather, …) "
"automatisch einen Skill an — Du siehst ihn dann in `## Skills` "
"ohne dass Du ihn selbst gebaut hast (Markierung "
"`author=aria-auto`). NUTZE diesen Skill via `run_<name>` "
"direkt, NICHT mehr Bash-curl gegen den Host. Beispiel: wenn "
"`spotify` plotzlich in der Skill-Liste auftaucht → "
"`run_spotify({method:'GET', path:'/v1/me/player'})` statt "
"Token holen + curl.\n"
"\n"
"Wenn die API-Heuristik einen Eintrag OHNE Suggestion zeigt "
"(unbekannter Host): rufe selbst `skill_scaffold` mit dem "
"passenden Template (oauth-api / apikey-api / file-process), "
"BEVOR Du wieder Bash-curl machst.\n"
"\n"
"Warum: jede Chat-Anfrage ist eine eigene Claude-CLI-Session — "
"Du siehst nicht dass Du gestern auch schon 10x Spotify gecurled "
"hast. Der API-Heuristik-Block ist Dein Cross-Session-Gedaechtnis. "
"Wenn er leer ist: alles OK, weitermachen. Wenn nicht: scaffolden.\n"
"\n"
"Templates (ausfuehrliche Doku siehe skill_scaffold-Tool):\n"
" - **oauth-api**: Spotify/GitHub/Reddit/Google/Discord. Token "
"kommt vom Brain mit Auto-Refresh.\n"
" - **apikey-api**: OpenWeather/OpenAI/Twilio. Key landet im "
"config_schema → CFG_<NAME> ENV. Stefan setzt ihn in Diagnostic.\n"
" - **file-process**: PDF/Bild/JSON-Wandler. process()-Stub, "
"danach `skill_update` mit echtem Code.\n"
"\n"
"Belohnung konkret: ein Spotify-Skill macht 'welches lied laeuft' "
"in 1 Tool-Call (~3s) statt 3-5 Bash-Roundtrips (~13-20s). Stefan "
"merkt das sofort. Ein einmaliger Scaffold-Aufwand spart hunderte "
"Bash-Roundtrips."
),
},
{
"migration_key": "seed/skill-rule/external-api-auth-strategy",
"type": "rule",
"title": "Skill-Regel: Auth-Strategie fuer externe APIs",
"category": "skills",
"content": (
"Wenn dein Skill mit einer externen API redet (Spotify, Google, "
"Reddit, GitHub, OpenWeather, OpenAI, …), entscheide IMMER bewusst "
"die Auth-Strategie in dieser Reihenfolge:\n"
" 1. OAuth2? (Spotify, Google, GitHub, Reddit, Discord, Twitch, "
"Microsoft, …) -> nutze `oauth_register_provider` falls der "
"Provider noch nicht da ist, dann `oauth_authorize` fuer "
"Initial-Login. Im Skill: Token via "
"BRAIN_INTERNAL_URL/oauth/<service>/token holen — Brain macht "
"Auto-Refresh, Stefan muss sich nicht alle 60min neu einloggen.\n"
" 2. Statischer API-Key / Bearer-Token? (OpenWeather, OpenAI, "
"Twilio, SendGrid, …) -> in skill.json `config_schema` "
"deklarieren. Stefan setzt den Wert in Diagnostic, Skill bekommt "
"ihn als CFG_<NAME> ENV.\n"
" 3. NIEMALS hardcoden — egal wie 'temporaer' es ist.\n"
"Wenn Du nicht sicher bist welche Strategie ein Service nutzt: "
"in der API-Doku des Services nachsehen ('OAuth' oder "
"'API Key' im Auth-Kapitel). Nicht raten."
),
},
]
def apply(store: VectorStore, embedder: Embedder) -> dict:
"""Schreibt alle SEED_RULES idempotent in die DB.
Vorgehen: erst alle Punkte mit `source=seed` UND passender migration_key
loeschen, dann frisch upserten. So koennen Regeln editiert/entfernt
werden indem die SEED_RULES-Liste angepasst wird.
"""
if not SEED_RULES:
return {"written": 0}
migration_keys = [r["migration_key"] for r in SEED_RULES]
# Alte Versionen entfernen (nur die mit unserer migration_key — andere
# source=seed Punkte aus zukuenftigen seed-Files sind sicher)
try:
store.client.delete(
collection_name=COLLECTION,
points_selector=qm.FilterSelector(filter=qm.Filter(must=[
qm.FieldCondition(key="migration_key", match=qm.MatchAny(any=migration_keys))
])),
)
except Exception as exc:
logger.warning("seed_rules: delete-by-migration_key fehlgeschlagen (%s) — wahrscheinlich erster Run", exc)
# Frisch einbetten + schreiben
texts = [r["content"] for r in SEED_RULES]
vectors = embedder.embed_batch(texts)
now = datetime.now(timezone.utc).isoformat()
written = 0
for rule, vec in zip(SEED_RULES, vectors):
payload = {
"type": rule["type"],
"title": rule["title"],
"content": rule["content"],
"pinned": True,
"category": rule.get("category", ""),
"source": "seed",
"tags": [],
"created_at": now,
"updated_at": now,
"migration_key": rule["migration_key"],
"attachments": [],
}
store.client.upsert(
collection_name=COLLECTION,
points=[qm.PointStruct(id=str(uuid.uuid4()), vector=vec, payload=payload)],
)
written += 1
logger.info("seed_rules: %d Regeln in DB geschrieben", written)
return {"written": written, "keys": migration_keys}