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
|
||||
category: str = ""
|
||||
source: str = "manual"
|
||||
scope: str = "personal" # system | personal — steuert Bootstrap-Export
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
conversation_id: Optional[str] = None
|
||||
# Vorhandene Anhang-Metadaten beim Save mitgeben (i.d.R. werden Anhaenge
|
||||
@@ -203,6 +204,7 @@ class MemoryUpdate(BaseModel):
|
||||
content: Optional[str] = None
|
||||
pinned: Optional[bool] = None
|
||||
category: Optional[str] = None
|
||||
scope: Optional[str] = None # system | personal
|
||||
tags: Optional[List[str]] = None
|
||||
|
||||
|
||||
@@ -214,6 +216,7 @@ class MemoryOut(BaseModel):
|
||||
pinned: bool
|
||||
category: str
|
||||
source: str
|
||||
scope: str = "personal"
|
||||
tags: List[str]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
@@ -328,6 +331,7 @@ def memory_save(body: MemoryIn):
|
||||
pinned=body.pinned,
|
||||
category=body.category,
|
||||
source=body.source,
|
||||
scope=body.scope,
|
||||
tags=body.tags,
|
||||
conversation_id=body.conversation_id,
|
||||
attachments=body.attachments or [],
|
||||
@@ -353,6 +357,8 @@ def memory_update(point_id: str, body: MemoryUpdate):
|
||||
existing.pinned = body.pinned
|
||||
if body.category is not None:
|
||||
existing.category = body.category
|
||||
if body.scope is not None:
|
||||
existing.scope = body.scope
|
||||
if body.tags is not None:
|
||||
existing.tags = body.tags
|
||||
|
||||
@@ -537,12 +543,23 @@ def memory_import_files():
|
||||
# Wiederherstellen einer schlanken ARIA nach Wipe.
|
||||
|
||||
@app.get("/memory/export-bootstrap")
|
||||
def memory_export_bootstrap():
|
||||
"""Gibt alle pinned Memories als JSON zurueck — fuer Browser-Download."""
|
||||
def memory_export_bootstrap(scope: str = "system"):
|
||||
"""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()
|
||||
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 {
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"scope": scope,
|
||||
"exported_at": __import__("datetime").datetime.now(
|
||||
__import__("datetime").timezone.utc
|
||||
).isoformat(),
|
||||
@@ -555,6 +572,7 @@ def memory_export_bootstrap():
|
||||
"pinned": True,
|
||||
"category": p.category,
|
||||
"source": p.source,
|
||||
"scope": p.scope,
|
||||
"tags": p.tags,
|
||||
}
|
||||
for p in pinned
|
||||
@@ -564,13 +582,18 @@ def memory_export_bootstrap():
|
||||
|
||||
class BootstrapBundle(BaseModel):
|
||||
version: int = 1
|
||||
scope: Optional[str] = None # system | personal | all (aus dem Export)
|
||||
memories: List[dict]
|
||||
|
||||
|
||||
@app.post("/memory/import-bootstrap")
|
||||
def memory_import_bootstrap(body: BootstrapBundle):
|
||||
"""Loescht alle pinned Memories und importiert die im Bundle.
|
||||
Cold Memory (unpinned) bleibt unangetastet.
|
||||
"""Importiert ein Bootstrap-Bundle scope-sicher.
|
||||
|
||||
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
|
||||
Caller soll erst exportieren und dann importieren.
|
||||
@@ -580,23 +603,31 @@ def memory_import_bootstrap(body: BootstrapBundle):
|
||||
|
||||
s = store()
|
||||
e = embedder()
|
||||
|
||||
# Alle aktuell pinned Punkte loeschen
|
||||
from qdrant_client.http import models as qm
|
||||
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(
|
||||
collection_name=COLLECTION,
|
||||
points_selector=qm.FilterSelector(filter=qm.Filter(must=[
|
||||
qm.FieldCondition(key="pinned", match=qm.MatchValue(value=True))
|
||||
])),
|
||||
points_selector=qm.FilterSelector(filter=qm.Filter(must=del_must)),
|
||||
)
|
||||
|
||||
# Neue Punkte einspeisen
|
||||
# Neue Punkte einspeisen — scope pro memory (Fallback: bundle_scope bzw. personal).
|
||||
created = 0
|
||||
for m in body.memories:
|
||||
content = (m.get("content") or "").strip()
|
||||
if not content:
|
||||
continue
|
||||
mscope = m.get("scope") or (bundle_scope if bundle_scope != "all" else "personal")
|
||||
point = MemoryPoint(
|
||||
id="",
|
||||
type=m.get("type", "fact"),
|
||||
@@ -605,13 +636,14 @@ def memory_import_bootstrap(body: BootstrapBundle):
|
||||
pinned=True,
|
||||
category=m.get("category", ""),
|
||||
source=m.get("source", "bootstrap-import"),
|
||||
scope=mscope,
|
||||
tags=list(m.get("tags", [])),
|
||||
)
|
||||
vec = e.embed(content)
|
||||
s.upsert(point, vec)
|
||||
created += 1
|
||||
|
||||
return {"created": created, "deleted_previous_pinned": True}
|
||||
return {"created": created, "scope": bundle_scope, "deleted_previous_pinned": True}
|
||||
|
||||
|
||||
# ─── Conversation-Loop ──────────────────────────────────────────────
|
||||
|
||||
@@ -11,6 +11,10 @@ Punkt-Schema (Payload):
|
||||
content — eigentlicher Text (wird embedded)
|
||||
pinned — bool, True = Hot Memory (immer in Prompt)
|
||||
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
|
||||
created_at, updated_at — ISO-Strings
|
||||
conversation_id — optional, nur fuer type=conversation
|
||||
@@ -55,6 +59,7 @@ class MemoryPoint:
|
||||
pinned: bool = False
|
||||
category: str = ""
|
||||
source: str = "manual"
|
||||
scope: str = "personal" # system | personal — steuert Bootstrap-Export
|
||||
tags: List[str] = field(default_factory=list)
|
||||
created_at: str = ""
|
||||
updated_at: str = ""
|
||||
@@ -74,6 +79,7 @@ class MemoryPoint:
|
||||
"pinned": self.pinned,
|
||||
"category": self.category,
|
||||
"source": self.source,
|
||||
"scope": self.scope,
|
||||
"tags": self.tags,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
@@ -94,6 +100,7 @@ class MemoryPoint:
|
||||
pinned=payload.get("pinned", False),
|
||||
category=payload.get("category", ""),
|
||||
source=payload.get("source", "manual"),
|
||||
scope=payload.get("scope", "personal"),
|
||||
tags=payload.get("tags", []),
|
||||
created_at=payload.get("created_at", ""),
|
||||
updated_at=payload.get("updated_at", ""),
|
||||
@@ -120,14 +127,23 @@ class VectorStore:
|
||||
collection_name=COLLECTION,
|
||||
vectors_config=qm.VectorParams(size=VECTOR_DIM, distance=qm.Distance.COSINE),
|
||||
)
|
||||
# Indexe fuer typische Filter-Felder
|
||||
for field_name in ("type", "pinned", "category", "source", "migration_key"):
|
||||
# Indexe fuer typische Filter-Felder — idempotent, laeuft auch auf
|
||||
# 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(
|
||||
collection_name=COLLECTION,
|
||||
field_name=field_name,
|
||||
field_schema=qm.PayloadSchemaType.KEYWORD if field_name != "pinned"
|
||||
else qm.PayloadSchemaType.BOOL,
|
||||
field_schema=schema,
|
||||
)
|
||||
except Exception:
|
||||
# Index existiert bereits — kein Problem.
|
||||
pass
|
||||
|
||||
# ─── Schreib-Operationen ─────────────────────────────────────────
|
||||
|
||||
@@ -164,6 +180,38 @@ class VectorStore:
|
||||
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]:
|
||||
return self._scroll(
|
||||
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}",
|
||||
content=btext, category="allgemein",
|
||||
migration_key=f"{source_file}/general-{idx}",
|
||||
scope="personal",
|
||||
))
|
||||
else:
|
||||
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,
|
||||
content=content, category=cat_key,
|
||||
migration_key=f"{source_file}/{cat_key}",
|
||||
scope="personal",
|
||||
))
|
||||
return points
|
||||
|
||||
@@ -283,7 +285,11 @@ def _mk(
|
||||
migration_key: str,
|
||||
pinned: bool = True,
|
||||
category: str = "",
|
||||
scope: str = "system",
|
||||
) -> 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(
|
||||
id="",
|
||||
type=type_,
|
||||
@@ -292,6 +298,7 @@ def _mk(
|
||||
pinned=pinned,
|
||||
category=category,
|
||||
source="import",
|
||||
scope=scope,
|
||||
tags=[],
|
||||
)
|
||||
# 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,
|
||||
"category": rule.get("category", ""),
|
||||
"source": "seed",
|
||||
"scope": "system",
|
||||
"tags": [],
|
||||
"created_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="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;">
|
||||
Klein und schnell: <strong>nur</strong> die pinned Memories (Identität, Regeln, Präferenzen, Tools, Skills) als JSON.
|
||||
Use-Case: Wipe → Bootstrap-Import → ARIA hat Persönlichkeit zurück, sonst leer.
|
||||
Cold Memory (Konversations-Fakten) bleibt beim Import unangetastet.
|
||||
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.
|
||||
<span style="color:#FF9F0A;">Persönlich</span> = Stefan-spezifisch (Name, Zugangsdaten, Projekte) — bleibt privat.
|
||||
Import ersetzt nur die pinned Memories des jeweiligen scope; Cold Memory bleibt unangetastet.
|
||||
</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)">
|
||||
<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>
|
||||
@@ -1408,6 +1410,11 @@
|
||||
<input type="checkbox" id="memory-pinned">
|
||||
<span>📌 Pinned (Hot Memory — IMMER im System-Prompt)</span>
|
||||
</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 -->
|
||||
<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 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>` : '';
|
||||
// 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;">
|
||||
<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>` : ''}
|
||||
</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-tags').value = (m.tags || []).join(', ');
|
||||
document.getElementById('memory-pinned').checked = !!m.pinned;
|
||||
document.getElementById('memory-scope').value = (m.scope === 'system') ? 'system' : 'personal';
|
||||
// Anhang-Block sichtbar — Liste rendern
|
||||
if (attBlock) attBlock.style.display = 'block';
|
||||
if (attHint) attHint.style.display = 'none';
|
||||
@@ -6029,6 +6043,7 @@
|
||||
document.getElementById('memory-category').value = '';
|
||||
document.getElementById('memory-tags').value = '';
|
||||
document.getElementById('memory-pinned').checked = false;
|
||||
document.getElementById('memory-scope').value = 'personal';
|
||||
// Bei neuem Memory: nur Hinweis, dass Anhaenge nach Save gehen
|
||||
if (attBlock) attBlock.style.display = 'none';
|
||||
if (attHint) attHint.style.display = 'block';
|
||||
@@ -6133,6 +6148,7 @@
|
||||
const category = document.getElementById('memory-category').value.trim();
|
||||
const tags = document.getElementById('memory-tags').value.split(',').map(t => t.trim()).filter(Boolean);
|
||||
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 (!content) { errEl.textContent = 'Inhalt fehlt.'; errEl.style.display = 'block'; return; }
|
||||
@@ -6143,13 +6159,13 @@
|
||||
r = await fetch('/api/brain/memory/update/' + encodeURIComponent(id), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, content, pinned, category, tags }),
|
||||
body: JSON.stringify({ title, content, pinned, category, scope, tags }),
|
||||
});
|
||||
} else {
|
||||
r = await fetch('/api/brain/memory/save', {
|
||||
method: 'POST',
|
||||
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) {
|
||||
@@ -6526,11 +6542,12 @@
|
||||
}
|
||||
|
||||
// ── Bootstrap Export / Import ──────────────────────────
|
||||
async function exportBootstrap() {
|
||||
async function exportBootstrap(scope) {
|
||||
scope = scope || 'system';
|
||||
const status = document.getElementById('bootstrap-status');
|
||||
if (status) status.innerHTML = '⏳ Lade...';
|
||||
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);
|
||||
const data = await r.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 a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `aria-bootstrap-${ts}.json`;
|
||||
a.download = `aria-bootstrap-${scope}-${ts}.json`;
|
||||
document.body.appendChild(a); a.click();
|
||||
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) {
|
||||
if (status) status.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
|
||||
}
|
||||
@@ -6555,7 +6573,11 @@
|
||||
const text = await file.text();
|
||||
const bundle = JSON.parse(text);
|
||||
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 = '';
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user