feat(brain): Skill-Regeln als seed_rules — idempotent auf Brain-Boot in DB
Stefans Skill-Friedhof (9 Spotify-Skills, hardcoded Credentials) hatte keine systemische Ursache im Code, sondern im fehlenden Leitplanken- Memory. Lösung: System-Seed-Regeln als pinned Hot Memory, mit jedem Deploy ausgerollt. - aria-brain/seed_rules.py: 5 rule-type Memories (skill_list-vor-create, no-version-suffix, update-not-recreate, no-hardcoded-credentials, config-schema-for-settings), source="seed", pinned=true - Lifespan ruft seed_rules.apply() beim Brain-Start — idempotent via migration_key (alte Versionen werden vor dem Schreiben gelöscht) - skill_create Tool-Description um PFLICHT-VORHER-Block ergänzt: skill_list-check, kein Versionssuffix, oauth_get_token bei OAuth, config_schema statt hardcoded Werte Editieren = SEED_RULES-Liste anpassen, Brain neu starten. Im Gegensatz zu brain-import/ (User-Saatgut, gitignored, manueller Diagnostic-Klick) gehört das hier zum Brain-Code und rollt mit jedem Deploy aus. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,18 @@ META_TOOLS = [
|
|||||||
"Erstelle einen neuen Skill (wiederverwendbare Faehigkeit). "
|
"Erstelle einen neuen Skill (wiederverwendbare Faehigkeit). "
|
||||||
"Skills sind IMMER Python — jeder Skill bekommt seine eigene venv "
|
"Skills sind IMMER Python — jeder Skill bekommt seine eigene venv "
|
||||||
"mit den pip_packages die er braucht.\n\n"
|
"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('<service>')` — 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 "
|
"HARTE REGEL — IMMER Skill anlegen wenn: die Loesung erfordert eine "
|
||||||
"pip-Library. Sonst muesste der Install bei jedem Container-Restart "
|
"pip-Library. Sonst muesste der Install bei jedem Container-Restart "
|
||||||
"neu laufen (Brain hat keinen persistenten State ausser /data/skills/).\n\n"
|
"neu laufen (Brain hat keinen persistenten State ausser /data/skills/).\n\n"
|
||||||
|
|||||||
+8
-1
@@ -37,6 +37,7 @@ import triggers as triggers_mod
|
|||||||
import watcher as watcher_mod
|
import watcher as watcher_mod
|
||||||
import background as background_mod
|
import background as background_mod
|
||||||
import oauth as oauth_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")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
||||||
logger = logging.getLogger("aria-brain")
|
logger = logging.getLogger("aria-brain")
|
||||||
@@ -46,7 +47,13 @@ QDRANT_PORT = int(os.environ.get("QDRANT_PORT", "6333"))
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
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))
|
task = asyncio.create_task(background_mod.run_loop(agent))
|
||||||
logger.info("Lifespan: Trigger-Loop gestartet")
|
logger.info("Lifespan: Trigger-Loop gestartet")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -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('<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."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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}
|
||||||
Reference in New Issue
Block a user