Compare commits
2
Commits
e26226f370
...
9ea7908fe4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ea7908fe4 | ||
|
|
7237f05344 |
@@ -216,11 +216,14 @@ Der Proxy-Container (`node:22-alpine`) installiert bei jedem Start:
|
||||
- `@anthropic-ai/claude-code` — Claude Code CLI
|
||||
- `claude-max-api-proxy` — OpenAI-kompatible API
|
||||
|
||||
Danach werden per `sed` vier Patches angewendet:
|
||||
1. **Host-Binding**: Server hoert auf `0.0.0.0` statt localhost
|
||||
2. **Model-Fallback**: Undefined Model → `claude-sonnet-4`
|
||||
3. **Content-Format**: Array → String Konvertierung fuer die CLI
|
||||
4. **Tool-Permissions**: `--dangerously-skip-permissions` Flag injizieren
|
||||
Danach wird der Proxy gepatcht:
|
||||
1. **Host-Binding** (sed): Server hoert auf `0.0.0.0` statt localhost
|
||||
2. **Tool-Permissions** (sed): `--dangerously-skip-permissions` Flag injizieren
|
||||
3. **Tool-Use-Adapter** (Datei-Overwrite aus [`proxy-patches/`](proxy-patches/)):
|
||||
- `openai-to-cli.js` injiziert das OpenAI-`tools`-Feld als `<system>`-Block mit Schema-Beschreibungen + Anweisung `<tool_call name="X">{json}</tool_call>` als Antwortformat. `role=tool`-Messages werden als `<tool_result>`-Bloecke eingewoben. Multimodal-Content (Array von Parts) bleibt String-kompatibel.
|
||||
- `cli-to-openai.js` parsed `<tool_call>`-Bloecke aus Claudes Antwort und liefert sie als echte OpenAI `tool_calls` mit `finish_reason="tool_calls"`. Pre-Tool-Text bleibt im `content`. Mehrere parallele Calls werden korrekt aufgeteilt. Model-Name null-safe.
|
||||
|
||||
**Warum?** Die npm-Version des Proxys ignoriert das `tools`-Feld komplett und reicht nur einen Prompt-String an die CLI weiter. Claude Code nutzt dann ihre internen Tools (Bash, Read, …) und „simuliert" Aktionen — z.B. `sleep 120` statt `trigger_timer`. Mit den eigenen Adaptern landen ARIA-Tools wieder auf der Linie und Side-Effects (Trigger anlegen, Skills aufrufen, GPS-Tracking schalten) funktionieren.
|
||||
|
||||
**Wichtige Umgebungsvariablen im Proxy:**
|
||||
- `HOST=0.0.0.0` — API von aussen erreichbar (Docker-Netz)
|
||||
@@ -862,7 +865,8 @@ docker exec aria-brain curl localhost:8080/memory/stats
|
||||
- [x] **Phase B Punkt 2:** Migration aus `aria-data/brain-import/` → atomare Memory-Punkte (Identity / Rule / Preference / Tool / Skill, idempotent ueber migration_key) + Bootstrap-Snapshot Export/Import (nur pinned)
|
||||
- [x] **Phase B Punkt 3:** Brain Conversation-Loop (Single-Chat UI, Rolling Window 50 Turns, Schwelle 60 → automatisches Destillat, manueller Trigger)
|
||||
- [x] **Phase B Punkt 4:** Skills-System (Python-only via local-venv, skill_create als Tool, dynamische run_<skill> Tools, Diagnostic Skills-Tab mit Logs/Toggle/Export/Import, skill_created Live-Notification in App+Diagnostic, harte Schwelle "pip → Skill")
|
||||
- [x] **Phase B Punkt 5:** Triggers-System (passive Aufweck-Quellen — Timer + Watcher mit safe Condition-Parser, GPS-near(), Diagnostic Trigger-Tab, kontinuierliches GPS-Tracking in der App fuer Use-Cases wie Blitzer-Warner)
|
||||
- [x] **Phase B Punkt 5:** Triggers-System (passive Aufweck-Quellen — Timer + Watcher mit safe Condition-Parser, GPS-near(), Diagnostic Trigger-Tab, kontinuierliches GPS-Tracking in der App fuer Use-Cases wie Blitzer-Warner). Inklusive Brain → Bridge HTTP-Push (Port 8090 intern) damit Trigger-Antworten ueber RVS in App + Diagnostic + TTS landen.
|
||||
- [x] **Proxy Tool-Use durchreichen**: claude-max-api-proxy patcht via eigene Adapter (`proxy-patches/`) den `tools`/`tool_calls`-Roundtrip — Claude Code rief vorher ihre internen Tools (Bash, sleep) statt der ARIA-Brain-Tools (trigger_timer, skill_*, ...). Jetzt funktioniert Tool-Use End-to-End.
|
||||
- [x] Sprachmodell-Setting wieder funktional (brainModel in runtime.json statt aria-core)
|
||||
- [x] App-Chat-Sync: kompletter Server-Sync bei Reconnect (Server = Source of Truth) + chat_cleared Live-Update. Lokal-only Bubbles (Skill-Notifications, laufende Voice ohne STT) bleiben erhalten.
|
||||
- [x] App: Chat-Suche mit Next/Prev Navigation statt Filter
|
||||
|
||||
@@ -14,7 +14,11 @@ Feuern bedeutet:
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
@@ -24,6 +28,34 @@ import watcher as watcher_mod
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TICK_SEC = 30
|
||||
BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090")
|
||||
|
||||
|
||||
def _push_to_bridge(reply: str, trigger_name: str, ttype: str, events: list) -> None:
|
||||
"""POSTed eine Trigger-Antwort an die Bridge fuer RVS-Broadcast + TTS.
|
||||
|
||||
Synchron via urllib — wird per run_in_executor aus dem async-Loop
|
||||
gerufen. Failures werden geloggt, brechen aber nicht ab.
|
||||
"""
|
||||
payload = json.dumps({
|
||||
"reply": reply,
|
||||
"trigger_name": trigger_name,
|
||||
"type": ttype,
|
||||
"events": events or [],
|
||||
}).encode("utf-8")
|
||||
url = f"{BRIDGE_URL}/internal/trigger-fired"
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
url, data=payload, method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
if resp.status != 200:
|
||||
logger.warning("[trigger-push] Bridge hat %s zurueckgegeben", resp.status)
|
||||
except urllib.error.URLError as exc:
|
||||
logger.warning("[trigger-push] Bridge unerreichbar (%s): %s", url, exc)
|
||||
except Exception as exc:
|
||||
logger.warning("[trigger-push] Push fehlgeschlagen: %s", exc)
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
@@ -114,8 +146,13 @@ async def _fire(trigger: dict, agent_factory) -> None:
|
||||
try:
|
||||
agent = agent_factory()
|
||||
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]})
|
||||
# Reply an die Bridge pushen, damit App + Diagnostic + TTS sie kriegen.
|
||||
# Ohne diesen Push wuerde die Antwort nur im Brain-Log landen.
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, _push_to_bridge, reply, name, ttype, events)
|
||||
except Exception as e:
|
||||
logger.exception("Trigger %s feuern fehlgeschlagen: %s", name, e)
|
||||
triggers_mod.append_log(name, {"event": "error", "error": str(e)[:300]})
|
||||
|
||||
@@ -2392,6 +2392,145 @@ class ARIABridge:
|
||||
logger.exception("Fehler in der Audio-Schleife")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# ── Internal HTTP (Brain → Bridge: Trigger-Feuer-Push) ───
|
||||
|
||||
async def _serve_internal_http(self) -> None:
|
||||
"""Kleiner asyncio HTTP-Listener auf Port 8090.
|
||||
|
||||
Empfaengt Push-Events vom Brain wenn ein Trigger feuert. Nicht
|
||||
nach aussen exposed — nur erreichbar im docker-internen aria-net.
|
||||
Endpoint:
|
||||
POST /internal/trigger-fired
|
||||
{ "reply": "...", "trigger_name": "...", "type": "timer",
|
||||
"events": [{"type":"trigger_created",...}, ...] }
|
||||
"""
|
||||
host, port = "0.0.0.0", 8090
|
||||
|
||||
async def _send_response(writer, status: int, payload: dict) -> None:
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
status_text = "OK" if status == 200 else "Error"
|
||||
writer.write(
|
||||
f"HTTP/1.1 {status} {status_text}\r\n"
|
||||
f"Content-Type: application/json\r\n"
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
f"Connection: close\r\n\r\n".encode("utf-8")
|
||||
)
|
||||
writer.write(body)
|
||||
await writer.drain()
|
||||
|
||||
async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||
try:
|
||||
request_line = await asyncio.wait_for(reader.readline(), timeout=10)
|
||||
if not request_line:
|
||||
return
|
||||
try:
|
||||
method, path, _ver = request_line.decode("utf-8", "ignore").strip().split(" ", 2)
|
||||
except ValueError:
|
||||
await _send_response(writer, 400, {"error": "bad request line"})
|
||||
return
|
||||
headers: dict[str, str] = {}
|
||||
while True:
|
||||
line = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
if not line or line in (b"\r\n", b"\n"):
|
||||
break
|
||||
name, _, value = line.decode("utf-8", "ignore").partition(":")
|
||||
headers[name.strip().lower()] = value.strip()
|
||||
content_length = int(headers.get("content-length", "0") or "0")
|
||||
body = await reader.readexactly(content_length) if content_length else b""
|
||||
|
||||
if method == "POST" and path == "/internal/trigger-fired":
|
||||
try:
|
||||
data = json.loads(body.decode("utf-8", "ignore"))
|
||||
except Exception as exc:
|
||||
await _send_response(writer, 400, {"error": f"bad json: {exc}"})
|
||||
return
|
||||
reply = (data.get("reply") or "").strip()
|
||||
trigger_name = data.get("trigger_name", "")
|
||||
ttype = data.get("type", "trigger")
|
||||
events = data.get("events") or []
|
||||
logger.info("[bridge ← brain] Trigger '%s' (%s) gefeuert, reply=%d chars, events=%d",
|
||||
trigger_name, ttype, len(reply), len(events))
|
||||
# Async-spawn — HTTP-Antwort nicht durch RVS-Broadcast blockieren
|
||||
asyncio.create_task(
|
||||
self._handle_trigger_fired(reply, trigger_name, ttype, events)
|
||||
)
|
||||
await _send_response(writer, 200, {"ok": True})
|
||||
elif method == "GET" and path == "/health":
|
||||
await _send_response(writer, 200, {"ok": True, "service": "bridge-internal"})
|
||||
else:
|
||||
await _send_response(writer, 404, {"error": "not found"})
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("[bridge http] Timeout beim Request-Lesen")
|
||||
except Exception as exc:
|
||||
logger.exception("[bridge http] Fehler: %s", exc)
|
||||
try:
|
||||
await _send_response(writer, 500, {"error": str(exc)[:200]})
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
server = await asyncio.start_server(handle, host, port)
|
||||
logger.info("[bridge] Internal HTTP-Listener auf %s:%d (Brain-Push)", host, port)
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
except Exception:
|
||||
logger.exception("[bridge] Internal HTTP-Listener konnte nicht starten")
|
||||
|
||||
async def _handle_trigger_fired(self, reply: str, trigger_name: str,
|
||||
ttype: str, events: list) -> None:
|
||||
"""Spiegelt eine Brain-Trigger-Antwort wie eine normale ARIA-Antwort.
|
||||
|
||||
Side-Channel-Events zuerst (trigger_created, location_tracking, ...),
|
||||
dann _process_core_response (Chat-Bubble, TTS, chat_backup).
|
||||
"""
|
||||
# Side-Channel-Events erst (gleich wie in send_to_core)
|
||||
for event in events or []:
|
||||
etype = event.get("type")
|
||||
try:
|
||||
if etype == "skill_created":
|
||||
await self._send_to_rvs({
|
||||
"type": "skill_created",
|
||||
"payload": event.get("skill", {}),
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
elif etype == "trigger_created":
|
||||
await self._send_to_rvs({
|
||||
"type": "trigger_created",
|
||||
"payload": event.get("trigger", {}),
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
elif etype == "location_tracking":
|
||||
await self._send_to_rvs({
|
||||
"type": "location_tracking",
|
||||
"payload": {
|
||||
"on": bool(event.get("on")),
|
||||
"reason": event.get("reason") or "",
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
except Exception:
|
||||
logger.exception("[trigger-fire] Side-Channel-Event %s fehlgeschlagen", etype)
|
||||
|
||||
if not reply:
|
||||
logger.info("[trigger-fire] Trigger '%s' hat leeren Reply — nichts zu broadcasten",
|
||||
trigger_name)
|
||||
return
|
||||
|
||||
# Reply wie eine normale ARIA-Antwort behandeln
|
||||
try:
|
||||
await self._process_core_response(
|
||||
reply,
|
||||
{"metadata": {"trigger_name": trigger_name, "trigger_type": ttype}},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("[trigger-fire] _process_core_response fehlgeschlagen")
|
||||
|
||||
# ── Run & Shutdown ───────────────────────────────────────
|
||||
|
||||
async def run(self) -> None:
|
||||
@@ -2405,6 +2544,8 @@ class ARIABridge:
|
||||
# connect_to_core entfaellt — Bridge ruft jetzt aria-brain ueber
|
||||
# HTTP (siehe send_to_core). Keine persistente WS-Verbindung mehr.
|
||||
asyncio.create_task(self.connect_to_rvs()),
|
||||
# Interner HTTP-Listener — empfaengt Trigger-Feuer-Pushes vom Brain.
|
||||
asyncio.create_task(self._serve_internal_http()),
|
||||
]
|
||||
|
||||
if self.audio_available:
|
||||
|
||||
@@ -55,6 +55,8 @@ Wichtige Mechanismen:
|
||||
|
||||
### Bugs / Fixes
|
||||
|
||||
- [x] **Trigger-Antworten landen jetzt im Chat** (App + Diagnostic + TTS): Wenn der Brain-Background-Loop einen Timer/Watcher feuert, ruft er `agent.chat()` direkt im eigenen Prozess. Die Antwort wurde nur ins Trigger-Log geschrieben — kein RVS-Broadcast, nichts sichtbar. Fix: Bridge hat jetzt einen kleinen asyncio HTTP-Listener auf Port 8090 (intern, nicht exposed). Brain pusht nach jedem Trigger-Feuer per `urllib.request.urlopen` an `http://aria-bridge:8090/internal/trigger-fired` mit `{reply, trigger_name, type, events}`. Bridge ruft `_handle_trigger_fired` → Side-Channel-Events (skill_created/trigger_created/location_tracking) + `_process_core_response` — exakt derselbe Pfad wie normale Chat-Antworten (Bubble + TTS + chat_backup)
|
||||
- [x] **Tool-Use im Proxy durchgereicht** (claude-max-api-proxy): Der Proxy nahm das OpenAI-`tools`-Feld an, ignorierte es aber komplett — `openai-to-cli.js` wandelte nur `messages` zu einem String, `manager.js` rief `claude --print` ohne Tools. Claude Code nutzte ihre internen Tools (Bash, Read, ...) und „simulierte" Aktionen wie `sleep 120` statt `trigger_timer` zu rufen. Fix: zwei eigene Adapter-Files unter `proxy-patches/`, die zur Container-Startzeit ueber die npm-Version kopiert werden. `openai-to-cli.js` injiziert die `tools` als `<system>`-Block mit Schema-Beschreibungen und der Anweisung `<tool_call name="X">{json}</tool_call>` als Antwortformat zu verwenden; weiterhin verarbeitet sie `role=tool`-Messages als `<tool_result>`-Bloecke fuer den Loop-Replay. `cli-to-openai.js` parsed die `<tool_call>`-Bloecke aus dem Result-Text zurueck zu OpenAI `tool_calls` mit `finish_reason=tool_calls`. Mehrere Tool-Calls + Pre-Tool-Text werden korrekt aufgeteilt
|
||||
- [x] **Timer "in 2 Minuten" wird wieder angelegt**: ARIA hatte keine Moeglichkeit die aktuelle Zeit zu kennen — kein Bash-Tool, kein Time-Tool, kein Timestamp im System-Prompt. Die Tool-Beschreibung von `trigger_timer` empfahl sogar `date -u -d '+10 minutes'` via Bash, aber Bash gab's nicht. Folge: LLM liess den Tool-Call entweder weg oder riet einen Cutoff-Zeitstempel (Vergangenheit) → Background-Loop feuerte beim naechsten 30s-Tick sofort statt in 2min. Fix: (1) `build_time_section()` in `prompts.py` injiziert UTC + lokale Europa/Berlin-Zeit als `## Aktuelle Zeit`-Block oben im System-Prompt. (2) `trigger_timer` akzeptiert jetzt `in_seconds` als Alternative zu `fires_at` — Server rechnet den absoluten Timestamp, ARIA muss nicht ISO-rechnen
|
||||
- [x] **"ARIA denkt..." haengt nach Brain-Antwort** (App + Diagnostic): `send_to_core` schickte `thinking` direkt via `_send_to_rvs`, hat aber `_last_activity_state` nicht gepflegt — der spaetere `_emit_activity("idle")` wurde dedupliziert und verschluckt. Fix: durchgehend `_emit_activity` fuer beide Zustaende
|
||||
- [x] **Such-Scroll in App-Chat springt jetzt zur Treffer-Bubble**: `scrollToIndex` wurde zu frueh gerufen + `viewPosition: 0.4` schoss vorbei. Fix: `requestAnimationFrame` + `viewPosition: 0.5` + `onScrollToIndexFailed`-Fallback mit averageItemLength-Schaetzung + 250ms-Retry
|
||||
@@ -301,6 +303,5 @@ Skills mit Tool-Use.
|
||||
- [ ] RVS Zombie-Connections endgueltig loesen
|
||||
- [ ] Gamebox: kleine Web-Oberflaeche fuer Credentials/Server-Config oder zentral aus Diagnostic per RVS push
|
||||
- [ ] Erste Skills bauen lassen (yt-dlp, pdf-extract, image-resize, etc.) — durch normale Anfragen, ARIA legt sie selbst an
|
||||
- [ ] Tool-Use-Verifikation: Live-Test ob claude-max-api-proxy `tools` und `tool_calls` sauber durchreicht
|
||||
- [ ] Heartbeat (periodische Selbst-Checks)
|
||||
- [ ] Lokales LLM als Waechter (Triage vor Claude-Call)
|
||||
|
||||
Reference in New Issue
Block a user