Pro Memory koennen jetzt Dateien (Bilder, PDFs, Sound, ...) angehaengt
werden. Use-Case: Stefan sagt "ich hab eine Cessna 172" und pinnt
gleich ein Foto dran — ARIA sieht spaeter neben dem Memory auch die
visuelle Referenz (Stufe E = Multi-Modal-Pipeline).
Stufe A baut nur den Backend-Layer; UI kommt in Stufe B (Diagnostic)
und C (App). Anhaenge werden in Stufe A nur via HTTP-API gepflegt
(curl), ARIA selbst kann sie noch nicht hochladen — sinnvoll erst
wenn die Vision-Pipeline (Stufe E) steht.
Komponenten:
- memory_attachments.py: neuer Storage-Helper. Layout
/shared/memory-attachments/<memory-id>/<safe-filename>.
Filename-Sanitization (kein Path-Traversal), Limit 20 MB
konfigurierbar, save/list/delete/read_bytes + delete_all fuer
Cleanup beim Memory-Delete.
- vector_store.py: MemoryPoint.attachments (List[dict]) — Metadaten
{name, mime, size, path} im Qdrant-Payload damit Suche/Anzeige
sie ohne Filesystem-Lookup kennt.
- main.py:
- MemoryIn akzeptiert attachments-Liste (fuer Restore-Faelle)
- MemoryOut liefert attachments
- GET /memory/{id}/attachments → Liste vom FS
- POST /memory/{id}/attachments → Base64-Upload,
schreibt FS + updated Payload-Liste
- DELETE /memory/{id}/attachments/{filename} → FS + Payload-Eintrag weg
- GET /memory/{id}/attachments/{filename} → Bytes mit MIME serve
- /memory/delete cleanup: ruft attachments.delete_all damit kein
Verzeichnis verwaist
Smoke-Test nach Brain-Rebuild (Stefan auf VM):
# Memory-ID rauspicken
ID=$(curl -s "$ARIA_BRAIN_URL/memory/list?type=fact" | python3 -c "import sys,json;print(json.load(sys.stdin)[0]['id'])")
# Bild als Base64 hochladen
B64=$(base64 -w0 /pfad/zu/foto.jpg)
curl -s -X POST "$ARIA_BRAIN_URL/memory/$ID/attachments" \
-H 'Content-Type: application/json' \
-d "{\"name\":\"foto.jpg\",\"data_base64\":\"$B64\"}" | jq
# Liste anzeigen
curl -s "$ARIA_BRAIN_URL/memory/$ID/attachments" | jq
# Datei wieder laden
curl -s "$ARIA_BRAIN_URL/memory/$ID/attachments/foto.jpg" -o /tmp/back.jpg
Stufe B (Diagnostic-UI) folgt sobald A getestet ist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stefan wollte ne richtige Suche statt nur "klingt aehnlich". Beide
Modi sind jetzt verfuegbar, Default ist Volltext:
- 📝 Wortlich (Substring, case-insensitive ueber Title + Content +
Category + Tags) — neuer Endpoint /memory/search-text. Full-Scan
via Qdrant scroll, k=50. Findet "cessna" exakt im Content. Bei
kleiner DB (<1000 Eintraege) unkritisch performant.
- 🧠 Semantisch (Embedder + score_threshold 0.30) — bestehender
/memory/search Endpoint. Findet konzeptuell verwandte Eintraege.
Diagnostic UI: Dropdown neben dem Suchfeld zum Modus-Wechsel.
Info-Banner zeigt klar welcher Modus aktiv ist.
Warum Wortlich Default: bei kleiner DB liefert Semantic gern False
Positives mit Score 0.30-0.45 fuer komplett unverwandte Begriffe
(z.B. "cessna" matched "Tageslog fuehren" mit 0.43). Wortlich ist
deterministisch und vermeidet das Rauschen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: bei kleiner DB (31 Eintraege) lieferte die Suche fuer JEDES Wort
fast alles als Treffer zurueck — k=20 Top-N ohne Threshold sorgte
dafuer dass auch "banane" zehn vermeintliche Treffer mit Scores
0.09-0.22 (= Rauschen) zurueckgab.
Fix:
- vector_store.search() bekommt optional score_threshold (an Qdrant
durchgereicht, das nimmt's nativ)
- /memory/search endpoint hat score_threshold-Query-Param (default 0.30)
- Diagnostic schickt k=10 + score_threshold=0.30 statt k=20 ohne Threshold
- "Keine Treffer"-Info-Box wenn alle Treffer < Threshold
MiniLM-multilingual liefert typischerweise:
>0.50 → starker Treffer
0.30-0.50 → relevant
0.20-0.30 → grenzwertig
<0.20 → Rauschen
Mit score_threshold=0 (oder None) bleibt die alte Top-N-Semantik
fuer Aufrufer die Rauschen explizit wollen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>