From d5531521faa550181ee71c37c1a932dde1d48c0f Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 13 May 2026 02:45:51 +0200 Subject: [PATCH] feat(memory): Anhaenge in App-Bubble + System-Prompt (Stufe C + D) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//" - 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) --- android/src/screens/ChatScreen.tsx | 102 +++++++++++++++++++++++++++-- aria-brain/agent.py | 1 + aria-brain/prompts.py | 32 +++++++++ 3 files changed, 129 insertions(+), 6 deletions(-) diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index edc882a..ea9035e 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -89,11 +89,19 @@ interface ChatMessage { }; /** Memory-Saved-Bubble: ARIA hat etwas via memory_save in die Qdrant-DB gepackt */ memorySaved?: { + id?: string; title: string; type: string; category?: string; pinned: boolean; preview?: string; + attachments?: Array<{ + name: string; + mime?: string; + size?: number; + path?: string; // Server-Pfad /shared/memory-attachments// + localUri?: string; // Nach file_request gefuelltes file://-URI + }>; }; /** Backup-Timestamp aus chat_backup.jsonl auf dem Bridge โ€” Voraussetzung * zum Loeschen der Bubble via Muelltonne. Lokale Bubbles ohne backupTs @@ -534,17 +542,25 @@ const ChatScreen: React.FC = () => { // gepackt โ€” eigene Bubble (gelb wie trigger/skill). if (message.type === 'memory_saved') { const p = (message.payload || {}) as any; + const atts = Array.isArray(p.attachments) ? p.attachments.map((a: any) => ({ + name: String(a?.name || 'datei'), + mime: a?.mime ? String(a.mime) : undefined, + size: typeof a?.size === 'number' ? a.size : undefined, + path: a?.path ? String(a.path) : undefined, + })) : []; const memoryMsg: ChatMessage = { id: nextId(), sender: 'aria', text: '', timestamp: Date.now(), memorySaved: { + id: p.id ? String(p.id) : undefined, title: String(p.title || '(ohne Titel)'), type: String(p.type || 'fact'), category: p.category ? String(p.category) : undefined, pinned: !!p.pinned, preview: p.content_preview ? String(p.content_preview) : undefined, + attachments: atts.length ? atts : undefined, }, }; setMessages(prev => capMessages([...prev, memoryMsg])); @@ -595,16 +611,38 @@ const ChatScreen: React.FC = () => { if (b64 && reqId) { const fileName = (message.payload.name as string) || 'download'; persistAttachment(b64, reqId, fileName).then(filePath => { - setMessages(prev => prev.map(m => ({ - ...m, - attachments: m.attachments?.map(a => + setMessages(prev => prev.map(m => { + // Hauptattachments updaten (Bilder/Files am User-Send / ARIA-File-Bubble) + const updatedAtts = m.attachments?.map(a => a.serverPath === serverPath ? { ...a, uri: filePath } : a - ), - }))); + ); + // Memory-Anhang-Match (Bubble vom memory_saved-Event) + const ms = m.memorySaved; + let updatedMs = ms; + if (ms && Array.isArray(ms.attachments)) { + const hit = ms.attachments.some(a => a.path === serverPath); + if (hit) { + updatedMs = { + ...ms, + attachments: ms.attachments.map(a => + a.path === serverPath ? { ...a, localUri: filePath } : a + ), + }; + } + } + return { ...m, attachments: updatedAtts, memorySaved: updatedMs }; + })); // Wenn der User dieses File explizit oeffnen wollte โ†’ Intent-Picker + // (Bilder werden separat via setFullscreenImage in der memorySaved- + // Bubble geoeffnet, das laeuft nicht ueber autoOpenPaths) if (serverPath && autoOpenPaths.current.has(serverPath)) { autoOpenPaths.current.delete(serverPath); - openFileWithIntent(filePath.replace(/^file:\/\//, ''), mimeType); + const isImage = (mimeType || '').startsWith('image/'); + if (isImage) { + setFullscreenImage(filePath); + } else { + openFileWithIntent(filePath.replace(/^file:\/\//, ''), mimeType); + } } }).catch(() => {}); } @@ -1287,6 +1325,7 @@ const ChatScreen: React.FC = () => { if (item.memorySaved) { const m = item.memorySaved; const catPart = m.category ? ` ยท [${m.category}]` : ''; + const atts = m.attachments || []; return ( @@ -1299,6 +1338,35 @@ const ChatScreen: React.FC = () => { {m.preview ? ( {m.preview}{m.preview.length >= 140 ? 'โ€ฆ' : ''} ) : null} + {atts.map((a, idx) => { + const isImage = (a.mime || '').startsWith('image/'); + const icon = isImage ? '๐Ÿ–ผ๏ธ' : '๐Ÿ“„'; + const sizeStr = a.size ? ` ยท ${(a.size / 1024).toFixed(0)} KB` : ''; + return ( + { + if (!a.path) return; + if (a.localUri) { + if (isImage) setFullscreenImage(a.localUri); + else openFileWithIntent(a.localUri.replace(/^file:\/\//, ''), a.mime || ''); + } else { + // Datei via Bridge nachladen โ€” file_response hat den + // memorySaved-Match-Path und cached + zeigt direkt + autoOpenPaths.current.add(a.path); + rvs.send('file_request' as any, { serverPath: a.path, requestId: `memAtt_${item.id}_${idx}` }); + } + }} + > + {icon} + {a.name} + + {a.localUri ? '(tippen zum oeffnen)' : `(tippen zum Laden${sizeStr})`} + + + ); + })} ARIA-Memory ยท {time} ); @@ -2065,6 +2133,28 @@ const styles = StyleSheet.create({ playButtonText: { fontSize: 16, }, + memoryAttachmentRow: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#0D0D1A', + borderRadius: 6, + paddingHorizontal: 8, + paddingVertical: 6, + marginTop: 4, + gap: 6, + }, + memoryAttachmentIcon: { + fontSize: 16, + }, + memoryAttachmentName: { + flex: 1, + color: '#E0E0F0', + fontSize: 12, + }, + memoryAttachmentMeta: { + color: '#555570', + fontSize: 10, + }, bubbleTrash: { position: 'absolute', top: 4, diff --git a/aria-brain/agent.py b/aria-brain/agent.py index c8046d8..afbab97 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -546,6 +546,7 @@ class Agent: "id": saved.id, "type": saved.type, "title": saved.title, "content_preview": (saved.content or "")[:140], "category": saved.category, "pinned": saved.pinned, + "attachments": saved.attachments or [], }, }) return (f"OK โ€” Memory '{title}' gespeichert " diff --git a/aria-brain/prompts.py b/aria-brain/prompts.py index 161708f..5ba12df 100644 --- a/aria-brain/prompts.py +++ b/aria-brain/prompts.py @@ -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// + โ€” 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)