diff --git a/aria-brain/agent.py b/aria-brain/agent.py index b807bf1..cd7df17 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -72,6 +72,18 @@ META_TOOLS = [ "Erstelle einen neuen Skill (wiederverwendbare Faehigkeit). " "Skills sind IMMER Python — jeder Skill bekommt seine eigene venv " "mit den pip_packages die er braucht.\n\n" + "PFLICHT VORHER:\n" + " - `skill_list` aufrufen und pruefen ob ein passender Skill schon " + "existiert. Wenn ja: `skill_update` statt neu anlegen.\n" + " - Name OHNE Versionssuffix waehlen (kein `-v2`, `_v3`, `-new`, " + "`-fixed`, `-aria`, `-ctl`). Versionsverwaltung ist intern, Du brauchst " + "nur einen klaren Namen.\n" + " - Bei OAuth-Services (Spotify, Google, GitHub etc.): NIEMALS " + "client_id/client_secret/Tokens in den Code schreiben. Nutze " + "`oauth_get_token('')` — das macht Auto-Refresh. Sonst muss " + "Stefan sich alle 60min manuell neu einloggen.\n" + " - Bei konfigurierbaren Werten (User-IDs, Endpoints, Defaults): " + "ueber `config_schema` deklarieren, NICHT hardcoden.\n\n" "HARTE REGEL — IMMER Skill anlegen wenn: die Loesung erfordert eine " "pip-Library. Sonst muesste der Install bei jedem Container-Restart " "neu laufen (Brain hat keinen persistenten State ausser /data/skills/).\n\n" diff --git a/aria-brain/main.py b/aria-brain/main.py index a2da99c..1979210 100644 --- a/aria-brain/main.py +++ b/aria-brain/main.py @@ -37,6 +37,7 @@ import triggers as triggers_mod import watcher as watcher_mod import background as background_mod import oauth as oauth_mod +import seed_rules as seed_rules_mod logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") logger = logging.getLogger("aria-brain") @@ -46,7 +47,13 @@ QDRANT_PORT = int(os.environ.get("QDRANT_PORT", "6333")) @asynccontextmanager async def lifespan(app: FastAPI): - """Beim Brain-Start: Trigger-Background-Loop anwerfen. Beim Shutdown: stoppen.""" + """Beim Brain-Start: System-Seed-Regeln idempotent in DB schreiben, + Trigger-Background-Loop anwerfen. Beim Shutdown: Loop stoppen.""" + try: + result = seed_rules_mod.apply(store(), embedder()) + logger.info("Lifespan: seed_rules angewendet (%s)", result) + except Exception as exc: + logger.exception("Lifespan: seed_rules fehlgeschlagen — Brain startet trotzdem (%s)", exc) task = asyncio.create_task(background_mod.run_loop(agent)) logger.info("Lifespan: Trigger-Loop gestartet") try: diff --git a/aria-brain/seed_rules.py b/aria-brain/seed_rules.py new file mode 100644 index 0000000..134b413 --- /dev/null +++ b/aria-brain/seed_rules.py @@ -0,0 +1,150 @@ +""" +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/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('')` — 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_`. NICHT als " + "Argument, NICHT hardcoded." + ), + }, +] + + +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}