feat(brain): Gedächtnis nach scope trennen (system/personal)
Neues Feld scope=system|personal auf jedem Memory-Punkt. Bootstrap-Export getrennt: System-Regeln (generisch, teilbar) vs. Persönliches (Name, Zugangsdaten, Projekte). Import ist scope-sicher — ein System-Import löscht NICHT die persönlichen pinned Memories. seed_rules + AGENT.md/TOOLING.md → system, USER.md-Präferenzen → personal. Backfill für Bestand (57 system / 617 personal). Diagnostic: zwei Export-Buttons, scope-Badge (SYS/PRIV) + Umschalter pro Memory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
|||||||
|
"""Einmaliger Backfill: weist bestehenden Memory-Punkten ein `scope`
|
||||||
|
(system | personal) zu. Sicher & reversibel — Stefan kann pro Eintrag in der
|
||||||
|
Diagnostic-UI umschalten. Idempotent: laeuft mehrfach ohne Schaden.
|
||||||
|
|
||||||
|
Heuristik (datengetrieben aus dem realen Bestand):
|
||||||
|
- type=preference / fact / conversation / reminder -> personal
|
||||||
|
- source in (seed, auto-feedback) -> system
|
||||||
|
- type=identity -> system
|
||||||
|
- type in (rule, tool, skill) und category in SYSTEM_CATS -> system
|
||||||
|
- sonst -> personal (sicher: nichts leakt)
|
||||||
|
|
||||||
|
Aufruf im Brain-Container:
|
||||||
|
docker exec aria-brain python3 /app/backfill_scope.py # dry-run
|
||||||
|
docker exec aria-brain python3 /app/backfill_scope.py --apply # schreibt
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
from qdrant_client import QdrantClient
|
||||||
|
from qdrant_client.http import models as qm
|
||||||
|
|
||||||
|
COLLECTION = "aria_memory"
|
||||||
|
SYSTEM_CATS = {
|
||||||
|
"sicherheit", "arbeitsweise", "architektur", "ehrlichkeit", "verhalten",
|
||||||
|
"voice", "skills", "freigaben", "infrastruktur", "persoenlichkeit",
|
||||||
|
"pentest", "ausgabe",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_scope(pl: dict) -> str:
|
||||||
|
typ = pl.get("type")
|
||||||
|
src = pl.get("source")
|
||||||
|
cat = (pl.get("category") or "").lower()
|
||||||
|
if typ == "preference":
|
||||||
|
return "personal"
|
||||||
|
if typ in ("fact", "conversation", "reminder"):
|
||||||
|
return "personal"
|
||||||
|
if src in ("seed", "auto-feedback"):
|
||||||
|
return "system"
|
||||||
|
if typ == "identity":
|
||||||
|
return "system"
|
||||||
|
if typ in ("rule", "tool", "skill") and cat in SYSTEM_CATS:
|
||||||
|
return "system"
|
||||||
|
return "personal"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
apply = "--apply" in sys.argv
|
||||||
|
force = "--force" in sys.argv # auch schon gesetzte scopes ueberschreiben
|
||||||
|
c = QdrantClient(
|
||||||
|
host=os.environ.get("QDRANT_HOST", "aria-qdrant"),
|
||||||
|
port=int(os.environ.get("QDRANT_PORT", "6333")),
|
||||||
|
)
|
||||||
|
pts, _ = c.scroll(collection_name=COLLECTION, limit=5000,
|
||||||
|
with_payload=True, with_vectors=False)
|
||||||
|
|
||||||
|
per_scope: dict[str, list] = {"system": [], "personal": []}
|
||||||
|
pinned_examples = Counter()
|
||||||
|
skipped = 0
|
||||||
|
for p in pts:
|
||||||
|
pl = p.payload or {}
|
||||||
|
if pl.get("scope") in ("system", "personal") and not force:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
scope = compute_scope(pl)
|
||||||
|
per_scope[scope].append(p.id)
|
||||||
|
if pl.get("pinned"):
|
||||||
|
pinned_examples[(scope, pl.get("source"), pl.get("type"),
|
||||||
|
pl.get("category"))] += 1
|
||||||
|
|
||||||
|
print(f"total={len(pts)} skipped(already set)={skipped}")
|
||||||
|
print(f"-> system={len(per_scope['system'])} personal={len(per_scope['personal'])}")
|
||||||
|
print("pinned split (scope, source, type, category):")
|
||||||
|
for k, v in sorted(pinned_examples.items()):
|
||||||
|
print(" ", k, v)
|
||||||
|
|
||||||
|
if not apply:
|
||||||
|
print("\nDRY-RUN — nichts geschrieben. Mit --apply ausfuehren.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for scope, ids in per_scope.items():
|
||||||
|
if not ids:
|
||||||
|
continue
|
||||||
|
c.set_payload(collection_name=COLLECTION, payload={"scope": scope}, points=ids)
|
||||||
|
print(f"\nAPPLIED: system={len(per_scope['system'])} personal={len(per_scope['personal'])}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+45
-13
@@ -190,6 +190,7 @@ class MemoryIn(BaseModel):
|
|||||||
pinned: bool = False
|
pinned: bool = False
|
||||||
category: str = ""
|
category: str = ""
|
||||||
source: str = "manual"
|
source: str = "manual"
|
||||||
|
scope: str = "personal" # system | personal — steuert Bootstrap-Export
|
||||||
tags: List[str] = Field(default_factory=list)
|
tags: List[str] = Field(default_factory=list)
|
||||||
conversation_id: Optional[str] = None
|
conversation_id: Optional[str] = None
|
||||||
# Vorhandene Anhang-Metadaten beim Save mitgeben (i.d.R. werden Anhaenge
|
# Vorhandene Anhang-Metadaten beim Save mitgeben (i.d.R. werden Anhaenge
|
||||||
@@ -203,6 +204,7 @@ class MemoryUpdate(BaseModel):
|
|||||||
content: Optional[str] = None
|
content: Optional[str] = None
|
||||||
pinned: Optional[bool] = None
|
pinned: Optional[bool] = None
|
||||||
category: Optional[str] = None
|
category: Optional[str] = None
|
||||||
|
scope: Optional[str] = None # system | personal
|
||||||
tags: Optional[List[str]] = None
|
tags: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -214,6 +216,7 @@ class MemoryOut(BaseModel):
|
|||||||
pinned: bool
|
pinned: bool
|
||||||
category: str
|
category: str
|
||||||
source: str
|
source: str
|
||||||
|
scope: str = "personal"
|
||||||
tags: List[str]
|
tags: List[str]
|
||||||
created_at: str
|
created_at: str
|
||||||
updated_at: str
|
updated_at: str
|
||||||
@@ -328,6 +331,7 @@ def memory_save(body: MemoryIn):
|
|||||||
pinned=body.pinned,
|
pinned=body.pinned,
|
||||||
category=body.category,
|
category=body.category,
|
||||||
source=body.source,
|
source=body.source,
|
||||||
|
scope=body.scope,
|
||||||
tags=body.tags,
|
tags=body.tags,
|
||||||
conversation_id=body.conversation_id,
|
conversation_id=body.conversation_id,
|
||||||
attachments=body.attachments or [],
|
attachments=body.attachments or [],
|
||||||
@@ -353,6 +357,8 @@ def memory_update(point_id: str, body: MemoryUpdate):
|
|||||||
existing.pinned = body.pinned
|
existing.pinned = body.pinned
|
||||||
if body.category is not None:
|
if body.category is not None:
|
||||||
existing.category = body.category
|
existing.category = body.category
|
||||||
|
if body.scope is not None:
|
||||||
|
existing.scope = body.scope
|
||||||
if body.tags is not None:
|
if body.tags is not None:
|
||||||
existing.tags = body.tags
|
existing.tags = body.tags
|
||||||
|
|
||||||
@@ -537,12 +543,23 @@ def memory_import_files():
|
|||||||
# Wiederherstellen einer schlanken ARIA nach Wipe.
|
# Wiederherstellen einer schlanken ARIA nach Wipe.
|
||||||
|
|
||||||
@app.get("/memory/export-bootstrap")
|
@app.get("/memory/export-bootstrap")
|
||||||
def memory_export_bootstrap():
|
def memory_export_bootstrap(scope: str = "system"):
|
||||||
"""Gibt alle pinned Memories als JSON zurueck — fuer Browser-Download."""
|
"""Gibt pinned Memories als JSON zurueck — fuer Browser-Download.
|
||||||
|
|
||||||
|
scope='system' → nur generische Regeln (fuer ein frisches System),
|
||||||
|
scope='personal' → nur Stefan-spezifisches (Name, Zugangsdaten, Projekte),
|
||||||
|
scope='all' → alles pinned (Vollbackup).
|
||||||
|
Default 'system', damit man nicht versehentlich Persoenliches teilt."""
|
||||||
s = store()
|
s = store()
|
||||||
pinned = s.list_pinned()
|
if scope == "all":
|
||||||
|
pinned = s.list_pinned()
|
||||||
|
elif scope in ("system", "personal"):
|
||||||
|
pinned = s.list_pinned_by_scope(scope)
|
||||||
|
else:
|
||||||
|
raise HTTPException(400, f"Ungueltiger scope: {scope}")
|
||||||
return {
|
return {
|
||||||
"version": 1,
|
"version": 2,
|
||||||
|
"scope": scope,
|
||||||
"exported_at": __import__("datetime").datetime.now(
|
"exported_at": __import__("datetime").datetime.now(
|
||||||
__import__("datetime").timezone.utc
|
__import__("datetime").timezone.utc
|
||||||
).isoformat(),
|
).isoformat(),
|
||||||
@@ -555,6 +572,7 @@ def memory_export_bootstrap():
|
|||||||
"pinned": True,
|
"pinned": True,
|
||||||
"category": p.category,
|
"category": p.category,
|
||||||
"source": p.source,
|
"source": p.source,
|
||||||
|
"scope": p.scope,
|
||||||
"tags": p.tags,
|
"tags": p.tags,
|
||||||
}
|
}
|
||||||
for p in pinned
|
for p in pinned
|
||||||
@@ -564,13 +582,18 @@ def memory_export_bootstrap():
|
|||||||
|
|
||||||
class BootstrapBundle(BaseModel):
|
class BootstrapBundle(BaseModel):
|
||||||
version: int = 1
|
version: int = 1
|
||||||
|
scope: Optional[str] = None # system | personal | all (aus dem Export)
|
||||||
memories: List[dict]
|
memories: List[dict]
|
||||||
|
|
||||||
|
|
||||||
@app.post("/memory/import-bootstrap")
|
@app.post("/memory/import-bootstrap")
|
||||||
def memory_import_bootstrap(body: BootstrapBundle):
|
def memory_import_bootstrap(body: BootstrapBundle):
|
||||||
"""Loescht alle pinned Memories und importiert die im Bundle.
|
"""Importiert ein Bootstrap-Bundle scope-sicher.
|
||||||
Cold Memory (unpinned) bleibt unangetastet.
|
|
||||||
|
Es werden NUR die aktuell pinned Punkte geloescht, deren scope zum Import
|
||||||
|
gehoert — ein System-Import laesst also die persoenlichen pinned Memories
|
||||||
|
(Name, Zugangsdaten) unangetastet und umgekehrt. Bei einem 'all'-Bundle
|
||||||
|
(Vollbackup) werden alle pinned ersetzt.
|
||||||
|
|
||||||
Wenn keine Memories im Bundle: nur loeschen ist NICHT erlaubt — der
|
Wenn keine Memories im Bundle: nur loeschen ist NICHT erlaubt — der
|
||||||
Caller soll erst exportieren und dann importieren.
|
Caller soll erst exportieren und dann importieren.
|
||||||
@@ -580,23 +603,31 @@ def memory_import_bootstrap(body: BootstrapBundle):
|
|||||||
|
|
||||||
s = store()
|
s = store()
|
||||||
e = embedder()
|
e = embedder()
|
||||||
|
|
||||||
# Alle aktuell pinned Punkte loeschen
|
|
||||||
from qdrant_client.http import models as qm
|
from qdrant_client.http import models as qm
|
||||||
from memory.vector_store import COLLECTION
|
from memory.vector_store import COLLECTION
|
||||||
|
|
||||||
|
# Scope bestimmen: explizit aus dem Bundle, sonst aus den memories ableiten.
|
||||||
|
bundle_scope = body.scope
|
||||||
|
if bundle_scope not in ("system", "personal", "all"):
|
||||||
|
scopes_in_mems = {m.get("scope", "personal") for m in body.memories}
|
||||||
|
bundle_scope = scopes_in_mems.pop() if len(scopes_in_mems) == 1 else "all"
|
||||||
|
|
||||||
|
# Nur die pinned Punkte des betroffenen scope loeschen.
|
||||||
|
del_must = [qm.FieldCondition(key="pinned", match=qm.MatchValue(value=True))]
|
||||||
|
if bundle_scope in ("system", "personal"):
|
||||||
|
del_must.append(qm.FieldCondition(key="scope", match=qm.MatchValue(value=bundle_scope)))
|
||||||
s.client.delete(
|
s.client.delete(
|
||||||
collection_name=COLLECTION,
|
collection_name=COLLECTION,
|
||||||
points_selector=qm.FilterSelector(filter=qm.Filter(must=[
|
points_selector=qm.FilterSelector(filter=qm.Filter(must=del_must)),
|
||||||
qm.FieldCondition(key="pinned", match=qm.MatchValue(value=True))
|
|
||||||
])),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Neue Punkte einspeisen
|
# Neue Punkte einspeisen — scope pro memory (Fallback: bundle_scope bzw. personal).
|
||||||
created = 0
|
created = 0
|
||||||
for m in body.memories:
|
for m in body.memories:
|
||||||
content = (m.get("content") or "").strip()
|
content = (m.get("content") or "").strip()
|
||||||
if not content:
|
if not content:
|
||||||
continue
|
continue
|
||||||
|
mscope = m.get("scope") or (bundle_scope if bundle_scope != "all" else "personal")
|
||||||
point = MemoryPoint(
|
point = MemoryPoint(
|
||||||
id="",
|
id="",
|
||||||
type=m.get("type", "fact"),
|
type=m.get("type", "fact"),
|
||||||
@@ -605,13 +636,14 @@ def memory_import_bootstrap(body: BootstrapBundle):
|
|||||||
pinned=True,
|
pinned=True,
|
||||||
category=m.get("category", ""),
|
category=m.get("category", ""),
|
||||||
source=m.get("source", "bootstrap-import"),
|
source=m.get("source", "bootstrap-import"),
|
||||||
|
scope=mscope,
|
||||||
tags=list(m.get("tags", [])),
|
tags=list(m.get("tags", [])),
|
||||||
)
|
)
|
||||||
vec = e.embed(content)
|
vec = e.embed(content)
|
||||||
s.upsert(point, vec)
|
s.upsert(point, vec)
|
||||||
created += 1
|
created += 1
|
||||||
|
|
||||||
return {"created": created, "deleted_previous_pinned": True}
|
return {"created": created, "scope": bundle_scope, "deleted_previous_pinned": True}
|
||||||
|
|
||||||
|
|
||||||
# ─── Conversation-Loop ──────────────────────────────────────────────
|
# ─── Conversation-Loop ──────────────────────────────────────────────
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ Punkt-Schema (Payload):
|
|||||||
content — eigentlicher Text (wird embedded)
|
content — eigentlicher Text (wird embedded)
|
||||||
pinned — bool, True = Hot Memory (immer in Prompt)
|
pinned — bool, True = Hot Memory (immer in Prompt)
|
||||||
source — import | conversation | manual
|
source — import | conversation | manual
|
||||||
|
scope — system | personal. system = generische Regeln, die JEDER
|
||||||
|
braucht, der das System aufsetzt (Sicherheit, Ehrlichkeit,
|
||||||
|
Skill-Regeln). personal = Stefan-spezifisch (Name, Zugangs-
|
||||||
|
daten, Projekte). Steuert den getrennten Bootstrap-Export.
|
||||||
tags — Liste von Strings
|
tags — Liste von Strings
|
||||||
created_at, updated_at — ISO-Strings
|
created_at, updated_at — ISO-Strings
|
||||||
conversation_id — optional, nur fuer type=conversation
|
conversation_id — optional, nur fuer type=conversation
|
||||||
@@ -55,6 +59,7 @@ class MemoryPoint:
|
|||||||
pinned: bool = False
|
pinned: bool = False
|
||||||
category: str = ""
|
category: str = ""
|
||||||
source: str = "manual"
|
source: str = "manual"
|
||||||
|
scope: str = "personal" # system | personal — steuert Bootstrap-Export
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
created_at: str = ""
|
created_at: str = ""
|
||||||
updated_at: str = ""
|
updated_at: str = ""
|
||||||
@@ -74,6 +79,7 @@ class MemoryPoint:
|
|||||||
"pinned": self.pinned,
|
"pinned": self.pinned,
|
||||||
"category": self.category,
|
"category": self.category,
|
||||||
"source": self.source,
|
"source": self.source,
|
||||||
|
"scope": self.scope,
|
||||||
"tags": self.tags,
|
"tags": self.tags,
|
||||||
"created_at": self.created_at,
|
"created_at": self.created_at,
|
||||||
"updated_at": self.updated_at,
|
"updated_at": self.updated_at,
|
||||||
@@ -94,6 +100,7 @@ class MemoryPoint:
|
|||||||
pinned=payload.get("pinned", False),
|
pinned=payload.get("pinned", False),
|
||||||
category=payload.get("category", ""),
|
category=payload.get("category", ""),
|
||||||
source=payload.get("source", "manual"),
|
source=payload.get("source", "manual"),
|
||||||
|
scope=payload.get("scope", "personal"),
|
||||||
tags=payload.get("tags", []),
|
tags=payload.get("tags", []),
|
||||||
created_at=payload.get("created_at", ""),
|
created_at=payload.get("created_at", ""),
|
||||||
updated_at=payload.get("updated_at", ""),
|
updated_at=payload.get("updated_at", ""),
|
||||||
@@ -120,14 +127,23 @@ class VectorStore:
|
|||||||
collection_name=COLLECTION,
|
collection_name=COLLECTION,
|
||||||
vectors_config=qm.VectorParams(size=VECTOR_DIM, distance=qm.Distance.COSINE),
|
vectors_config=qm.VectorParams(size=VECTOR_DIM, distance=qm.Distance.COSINE),
|
||||||
)
|
)
|
||||||
# Indexe fuer typische Filter-Felder
|
# Indexe fuer typische Filter-Felder — idempotent, laeuft auch auf
|
||||||
for field_name in ("type", "pinned", "category", "source", "migration_key"):
|
# einer bestehenden Collection (fuer neu hinzugekommene Felder wie scope).
|
||||||
|
self._ensure_indexes()
|
||||||
|
|
||||||
|
def _ensure_indexes(self):
|
||||||
|
for field_name in ("type", "pinned", "category", "source", "scope", "migration_key"):
|
||||||
|
schema = (qm.PayloadSchemaType.BOOL if field_name == "pinned"
|
||||||
|
else qm.PayloadSchemaType.KEYWORD)
|
||||||
|
try:
|
||||||
self.client.create_payload_index(
|
self.client.create_payload_index(
|
||||||
collection_name=COLLECTION,
|
collection_name=COLLECTION,
|
||||||
field_name=field_name,
|
field_name=field_name,
|
||||||
field_schema=qm.PayloadSchemaType.KEYWORD if field_name != "pinned"
|
field_schema=schema,
|
||||||
else qm.PayloadSchemaType.BOOL,
|
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
# Index existiert bereits — kein Problem.
|
||||||
|
pass
|
||||||
|
|
||||||
# ─── Schreib-Operationen ─────────────────────────────────────────
|
# ─── Schreib-Operationen ─────────────────────────────────────────
|
||||||
|
|
||||||
@@ -164,6 +180,38 @@ class VectorStore:
|
|||||||
qm.FieldCondition(key="pinned", match=qm.MatchValue(value=True))
|
qm.FieldCondition(key="pinned", match=qm.MatchValue(value=True))
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
def list_pinned_by_scope(self, scope: str) -> List[MemoryPoint]:
|
||||||
|
"""Alle pinned Punkte eines scope (system | personal). Fuer den
|
||||||
|
getrennten Bootstrap-Export."""
|
||||||
|
return self._scroll(filter=qm.Filter(must=[
|
||||||
|
qm.FieldCondition(key="pinned", match=qm.MatchValue(value=True)),
|
||||||
|
qm.FieldCondition(key="scope", match=qm.MatchValue(value=scope)),
|
||||||
|
]))
|
||||||
|
|
||||||
|
def list_index_titles(self, limit: int = 500) -> List[MemoryPoint]:
|
||||||
|
"""Leichtgewichtiger Titel-Index des kalten Gedaechtnisses fuer den
|
||||||
|
System-Prompt: ARIA sieht WAS sie an Nachschlage-Wissen hat (Zugangs-
|
||||||
|
daten, Infrastruktur, Projekte) und holt den Inhalt bei Bedarf via
|
||||||
|
memory_search — statt Stefan nach etwas zu fragen, das schon da ist.
|
||||||
|
|
||||||
|
Bewusst NUR die deliberat gespeicherten Punkte:
|
||||||
|
- nicht pinned (die sind eh schon voll im Prompt),
|
||||||
|
- kein type=conversation (Chat-Mitschnitte),
|
||||||
|
- kein source=distilled (die 100e auto-destillierten Gespraechs-
|
||||||
|
Fakten — die traegt das semantische Auto-Retrieval, sie hier
|
||||||
|
als Titel zu listen wuerde nur Kontext fressen).
|
||||||
|
So bleibt der Index klein (Dutzende statt Hunderte Zeilen)."""
|
||||||
|
return self._scroll(
|
||||||
|
filter=qm.Filter(
|
||||||
|
must_not=[
|
||||||
|
qm.FieldCondition(key="pinned", match=qm.MatchValue(value=True)),
|
||||||
|
qm.FieldCondition(key="type", match=qm.MatchValue(value="conversation")),
|
||||||
|
qm.FieldCondition(key="source", match=qm.MatchValue(value="distilled")),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
def list_by_type(self, type_: str, limit: int = 100) -> List[MemoryPoint]:
|
def list_by_type(self, type_: str, limit: int = 100) -> List[MemoryPoint]:
|
||||||
return self._scroll(
|
return self._scroll(
|
||||||
filter=qm.Filter(must=[
|
filter=qm.Filter(must=[
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ def _parse_user_md(md: str, source_file: str) -> List[MemoryPoint]:
|
|||||||
type_="preference", title=f"User: {btitle}",
|
type_="preference", title=f"User: {btitle}",
|
||||||
content=btext, category="allgemein",
|
content=btext, category="allgemein",
|
||||||
migration_key=f"{source_file}/general-{idx}",
|
migration_key=f"{source_file}/general-{idx}",
|
||||||
|
scope="personal",
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
cat_key = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-") or "allgemein"
|
cat_key = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-") or "allgemein"
|
||||||
@@ -259,6 +260,7 @@ def _parse_user_md(md: str, source_file: str) -> List[MemoryPoint]:
|
|||||||
type_="preference", title=title,
|
type_="preference", title=title,
|
||||||
content=content, category=cat_key,
|
content=content, category=cat_key,
|
||||||
migration_key=f"{source_file}/{cat_key}",
|
migration_key=f"{source_file}/{cat_key}",
|
||||||
|
scope="personal",
|
||||||
))
|
))
|
||||||
return points
|
return points
|
||||||
|
|
||||||
@@ -283,7 +285,11 @@ def _mk(
|
|||||||
migration_key: str,
|
migration_key: str,
|
||||||
pinned: bool = True,
|
pinned: bool = True,
|
||||||
category: str = "",
|
category: str = "",
|
||||||
|
scope: str = "system",
|
||||||
) -> MemoryPoint:
|
) -> MemoryPoint:
|
||||||
|
# scope-Default 'system': AGENT.md + TOOLING.md beschreiben ARIA selbst
|
||||||
|
# (Identitaet, Sicherheit, Architektur) — das braucht jedes System.
|
||||||
|
# USER.md-Praeferenzen sind personal und uebergeben scope='personal'.
|
||||||
p = MemoryPoint(
|
p = MemoryPoint(
|
||||||
id="",
|
id="",
|
||||||
type=type_,
|
type=type_,
|
||||||
@@ -292,6 +298,7 @@ def _mk(
|
|||||||
pinned=pinned,
|
pinned=pinned,
|
||||||
category=category,
|
category=category,
|
||||||
source="import",
|
source="import",
|
||||||
|
scope=scope,
|
||||||
tags=[],
|
tags=[],
|
||||||
)
|
)
|
||||||
# migration_key wird ueber Payload-Index angesprochen — in to_payload manuell anhaengen
|
# migration_key wird ueber Payload-Index angesprochen — in to_payload manuell anhaengen
|
||||||
|
|||||||
@@ -915,6 +915,7 @@ def apply(store: VectorStore, embedder: Embedder) -> dict:
|
|||||||
"pinned": True,
|
"pinned": True,
|
||||||
"category": rule.get("category", ""),
|
"category": rule.get("category", ""),
|
||||||
"source": "seed",
|
"source": "seed",
|
||||||
|
"scope": "system",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"created_at": now,
|
"created_at": now,
|
||||||
"updated_at": now,
|
"updated_at": now,
|
||||||
|
|||||||
+34
-12
@@ -1080,11 +1080,13 @@
|
|||||||
<div style="background:#0D0D1A;border-radius:6px;padding:10px 12px;margin-bottom:8px;">
|
<div style="background:#0D0D1A;border-radius:6px;padding:10px 12px;margin-bottom:8px;">
|
||||||
<div style="color:#FFD60A;font-weight:bold;font-size:12px;margin-bottom:4px;">2. Bootstrap-Snapshot (nur pinned)</div>
|
<div style="color:#FFD60A;font-weight:bold;font-size:12px;margin-bottom:4px;">2. Bootstrap-Snapshot (nur pinned)</div>
|
||||||
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
|
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
|
||||||
Klein und schnell: <strong>nur</strong> die pinned Memories (Identität, Regeln, Präferenzen, Tools, Skills) als JSON.
|
Getrennt nach <strong>scope</strong>: <span style="color:#3FFF3F;">System</span> = generische Regeln, die jeder braucht (Sicherheit, Ehrlichkeit, Skill-Regeln) — teilbar für ein frisches System.
|
||||||
Use-Case: Wipe → Bootstrap-Import → ARIA hat Persönlichkeit zurück, sonst leer.
|
<span style="color:#FF9F0A;">Persönlich</span> = Stefan-spezifisch (Name, Zugangsdaten, Projekte) — bleibt privat.
|
||||||
Cold Memory (Konversations-Fakten) bleibt beim Import unangetastet.
|
Import ersetzt nur die pinned Memories des jeweiligen scope; Cold Memory bleibt unangetastet.
|
||||||
</div>
|
</div>
|
||||||
<button class="btn secondary" onclick="exportBootstrap()" style="color:#FFD60A;border-color:#FFD60A;">⬇ Bootstrap exportieren (JSON)</button>
|
<button class="btn secondary" onclick="exportBootstrap('system')" style="color:#3FFF3F;border-color:#3FFF3F;">⬇ System-Regeln exportieren</button>
|
||||||
|
<button class="btn secondary" onclick="exportBootstrap('personal')" style="color:#FF9F0A;border-color:#FF9F0A;">⬇ Persönliches exportieren</button>
|
||||||
|
<button class="btn secondary" onclick="exportBootstrap('all')" style="color:#FFD60A;border-color:#FFD60A;">⬇ Alles (Vollbackup)</button>
|
||||||
<input type="file" id="bootstrap-import-file" accept=".json,application/json" style="display:none" onchange="importBootstrap(event)">
|
<input type="file" id="bootstrap-import-file" accept=".json,application/json" style="display:none" onchange="importBootstrap(event)">
|
||||||
<button class="btn secondary" onclick="document.getElementById('bootstrap-import-file').click()" style="color:#FFD60A;border-color:#FFD60A;">⬆ Bootstrap importieren</button>
|
<button class="btn secondary" onclick="document.getElementById('bootstrap-import-file').click()" style="color:#FFD60A;border-color:#FFD60A;">⬆ Bootstrap importieren</button>
|
||||||
<div id="bootstrap-status" style="margin-top:8px;font-size:11px;color:#8888AA;"></div>
|
<div id="bootstrap-status" style="margin-top:8px;font-size:11px;color:#8888AA;"></div>
|
||||||
@@ -1408,6 +1410,11 @@
|
|||||||
<input type="checkbox" id="memory-pinned">
|
<input type="checkbox" id="memory-pinned">
|
||||||
<span>📌 Pinned (Hot Memory — IMMER im System-Prompt)</span>
|
<span>📌 Pinned (Hot Memory — IMMER im System-Prompt)</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label style="display:block;color:#8888AA;font-size:11px;margin-top:10px;margin-bottom:3px;">Scope (steuert Bootstrap-Export):</label>
|
||||||
|
<select id="memory-scope" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;">
|
||||||
|
<option value="personal">🟠 Persönlich — Stefan-spezifisch, bleibt privat</option>
|
||||||
|
<option value="system">🟢 System — generische Regel, teilbar für frisches System</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<!-- Anhaenge — nur bei Edit (vorhandene ID) sichtbar -->
|
<!-- Anhaenge — nur bei Edit (vorhandene ID) sichtbar -->
|
||||||
<div id="memory-attachments-block" style="display:none;margin-top:14px;padding-top:10px;border-top:1px solid #1E1E2E;">
|
<div id="memory-attachments-block" style="display:none;margin-top:14px;padding-top:10px;border-top:1px solid #1E1E2E;">
|
||||||
@@ -5804,9 +5811,15 @@
|
|||||||
const typeBadge = withScore ? `<span style="color:#0096FF;font-size:10px;margin-right:6px;">${escapeHtml(BRAIN_TYPE_LABELS[m.type] || m.type)}</span>` : '';
|
const typeBadge = withScore ? `<span style="color:#0096FF;font-size:10px;margin-right:6px;">${escapeHtml(BRAIN_TYPE_LABELS[m.type] || m.type)}</span>` : '';
|
||||||
const attCount = Array.isArray(m.attachments) ? m.attachments.length : 0;
|
const attCount = Array.isArray(m.attachments) ? m.attachments.length : 0;
|
||||||
const attBadge = attCount > 0 ? `<span style="color:#34C759;font-size:10px;margin-left:6px;" title="${attCount} Anhang${attCount === 1 ? '' : ' / Anhaenge'}">📎${attCount}</span>` : '';
|
const attBadge = attCount > 0 ? `<span style="color:#34C759;font-size:10px;margin-left:6px;" title="${attCount} Anhang${attCount === 1 ? '' : ' / Anhaenge'}">📎${attCount}</span>` : '';
|
||||||
|
// scope-Badge nur bei pinned (nur die werden exportiert — da zaehlt die Trennung).
|
||||||
|
const scopeBadge = m.pinned
|
||||||
|
? (m.scope === 'system'
|
||||||
|
? `<span style="color:#3FFF3F;font-size:9px;margin-left:6px;border:1px solid #3FFF3F;border-radius:3px;padding:0 3px;" title="System-Regel — kommt in den System-Export">SYS</span>`
|
||||||
|
: `<span style="color:#FF9F0A;font-size:9px;margin-left:6px;border:1px solid #FF9F0A;border-radius:3px;padding:0 3px;" title="Persönlich — bleibt privat">PRIV</span>`)
|
||||||
|
: '';
|
||||||
return `<div style="padding:6px 0;border-bottom:1px solid #1E1E2E;display:flex;gap:6px;align-items:flex-start;">
|
return `<div style="padding:6px 0;border-bottom:1px solid #1E1E2E;display:flex;gap:6px;align-items:flex-start;">
|
||||||
<div style="flex:1;min-width:0;cursor:pointer;" onclick="openMemoryModal('${m.id}')">
|
<div style="flex:1;min-width:0;cursor:pointer;" onclick="openMemoryModal('${m.id}')">
|
||||||
<div style="color:#E0E0F0;font-size:12px;">${typeBadge}${pin}<strong>${escapeHtml(m.title || '(ohne Titel)')}</strong>${score}${attBadge}
|
<div style="color:#E0E0F0;font-size:12px;">${typeBadge}${pin}<strong>${escapeHtml(m.title || '(ohne Titel)')}</strong>${score}${attBadge}${scopeBadge}
|
||||||
${m.category ? `<span style="color:#555570;font-weight:normal;font-size:10px;margin-left:6px;">[${escapeHtml(m.category)}]</span>` : ''}
|
${m.category ? `<span style="color:#555570;font-weight:normal;font-size:10px;margin-left:6px;">[${escapeHtml(m.category)}]</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div style="color:#888;font-size:11px;line-height:1.4;">${escapeHtml(preview)}${m.content && m.content.length > 140 ? '...' : ''}</div>
|
<div style="color:#888;font-size:11px;line-height:1.4;">${escapeHtml(preview)}${m.content && m.content.length > 140 ? '...' : ''}</div>
|
||||||
@@ -6016,6 +6029,7 @@
|
|||||||
document.getElementById('memory-category').value = m.category || '';
|
document.getElementById('memory-category').value = m.category || '';
|
||||||
document.getElementById('memory-tags').value = (m.tags || []).join(', ');
|
document.getElementById('memory-tags').value = (m.tags || []).join(', ');
|
||||||
document.getElementById('memory-pinned').checked = !!m.pinned;
|
document.getElementById('memory-pinned').checked = !!m.pinned;
|
||||||
|
document.getElementById('memory-scope').value = (m.scope === 'system') ? 'system' : 'personal';
|
||||||
// Anhang-Block sichtbar — Liste rendern
|
// Anhang-Block sichtbar — Liste rendern
|
||||||
if (attBlock) attBlock.style.display = 'block';
|
if (attBlock) attBlock.style.display = 'block';
|
||||||
if (attHint) attHint.style.display = 'none';
|
if (attHint) attHint.style.display = 'none';
|
||||||
@@ -6029,6 +6043,7 @@
|
|||||||
document.getElementById('memory-category').value = '';
|
document.getElementById('memory-category').value = '';
|
||||||
document.getElementById('memory-tags').value = '';
|
document.getElementById('memory-tags').value = '';
|
||||||
document.getElementById('memory-pinned').checked = false;
|
document.getElementById('memory-pinned').checked = false;
|
||||||
|
document.getElementById('memory-scope').value = 'personal';
|
||||||
// Bei neuem Memory: nur Hinweis, dass Anhaenge nach Save gehen
|
// Bei neuem Memory: nur Hinweis, dass Anhaenge nach Save gehen
|
||||||
if (attBlock) attBlock.style.display = 'none';
|
if (attBlock) attBlock.style.display = 'none';
|
||||||
if (attHint) attHint.style.display = 'block';
|
if (attHint) attHint.style.display = 'block';
|
||||||
@@ -6133,6 +6148,7 @@
|
|||||||
const category = document.getElementById('memory-category').value.trim();
|
const category = document.getElementById('memory-category').value.trim();
|
||||||
const tags = document.getElementById('memory-tags').value.split(',').map(t => t.trim()).filter(Boolean);
|
const tags = document.getElementById('memory-tags').value.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
const pinned = document.getElementById('memory-pinned').checked;
|
const pinned = document.getElementById('memory-pinned').checked;
|
||||||
|
const scope = document.getElementById('memory-scope').value || 'personal';
|
||||||
|
|
||||||
if (!title) { errEl.textContent = 'Titel fehlt.'; errEl.style.display = 'block'; return; }
|
if (!title) { errEl.textContent = 'Titel fehlt.'; errEl.style.display = 'block'; return; }
|
||||||
if (!content) { errEl.textContent = 'Inhalt fehlt.'; errEl.style.display = 'block'; return; }
|
if (!content) { errEl.textContent = 'Inhalt fehlt.'; errEl.style.display = 'block'; return; }
|
||||||
@@ -6143,13 +6159,13 @@
|
|||||||
r = await fetch('/api/brain/memory/update/' + encodeURIComponent(id), {
|
r = await fetch('/api/brain/memory/update/' + encodeURIComponent(id), {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ title, content, pinned, category, tags }),
|
body: JSON.stringify({ title, content, pinned, category, scope, tags }),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
r = await fetch('/api/brain/memory/save', {
|
r = await fetch('/api/brain/memory/save', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ type, title, content, pinned, category, tags, source: 'manual' }),
|
body: JSON.stringify({ type, title, content, pinned, category, scope, tags, source: 'manual' }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
@@ -6526,11 +6542,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Bootstrap Export / Import ──────────────────────────
|
// ── Bootstrap Export / Import ──────────────────────────
|
||||||
async function exportBootstrap() {
|
async function exportBootstrap(scope) {
|
||||||
|
scope = scope || 'system';
|
||||||
const status = document.getElementById('bootstrap-status');
|
const status = document.getElementById('bootstrap-status');
|
||||||
if (status) status.innerHTML = '⏳ Lade...';
|
if (status) status.innerHTML = '⏳ Lade...';
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/brain/memory/export-bootstrap');
|
const r = await fetch('/api/brain/memory/export-bootstrap?scope=' + encodeURIComponent(scope));
|
||||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
@@ -6538,10 +6555,11 @@
|
|||||||
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `aria-bootstrap-${ts}.json`;
|
a.download = `aria-bootstrap-${scope}-${ts}.json`;
|
||||||
document.body.appendChild(a); a.click();
|
document.body.appendChild(a); a.click();
|
||||||
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
|
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
|
||||||
if (status) status.innerHTML = `<span style="color:#3FFF3F;">✓ ${data.count} pinned Memories exportiert</span>`;
|
const label = scope === 'system' ? 'System-Regeln' : (scope === 'personal' ? 'persönliche Memories' : 'pinned Memories');
|
||||||
|
if (status) status.innerHTML = `<span style="color:#3FFF3F;">✓ ${data.count} ${label} exportiert</span>`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (status) status.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
|
if (status) status.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
|
||||||
}
|
}
|
||||||
@@ -6555,7 +6573,11 @@
|
|||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const bundle = JSON.parse(text);
|
const bundle = JSON.parse(text);
|
||||||
if (!Array.isArray(bundle.memories)) throw new Error('Datei hat kein "memories"-Array');
|
if (!Array.isArray(bundle.memories)) throw new Error('Datei hat kein "memories"-Array');
|
||||||
if (!confirm(`Bootstrap importieren?\n\n${bundle.memories.length} pinned Memories aus "${file.name}".\n\nALLE aktuell pinned Memories werden überschrieben. Cold Memory bleibt unverändert.`)) {
|
const bScope = bundle.scope || 'all';
|
||||||
|
const scopeInfo = bScope === 'system' ? 'Nur die aktuell pinned SYSTEM-Regeln werden ersetzt — Persönliches bleibt.'
|
||||||
|
: bScope === 'personal' ? 'Nur die aktuell pinned PERSÖNLICHEN Memories werden ersetzt — System-Regeln bleiben.'
|
||||||
|
: 'ALLE aktuell pinned Memories werden überschrieben.';
|
||||||
|
if (!confirm(`Bootstrap importieren? (scope: ${bScope})\n\n${bundle.memories.length} pinned Memories aus "${file.name}".\n\n${scopeInfo} Cold Memory bleibt unverändert.`)) {
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user