feat(diagnostic): Quell-Badge an ARIA-Bubbles (local / claude / fast-path)

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 13:40:36 +02:00
co-authored by Claude Opus 4.8
parent 53c098a9c8
commit 9cc1aec5ec
6 changed files with 39 additions and 10 deletions
+4 -4
View File
@@ -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 ───────────────────────────────────────
+1 -1
View File
@@ -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]})
+5 -1
View File
@@ -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] = [
+9 -1
View File
@@ -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)
+18 -2
View File
@@ -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
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${m.ts})">🗑</button>`
: '';
const innerHtml = `${trashBtn}${linked}<div class="meta">${escapeHtml(m.meta)} — ${time}</div>`;
const histBadge = srcBadgeHtml(m.type, m.answeredBy || '');
const innerHtml = `${trashBtn}${linked}<div class="meta">${escapeHtml(m.meta)}${histBadge} — ${time}</div>`;
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 `<span title="Antwort erzeugt von: ${escapeHtml(answeredBy)}" style="display:inline-block;margin-left:6px;padding:1px 6px;border-radius:8px;font-size:9px;font-weight:bold;background:${b.c}22;color:${b.c};border:1px solid ${b.c}55;">${b.t}</span>`;
}
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
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${backupTs})">🗑</button>`
: '';
const html = `${trashBtn}${linked}${ttsBlock}${gpsBlock}<div class="meta">${escapeHtml(meta)} — ${new Date().toLocaleTimeString('de-DE')}</div>`;
// Quell-Badge: welcher Backend die Antwort erzeugt hat (nur ARIA-Bubbles)
const srcBadge = srcBadgeHtml(type, (options && options.answeredBy) || '');
const html = `${trashBtn}${linked}${ttsBlock}${gpsBlock}<div class="meta">${escapeHtml(meta)}${srcBadge} — ${new Date().toLocaleTimeString('de-DE')}</div>`;
// Thinking-Indikator ausblenden bei neuer Nachricht
updateThinkingIndicator({ activity: 'idle' });
+2 -1
View File
@@ -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 }));