From 9cc1aec5ecca82ccd138babb0a7970f60fa550a2 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 11 Jul 2026 13:40:36 +0200 Subject: [PATCH] feat(diagnostic): Quell-Badge an ARIA-Bubbles (local / claude / fast-path) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zeigt pro Antwort, welcher Backend sie erzeugt hat — farbcodiert (lokal=gruen, Claude=blau, Fast-Path=lila). Nur in Diagnostic (App bleibt Mama-tauglich). - agent.chat() gibt jetzt (reply, answered_by) zurueck; an jedem Return-Punkt gesetzt (fast-path/local/claude). Beide Aufrufer (main.py, background.py) angepasst. main.py: ChatOut.answered_by. - bridge: liest answered_by aus /chat, reicht es an _process_core_response, broadcastet es im chat-Payload UND persistiert es in chat_backup (answeredBy) → bleibt nach Reload. - diagnostic: srcBadgeHtml() rendert den Badge in Live-Chat + History; server.js liefert answeredBy in der chat_history. Erweiterbar: spaeter kann ein Bild-Backend (FLUX/Modellname) denselben Kanal nutzen. Modellwechsel-fuer-Antworten (groessere Modelle bei mehr GPUs) bleibt B0.5/Skalierungs-Thema im Plan. Co-Authored-By: Claude Opus 4.8 --- aria-brain/agent.py | 8 ++++---- aria-brain/background.py | 2 +- aria-brain/main.py | 6 +++++- bridge/aria_bridge.py | 10 +++++++++- diagnostic/index.html | 20 ++++++++++++++++++-- diagnostic/server.js | 3 ++- 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index ea2690c..ca4e95b 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1263,7 +1263,7 @@ class Agent: def chat(self, user_message: str, source: str = "", project_id: Optional[str] = None, - pending_queue: Optional[list[str]] = None) -> str: + pending_queue: Optional[list[str]] = None) -> tuple: """Verarbeitet eine User-Nachricht — pro Request project_id explizit angegeben (leer = Hauptchat). Kein globaler active_project-State mehr — so laufen parallele /chat-Requests fuer verschiedene Projekte echt @@ -1297,7 +1297,7 @@ class Agent: self.conversation.add("assistant", fast_reply, project_id=active_project_id) if active_project_id: projects_mod.touch_project(active_project_id) - return fast_reply + return fast_reply, "fast-path" # 1. User-Turn an die Konversation self.conversation.add("user", user_message, source=source, @@ -1311,7 +1311,7 @@ class Agent: # teure Claude-Aufbau + Tool-Loop wird uebersprungen. Sonst None → Claude. local_reply = self._try_local_fast_lane(user_message, active_project_id) if local_reply is not None: - return local_reply + return local_reply, "local" # 2. Hot Memory (alle pinned Punkte) hot = self.store.list_pinned() @@ -1512,7 +1512,7 @@ class Agent: # 7. Assistant-Turn (final reply) in die Conversation self.conversation.add("assistant", final_reply, project_id=active_project_id) - return final_reply + return final_reply, "claude" # ── Tool-Dispatcher ─────────────────────────────────────── diff --git a/aria-brain/background.py b/aria-brain/background.py index 0694c39..3c216bd 100644 --- a/aria-brain/background.py +++ b/aria-brain/background.py @@ -150,7 +150,7 @@ async def _fire(trigger: dict, agent_factory) -> None: try: agent = agent_factory() - reply = agent.chat(prompt, source="trigger") + reply, _ = agent.chat(prompt, source="trigger") events = agent.pop_events() logger.info("[trigger] %s gefeuert → ARIA-Reply: %s", name, reply[:80]) triggers_mod.append_log(name, {"event": "reply", "text": reply[:500]}) diff --git a/aria-brain/main.py b/aria-brain/main.py index b3c7be7..46c387d 100644 --- a/aria-brain/main.py +++ b/aria-brain/main.py @@ -630,6 +630,9 @@ class ChatOut(BaseModel): turns: int distilling: bool events: list = Field(default_factory=list) + # Welcher Backend die Antwort erzeugt hat: "local" (Qwen), "claude", + # "fast-path" (Skill/Regex). Fuer den Quell-Badge in Diagnostic. + answered_by: str = "claude" # Echo der project_id die dieser Turn hatte. Bridge nutzt sie damit die # ausgehende Chat-Bubble sauber getaggt in der richtigen Thread-Bahn der # UI landet. @@ -713,7 +716,7 @@ async def chat(body: ChatIn, background: BackgroundTasks): # Sync-Aufruf im Executor damit wir den Event-Loop nicht blocken — # chat() macht HTTP-Calls (Proxy) die 30-60s dauern koennen. loop = asyncio.get_running_loop() - reply = await loop.run_in_executor( + reply, answered_by = await loop.run_in_executor( None, lambda: a.chat( body.message, source=body.source, project_id=pid, @@ -735,6 +738,7 @@ async def chat(body: ChatIn, background: BackgroundTasks): distilling=needs_distill, events=a.pop_events(), project_id=pid, + answered_by=answered_by, ) finally: _project_pending[pid] = [ diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index 3831938..a0559e9 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -1257,12 +1257,14 @@ class ARIABridge: # Voice-Tag-Noise als Kontext sieht). # File-Marker werden separat als file_from_aria-Events ausgeliefert. display_text = strip_voice_tag_for_display(text) + _answered_by = (payload.get("answeredBy") or "") if isinstance(payload, dict) else "" assistant_backup_ts = self._append_chat_backup({ "role": "assistant", "text": display_text, "files": [{"serverPath": f["serverPath"], "name": f["name"], "mimeType": f["mimeType"], "size": f["size"]} for f in aria_files], "project_id": turn_pid, + "answeredBy": _answered_by, }) metadata = payload.get("metadata", {}) @@ -1305,6 +1307,8 @@ class ARIABridge: # Projekt-Zuordnung — App + Diagnostic sortieren die Bubble in # den passenden Projekt-Block. Leer = Hauptchat. "projectId": (payload.get("projectId") or "") if isinstance(payload, dict) else "", + # Quell-Backend (local/claude/fast-path) fuer den Diagnostic-Badge. + "answeredBy": (payload.get("answeredBy") or "") if isinstance(payload, dict) else "", }, "timestamp": int(asyncio.get_event_loop().time() * 1000), }) @@ -1633,6 +1637,9 @@ class ARIABridge: # gegeben damit der chat-Broadcast die Bubble dem richtigen Projekt- # Block in App + Diagnostic zuordnen kann. turn_project_id = (data.get("project_id") or "").strip() + # Welcher Backend geantwortet hat (local/claude/fast-path) — fuer den + # Quell-Badge in Diagnostic. + answered_by = (data.get("answered_by") or "claude").strip() # Side-Channel-Events VOR der Chat-Bubble broadcasten (z.B. skill_created) # damit sie in der UI vor der Reply auftauchen @@ -1699,7 +1706,8 @@ class ARIABridge: # passend behandelt wird (hier minimal, weil Brain noch keine # metadata mitschickt). try: - await self._process_core_response(reply, {"projectId": turn_project_id}) + await self._process_core_response(reply, {"projectId": turn_project_id, + "answeredBy": answered_by}) except Exception: logger.exception("[brain] _process_core_response Fehler") await self._emit_activity("idle", "", project_id=project_id) diff --git a/diagnostic/index.html b/diagnostic/index.html index b0c6758..310ff62 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -1891,6 +1891,7 @@ ttsText: p.ttsText, backupTs: p.backupTs, projectId: p.projectId || '', + answeredBy: p.answeredBy || '', }); return; } @@ -1992,7 +1993,8 @@ const trashBtn = m.ts ? `` : ''; - const innerHtml = `${trashBtn}${linked}
${escapeHtml(m.meta)} — ${time}
`; + const histBadge = srcBadgeHtml(m.type, m.answeredBy || ''); + const innerHtml = `${trashBtn}${linked}
${escapeHtml(m.meta)}${histBadge} — ${time}
`; for (const b of boxes) { const el = document.createElement('div'); el.className = `chat-msg ${m.type}`; @@ -2323,6 +2325,18 @@ return t.trim(); } + // Quell-Badge (local/claude/fast-path) fuer ARIA-Bubbles — in Live + History genutzt. + function srcBadgeHtml(type, answeredBy) { + if (type !== 'received' || !answeredBy) return ''; + const M = { + 'local': { t: '⚡ lokal', c: '#34C759' }, + 'claude': { t: 'Claude', c: '#0096FF' }, + 'fast-path': { t: '⚡ Fast-Path', c: '#AF7BFF' }, + }; + const b = M[answeredBy] || { t: answeredBy, c: '#8888AA' }; + return `${b.t}`; + } + function addChat(type, text, meta, options) { // [FILE: /shared/uploads/aria_xxx.ext]-Marker aus dem Antworttext entfernen — // die Datei kommt separat via file_from_aria-Event als eigene Bubble. @@ -2357,7 +2371,9 @@ const trashBtn = backupTs ? `` : ''; - const html = `${trashBtn}${linked}${ttsBlock}${gpsBlock}
${escapeHtml(meta)} — ${new Date().toLocaleTimeString('de-DE')}
`; + // Quell-Badge: welcher Backend die Antwort erzeugt hat (nur ARIA-Bubbles) + const srcBadge = srcBadgeHtml(type, (options && options.answeredBy) || ''); + const html = `${trashBtn}${linked}${ttsBlock}${gpsBlock}
${escapeHtml(meta)}${srcBadge} — ${new Date().toLocaleTimeString('de-DE')}
`; // Thinking-Indikator ausblenden bei neuer Nachricht updateThinkingIndicator({ activity: 'idle' }); diff --git a/diagnostic/server.js b/diagnostic/server.js index 108e5dd..4208270 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -2816,6 +2816,7 @@ async function handleLoadChatHistory(clientWs) { const ts = obj.ts || 0; const text = String(obj.text || ""); const projectId = String(obj.project_id || ""); // Multi-Threading: Kontext-Zuordnung + const answeredBy = String(obj.answeredBy || ""); // Quell-Badge (local/claude/fast-path) if (obj.role === "user") { if (text) messages.push({ type: "sent", text, meta: "Gateway direkt", ts, projectId }); continue; @@ -2842,7 +2843,7 @@ async function handleLoadChatHistory(clientWs) { projectId, }); } - if (text) messages.push({ type: "received", text, meta: "chat:final", ts, projectId }); + if (text) messages.push({ type: "received", text, meta: "chat:final", ts, projectId, answeredBy }); } clientWs.send(JSON.stringify({ type: "chat_history", messages }));