feat(memory): Anhaenge in App-Bubble + System-Prompt (Stufe C + D)

Stufe C — App:
- ChatMessage.memorySaved.attachments [{name, mime, size, path, localUri}]
- memory_saved-Listener uebernimmt payload.attachments
- renderMessage memorySaved-Bubble zeigt Anhaenge als Tap-Reihen
  (Icon 🖼/📄 + Filename + Hint). Tap → file_request via Bridge,
  beim ersten Mal "(tippen zum Laden)" → nach file_response cached
  + bei Bildern setFullscreenImage, bei anderen openFileWithIntent
- file_response-Handler updated zusaetzlich memorySaved.attachments
  per serverPath-Match
- Styles fuer memoryAttachmentRow/Icon/Name/Meta

Stufe D — System-Prompt:
- prompts._attachments_line: pro Memory eine Zeile
  "📎 Anhaenge: foo.jpg (image/jpeg, 109 KB) — Pfad: /shared/memory-attachments/<id>/"
- Wird in build_hot_memory_section + build_cold_memory_section
  nach dem Content angehangen
- ARIA "weiss" damit dass Anhaenge da sind und kann via Bash darauf
  zugreifen (file, head, base64 …). Echt sehen kann sie sie erst mit
  Multi-Modal-Pipeline (Stufe E)
- memory_save Dispatcher: attachments-Liste auch im memory_saved-Event
  (vermutlich [] beim Save, aber konsistent fuer spaeteres
  Speichern-mit-Anhaengen-Pattern)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 02:45:51 +02:00
parent de9b7b46f9
commit d5531521fa
3 changed files with 129 additions and 6 deletions
+32
View File
@@ -52,6 +52,29 @@ TYPE_HEADINGS = {
}
def _attachments_line(p: MemoryPoint) -> str:
"""Eine Zeile die ARIA verraet welche Dateien an einer Memory haengen.
Bilder/Files liegen physisch unter /shared/memory-attachments/<id>/<name>
— ARIA kann sie via Bash anschauen (file, head, base64...) wenn relevant.
Sehen kann sie sie erst mit der Multi-Modal-Pipeline (Stufe E)."""
atts = getattr(p, "attachments", None) or []
if not atts:
return ""
items = []
for a in atts:
if not isinstance(a, dict):
continue
name = a.get("name", "?")
mime = a.get("mime", "")
size = a.get("size")
size_part = f", {size // 1024} KB" if isinstance(size, int) and size else ""
items.append(f"{name} ({mime}{size_part})")
if not items:
return ""
base_dir = f"/shared/memory-attachments/{p.id}/" if p.id else ""
return f"📎 Anhaenge: {', '.join(items)}" + (f" — Pfad: {base_dir}" if base_dir else "")
def build_hot_memory_section(pinned: List[MemoryPoint]) -> str:
"""Baue den 'IMMER-im-Prompt'-Block aus pinned Punkten."""
grouped: dict[str, List[MemoryPoint]] = {}
@@ -69,6 +92,9 @@ def build_hot_memory_section(pinned: List[MemoryPoint]) -> str:
for p in items:
parts.append(f"### {p.title}")
parts.append(p.content.strip())
att_line = _attachments_line(p)
if att_line:
parts.append(att_line)
parts.append("")
# uebrige Types (falls jemand was anderes als pinned markiert)
@@ -77,6 +103,9 @@ def build_hot_memory_section(pinned: List[MemoryPoint]) -> str:
for p in items:
parts.append(f"### {p.title}")
parts.append(p.content.strip())
att_line = _attachments_line(p)
if att_line:
parts.append(att_line)
parts.append("")
return "\n".join(parts).strip()
@@ -91,6 +120,9 @@ def build_cold_memory_section(matches: List[MemoryPoint]) -> str:
score = f" [score={p.score:.2f}]" if p.score is not None else ""
lines.append(f"- **{p.title}**{score}")
lines.append(f" {p.content.strip()}")
att_line = _attachments_line(p)
if att_line:
lines.append(f" {att_line}")
return "\n".join(lines)