Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1796520b8 | |||
| 0ff44d99c4 | |||
| 8c74b3fed8 | |||
| c3fefc60c0 | |||
| 7107ce4fdd | |||
| fa47068d6d | |||
| 07c761fc72 |
@@ -195,11 +195,12 @@ Bestehendes Token nochmal als QR anzeigen: `./generate-token.sh show`
|
|||||||
http://<VM-IP>:3001
|
http://<VM-IP>:3001
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Diagnostic-UI hat fünf Top-Tabs:
|
Die Diagnostic-UI hat sechs Top-Tabs:
|
||||||
|
|
||||||
- **Main** — Live-Chat-Test, Status (Brain / RVS / Proxy), End-to-End-Trace
|
- **Main** — Live-Chat-Test, Status (Brain / RVS / Proxy), End-to-End-Trace
|
||||||
- **Gehirn** — Memory-Verwaltung (Vector-DB), Token/Call-Metrics (Subscription-Quota), Bootstrap & Migration, Komplett-Gehirn Export/Import
|
- **Gehirn** — Memory-Verwaltung (Vector-DB), Token/Call-Metrics (Subscription-Quota), Bootstrap & Migration, Komplett-Gehirn Export/Import
|
||||||
- **Skills** — Liste mit Logs, Run, Activate/Deactivate, Export/Import als tar.gz
|
- **Skills** — Liste mit Logs, Run, Activate/Deactivate, Export/Import als tar.gz
|
||||||
|
- **Trigger** — Timer + Watcher anlegen/anzeigen/loeschen, Live-Variablen-Anzeige (disk_free, current_lat, hour_of_day, …), near(lat, lon, m) als Condition-Funktion
|
||||||
- **Dateien** — alle Dateien aus `/shared/uploads/` mit Multi-Select, Bulk-Download (ZIP) + Bulk-Delete
|
- **Dateien** — alle Dateien aus `/shared/uploads/` mit Multi-Select, Bulk-Download (ZIP) + Bulk-Delete
|
||||||
- **Einstellungen** — Reparatur (Container-Restart), Wipe, Sprachausgabe, Whisper, Sprachmodell, Runtime-Config, App-Onboarding (QR), Komplett-Reset
|
- **Einstellungen** — Reparatur (Container-Restart), Wipe, Sprachausgabe, Whisper, Sprachmodell, Runtime-Config, App-Onboarding (QR), Komplett-Reset
|
||||||
|
|
||||||
@@ -314,6 +315,7 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
|
|||||||
- **Main**: Brain/RVS/Proxy-Status, Chat-Test, "ARIA denkt..."-Indikator, End-to-End-Trace, Container-Logs
|
- **Main**: Brain/RVS/Proxy-Status, Chat-Test, "ARIA denkt..."-Indikator, End-to-End-Trace, Container-Logs
|
||||||
- **Gehirn**: Memory-Browser (Vector-DB), Suche + Filter, Edit/Add/Delete, Konversation-Status mit Destillat-Trigger, **Token/Call-Metrics mit Subscription-Quota-Tracking**, Bootstrap & Migration (3 Wiederherstellungs-Wege), Gehirn-Export/Import (tar.gz). Info-Buttons (ℹ) ueberall mit Modal-Erklaerung.
|
- **Gehirn**: Memory-Browser (Vector-DB), Suche + Filter, Edit/Add/Delete, Konversation-Status mit Destillat-Trigger, **Token/Call-Metrics mit Subscription-Quota-Tracking**, Bootstrap & Migration (3 Wiederherstellungs-Wege), Gehirn-Export/Import (tar.gz). Info-Buttons (ℹ) ueberall mit Modal-Erklaerung.
|
||||||
- **Skills**: Liste aller Skills mit Logs pro Run, Activate/Deactivate, Export/Import als tar.gz, "von ARIA"-Badge fuer selbst gebaute
|
- **Skills**: Liste aller Skills mit Logs pro Run, Activate/Deactivate, Export/Import als tar.gz, "von ARIA"-Badge fuer selbst gebaute
|
||||||
|
- **Trigger**: passive Aufweck-Quellen. **Timer** (einmalig, ISO-Timestamp oder via `in_seconds` als Server-Berechnung) + **Watcher** (recurring, mit Condition + Throttle). Liste aktiver Trigger + Logs pro Feuer-Event. Modal mit Type-Dropdown, Live-Anzeige aller verfuegbaren Condition-Variablen (`disk_free_gb`, `hour_of_day`, `current_lat/lon`, `last_user_message_ago_sec`, …) und Condition-Funktionen (`near(lat, lon, m)` fuer GPS-Geofencing). Sicherer Condition-Parser via Python `ast` (Whitelist, kein `eval`). Der System-Prompt enthaelt zusaetzlich einen `## Aktuelle Zeit`-Block (UTC + Europa/Berlin) damit ARIA Timer-Zeitpunkte korrekt setzen kann.
|
||||||
- **Dateien**: Browser fuer `/shared/uploads/` mit Multi-Select + "Alle markieren" + Bulk-Download (ZIP bei 2+) + Bulk-Delete. Live-Update der Chat-Bubbles beim Delete.
|
- **Dateien**: Browser fuer `/shared/uploads/` mit Multi-Select + "Alle markieren" + Bulk-Download (ZIP bei 2+) + Bulk-Delete. Live-Update der Chat-Bubbles beim Delete.
|
||||||
- **Einstellungen**: Reparatur (Container-Restart fuer Brain/Bridge/Qdrant), Komplett-Reset, Betriebsmodi, Sprachausgabe + Voice-Cloning + F5-TTS-Tuning + Voice Export/Import, Whisper, Sprachmodell (brainModel), Onboarding-QR, App-Cleanup
|
- **Einstellungen**: Reparatur (Container-Restart fuer Brain/Bridge/Qdrant), Komplett-Reset, Betriebsmodi, Sprachausgabe + Voice-Cloning + F5-TTS-Tuning + Voice Export/Import, Whisper, Sprachmodell (brainModel), Onboarding-QR, App-Cleanup
|
||||||
|
|
||||||
@@ -356,6 +358,7 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
|
|||||||
- **Einstellungen**: TTS-aktiv, F5-TTS-Voice, Pre-Roll-Buffer, Stille-Toleranz, Speicherort, Auto-Download, GPS, Verbose-Logging
|
- **Einstellungen**: TTS-aktiv, F5-TTS-Voice, Pre-Roll-Buffer, Stille-Toleranz, Speicherort, Auto-Download, GPS, Verbose-Logging
|
||||||
- **Auto-Update**: Prueft beim Start + per Button auf neue Version, Download + Installation ueber RVS (FileProvider)
|
- **Auto-Update**: Prueft beim Start + per Button auf neue Version, Download + Installation ueber RVS (FileProvider)
|
||||||
- GPS-Position (optional, mit Runtime-Permission-Request) — wird in jeden Chat/Audio-Payload mitgegeben und ist in Diagnostic als Debug-Block einblendbar
|
- GPS-Position (optional, mit Runtime-Permission-Request) — wird in jeden Chat/Audio-Payload mitgegeben und ist in Diagnostic als Debug-Block einblendbar
|
||||||
|
- **GPS-Tracking (kontinuierlich)**: Toggle in Settings → Standort. Wenn aktiv, pushed die App alle ~15s bzw. ab 30m Bewegung ein `location_update` an die Bridge — Voraussetzung damit Watcher mit `near(lat, lon, m)` (z.B. Blitzer-Warner, Ankunft-Erinnerungen) ueberhaupt feuern koennen. ARIA selbst kann das Tracking via `request_location_tracking`-Tool an-/ausschalten und tut das automatisch wenn sie einen GPS-Watcher anlegt
|
||||||
- QR-Code Scanner fuer Token-Pairing
|
- QR-Code Scanner fuer Token-Pairing
|
||||||
- **ARIA-Dateien empfangen**: Wenn ARIA eine PDF/Bild/Markdown/ZIP fuer dich erstellt (Marker `[FILE: /shared/uploads/aria_*]` in der Antwort), erscheint sie als eigene Anhang-Bubble. Tippen → wird via RVS geladen + mit Android-Intent-Picker geoeffnet (PDF-Viewer, Bildbetrachter, Standard-App). Inline-Bilder aus Markdown-``-Syntax werden direkt unter dem Text gerendert (PNG/JPG via Image, SVG via react-native-svg)
|
- **ARIA-Dateien empfangen**: Wenn ARIA eine PDF/Bild/Markdown/ZIP fuer dich erstellt (Marker `[FILE: /shared/uploads/aria_*]` in der Antwort), erscheint sie als eigene Anhang-Bubble. Tippen → wird via RVS geladen + mit Android-Intent-Picker geoeffnet (PDF-Viewer, Bildbetrachter, Standard-App). Inline-Bilder aus Markdown-``-Syntax werden direkt unter dem Text gerendert (PNG/JPG via Image, SVG via react-native-svg)
|
||||||
- **Vollbild mit Pinch-Zoom**: Bilder im Vollbild-Modal sind pinch-zoombar (1x..5x), 1-Finger-Pan wenn gezoomt, Doppel-Tap toggelt 1x↔2.5x — alles ohne externe Lib
|
- **Vollbild mit Pinch-Zoom**: Bilder im Vollbild-Modal sind pinch-zoombar (1x..5x), 1-Finger-Pan wenn gezoomt, Doppel-Tap toggelt 1x↔2.5x — alles ohne externe Lib
|
||||||
@@ -859,6 +862,7 @@ 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 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 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 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] Sprachmodell-Setting wieder funktional (brainModel in runtime.json statt aria-core)
|
- [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-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
|
- [x] App: Chat-Suche mit Next/Prev Navigation statt Filter
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 10205
|
versionCode 10207
|
||||||
versionName "0.1.2.5"
|
versionName "0.1.2.7"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.2.5",
|
"version": "0.1.2.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
TTS_SPEED_STORAGE_KEY,
|
TTS_SPEED_STORAGE_KEY,
|
||||||
} from '../services/audio';
|
} from '../services/audio';
|
||||||
import audioService from '../services/audio';
|
import audioService from '../services/audio';
|
||||||
|
import gpsTrackingService from '../services/gpsTracking';
|
||||||
import { isVerboseLogging, setVerboseLogging } from '../services/logger';
|
import { isVerboseLogging, setVerboseLogging } from '../services/logger';
|
||||||
import {
|
import {
|
||||||
isWakeReadySoundEnabled,
|
isWakeReadySoundEnabled,
|
||||||
@@ -121,6 +122,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [manualPort, setManualPort] = useState('8765');
|
const [manualPort, setManualPort] = useState('8765');
|
||||||
const [currentMode, setCurrentMode] = useState('normal');
|
const [currentMode, setCurrentMode] = useState('normal');
|
||||||
const [gpsEnabled, setGpsEnabled] = useState(false);
|
const [gpsEnabled, setGpsEnabled] = useState(false);
|
||||||
|
const [gpsTracking, setGpsTracking] = useState(gpsTrackingService.isActive());
|
||||||
const [scannerVisible, setScannerVisible] = useState(false);
|
const [scannerVisible, setScannerVisible] = useState(false);
|
||||||
const [logTab, setLogTab] = useState<LogTab>('live');
|
const [logTab, setLogTab] = useState<LogTab>('live');
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
@@ -188,6 +190,11 @@ const SettingsScreen: React.FC = () => {
|
|||||||
AsyncStorage.getItem('aria_gps_enabled').then(saved => {
|
AsyncStorage.getItem('aria_gps_enabled').then(saved => {
|
||||||
if (saved !== null) setGpsEnabled(saved === 'true');
|
if (saved !== null) setGpsEnabled(saved === 'true');
|
||||||
});
|
});
|
||||||
|
// gpsTrackingService status syncen + auf Aenderungen lauschen
|
||||||
|
setGpsTracking(gpsTrackingService.isActive());
|
||||||
|
const offGps = gpsTrackingService.onChange(setGpsTracking);
|
||||||
|
// Persistierten Status wiederherstellen (war Tracking beim letzten Mal an?)
|
||||||
|
gpsTrackingService.restoreFromStorage().catch(() => {});
|
||||||
AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => {
|
AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => {
|
||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
const n = parseFloat(saved);
|
const n = parseFloat(saved);
|
||||||
@@ -245,6 +252,10 @@ const SettingsScreen: React.FC = () => {
|
|||||||
});
|
});
|
||||||
// Voice-Liste vom XTTS-Server holen (via RVS)
|
// Voice-Liste vom XTTS-Server holen (via RVS)
|
||||||
rvs.send('xtts_list_voices' as any, {});
|
rvs.send('xtts_list_voices' as any, {});
|
||||||
|
return () => {
|
||||||
|
// gpsTrackingService-Listener abmelden (Variable offGps oben definiert)
|
||||||
|
try { offGps(); } catch {}
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Speichergroesse berechnen
|
// Speichergroesse berechnen
|
||||||
@@ -407,6 +418,18 @@ const SettingsScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ARIA bittet um GPS-Tracking An/Aus (Tool request_location_tracking)
|
||||||
|
if (message.type === ('location_tracking' as any)) {
|
||||||
|
const p: any = message.payload || {};
|
||||||
|
const on = !!p.on;
|
||||||
|
const reason = (p.reason as string) || 'ARIA';
|
||||||
|
if (on) {
|
||||||
|
gpsTrackingService.start(reason).catch(() => {});
|
||||||
|
} else {
|
||||||
|
gpsTrackingService.stop(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Datei-Manager: ZIP-Response (Multi-Download)
|
// Datei-Manager: ZIP-Response (Multi-Download)
|
||||||
if (message.type === ('file_zip_response' as any)) {
|
if (message.type === ('file_zip_response' as any)) {
|
||||||
const p: any = message.payload || {};
|
const p: any = message.payload || {};
|
||||||
@@ -1004,6 +1027,29 @@ const SettingsScreen: React.FC = () => {
|
|||||||
thumbColor={gpsEnabled ? '#FFFFFF' : '#666680'}
|
thumbColor={gpsEnabled ? '#FFFFFF' : '#666680'}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* GPS-Tracking (kontinuierlich) — fuer near()-Watcher */}
|
||||||
|
<View style={[styles.toggleRow, {marginTop: 12, borderTopWidth: 1, borderTopColor: '#1E1E2E', paddingTop: 12}]}>
|
||||||
|
<View style={styles.toggleInfo}>
|
||||||
|
<Text style={styles.toggleLabel}>GPS-Tracking (kontinuierlich)</Text>
|
||||||
|
<Text style={styles.toggleHint}>
|
||||||
|
Sendet alle ~15s deine Position an ARIA (wenn du dich {'>'}30m bewegt
|
||||||
|
hast). Nur noetig fuer GPS-basierte Trigger wie Blitzer-Warner
|
||||||
|
(near()-Conditions). ARIA kann das auch selbst an-/abschalten wenn
|
||||||
|
sie einen GPS-Watcher anlegt. Akku-Verbrauch erhoeht — bei langer
|
||||||
|
Fahrt einplanen.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={gpsTracking}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v) gpsTrackingService.start('manuell').catch(() => {});
|
||||||
|
else gpsTrackingService.stop('manuell');
|
||||||
|
}}
|
||||||
|
trackColor={{ false: '#2A2A3E', true: '#FF9500' }}
|
||||||
|
thumbColor={gpsTracking ? '#FFFFFF' : '#666680'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</>)}
|
</>)}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* GPS-Tracking-Service.
|
||||||
|
*
|
||||||
|
* Wenn aktiv: pushed alle paar Sekunden die aktuelle Position als
|
||||||
|
* `location_update {lat, lon}` an den RVS-Server, damit Brain-Watcher
|
||||||
|
* mit `near()`-Conditions etwas zum Vergleichen haben.
|
||||||
|
*
|
||||||
|
* Default: AUS. Wird entweder vom User manuell in Settings angeschaltet
|
||||||
|
* oder von ARIA via location_tracking-RVS-Message (Brain-Tool
|
||||||
|
* `request_location_tracking`).
|
||||||
|
*
|
||||||
|
* Energie-Schutz: distanceFilter 30m, interval 15s. Echte Fahrt-Updates
|
||||||
|
* (Geschwindigkeit) kommen sauber durch, stationaer wird kaum gesendet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { PermissionsAndroid, Platform, ToastAndroid } from 'react-native';
|
||||||
|
import Geolocation from '@react-native-community/geolocation';
|
||||||
|
import rvs from './rvs';
|
||||||
|
|
||||||
|
type Listener = (active: boolean) => void;
|
||||||
|
|
||||||
|
class GpsTrackingService {
|
||||||
|
private watchId: number | null = null;
|
||||||
|
private active = false;
|
||||||
|
private listeners: Set<Listener> = new Set();
|
||||||
|
// Defensive: nicht zu schnell oeffentlich togglen
|
||||||
|
private lastChangeAt = 0;
|
||||||
|
|
||||||
|
isActive(): boolean {
|
||||||
|
return this.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(cb: Listener): () => void {
|
||||||
|
this.listeners.add(cb);
|
||||||
|
return () => { this.listeners.delete(cb); };
|
||||||
|
}
|
||||||
|
|
||||||
|
private notify() {
|
||||||
|
for (const cb of this.listeners) {
|
||||||
|
try { cb(this.active); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Beim App-Start: gespeicherten Zustand wiederherstellen (Default off). */
|
||||||
|
async restoreFromStorage(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const v = await AsyncStorage.getItem('aria_gps_tracking');
|
||||||
|
if (v === 'true') {
|
||||||
|
console.log('[gps-track] Restore: war an, starte wieder');
|
||||||
|
this.start('Beim Start wiederhergestellt');
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensurePermission(): Promise<boolean> {
|
||||||
|
if (Platform.OS !== 'android') return true;
|
||||||
|
try {
|
||||||
|
const granted = await PermissionsAndroid.request(
|
||||||
|
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
||||||
|
{
|
||||||
|
title: 'GPS-Tracking',
|
||||||
|
message: 'ARIA braucht laufende Standort-Updates damit GPS-Watcher (Blitzer-Warner, near()) funktionieren.',
|
||||||
|
buttonPositive: 'Erlauben',
|
||||||
|
buttonNegative: 'Abbrechen',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[gps-track] Permission-Fehler:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(reason: string = ''): Promise<boolean> {
|
||||||
|
if (this.active) return true;
|
||||||
|
const ok = await this.ensurePermission();
|
||||||
|
if (!ok) {
|
||||||
|
ToastAndroid.show('GPS-Tracking: Berechtigung abgelehnt', ToastAndroid.LONG);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.watchId = Geolocation.watchPosition(
|
||||||
|
(pos) => {
|
||||||
|
const lat = pos.coords.latitude;
|
||||||
|
const lon = pos.coords.longitude;
|
||||||
|
rvs.send('location_update' as any, { lat, lon });
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.warn('[gps-track] watchPosition error:', err?.code, err?.message);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
distanceFilter: 30, // erst senden wenn 30m gewandert
|
||||||
|
interval: 15000, // (Android) gewuenschte Frequenz
|
||||||
|
fastestInterval: 10000, // (Android) max Frequenz
|
||||||
|
} as any,
|
||||||
|
);
|
||||||
|
this.active = true;
|
||||||
|
this.lastChangeAt = Date.now();
|
||||||
|
this.notify();
|
||||||
|
AsyncStorage.setItem('aria_gps_tracking', 'true').catch(() => {});
|
||||||
|
ToastAndroid.show(
|
||||||
|
reason ? `GPS-Tracking aktiv (${reason})` : 'GPS-Tracking aktiv',
|
||||||
|
ToastAndroid.SHORT,
|
||||||
|
);
|
||||||
|
console.log('[gps-track] gestartet', reason ? `(${reason})` : '');
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn('[gps-track] start fehlgeschlagen:', e?.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(reason: string = ''): void {
|
||||||
|
if (!this.active) return;
|
||||||
|
if (this.watchId !== null) {
|
||||||
|
try { Geolocation.clearWatch(this.watchId); } catch {}
|
||||||
|
this.watchId = null;
|
||||||
|
}
|
||||||
|
this.active = false;
|
||||||
|
this.lastChangeAt = Date.now();
|
||||||
|
this.notify();
|
||||||
|
AsyncStorage.setItem('aria_gps_tracking', 'false').catch(() => {});
|
||||||
|
ToastAndroid.show(
|
||||||
|
reason ? `GPS-Tracking aus (${reason})` : 'GPS-Tracking aus',
|
||||||
|
ToastAndroid.SHORT,
|
||||||
|
);
|
||||||
|
console.log('[gps-track] gestoppt', reason ? `(${reason})` : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggle(reason: string = ''): Promise<void> {
|
||||||
|
if (this.active) this.stop(reason);
|
||||||
|
else await this.start(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new GpsTrackingService();
|
||||||
+64
-10
@@ -97,26 +97,35 @@ META_TOOLS = [
|
|||||||
"function": {
|
"function": {
|
||||||
"name": "trigger_timer",
|
"name": "trigger_timer",
|
||||||
"description": (
|
"description": (
|
||||||
"Lege einen Timer-Trigger an — feuert EINMALIG zum angegebenen Zeitpunkt "
|
"Lege einen Timer-Trigger an — feuert EINMALIG und ruft dich dann selbst auf "
|
||||||
"und ruft dich selbst auf (Push-Nachricht an Stefan). "
|
"(Push-Nachricht an Stefan). Use-Case: 'erinnere mich in 10min', "
|
||||||
"Use-Case: 'erinnere mich in 10min', 'sag mir um 14:30 Bescheid'."
|
"'sag mir um 14:30 Bescheid'. Genau EINES von `in_seconds` ODER `fires_at` "
|
||||||
|
"muss gesetzt sein."
|
||||||
),
|
),
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {"type": "string", "description": "kurzer kebab-case-Name, a-z 0-9 - _"},
|
"name": {"type": "string", "description": "kurzer kebab-case-Name, a-z 0-9 - _"},
|
||||||
|
"in_seconds": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": (
|
||||||
|
"Relativ ab jetzt in Sekunden. Bevorzugt bei Angaben wie "
|
||||||
|
"'in 2 Minuten' (=120), 'in 1 Stunde' (=3600). "
|
||||||
|
"Server berechnet daraus den absoluten Feuer-Zeitpunkt."
|
||||||
|
),
|
||||||
|
},
|
||||||
"fires_at": {
|
"fires_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": (
|
"description": (
|
||||||
"Absoluter ISO-Timestamp UTC, z.B. '2026-05-12T14:30:00Z'. "
|
"Absoluter ISO-Timestamp UTC fuer feste Termine, z.B. "
|
||||||
"Berechne aus relativer Angabe ('in 10min') selbst — die "
|
"'2026-05-12T14:30:00Z'. Die aktuelle Zeit findest du im "
|
||||||
"aktuelle Zeit findest du im System-Prompt nicht, also nutze "
|
"System-Prompt unter '## Aktuelle Zeit'. Fuer relative Angaben "
|
||||||
"Bash: `date -u -d '+10 minutes' --iso-8601=seconds`."
|
"lieber `in_seconds` nutzen."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"message": {"type": "string", "description": "Was soll bei der Erinnerung gesagt werden"},
|
"message": {"type": "string", "description": "Was soll bei der Erinnerung gesagt werden"},
|
||||||
},
|
},
|
||||||
"required": ["name", "fires_at", "message"],
|
"required": ["name", "message"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -176,6 +185,27 @@ META_TOOLS = [
|
|||||||
"parameters": {"type": "object", "properties": {}},
|
"parameters": {"type": "object", "properties": {}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "request_location_tracking",
|
||||||
|
"description": (
|
||||||
|
"Bittet die App, das kontinuierliche GPS-Tracking zu aktivieren oder zu "
|
||||||
|
"deaktivieren. Default ist AUS (Akku-Schutz). Nutze das wenn du einen "
|
||||||
|
"GPS-basierten Watcher anlegst (z.B. `near(...)`), sonst hat die App "
|
||||||
|
"veraltete Position und der Watcher feuert nie. Auch wieder ausschalten "
|
||||||
|
"wenn der letzte GPS-Watcher geloescht wurde."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"on": {"type": "boolean", "description": "true = Tracking an, false = aus"},
|
||||||
|
"reason": {"type": "string", "description": "Kurzer Grund (wird in App-Notification angezeigt)"},
|
||||||
|
},
|
||||||
|
"required": ["on"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -264,11 +294,13 @@ class Agent:
|
|||||||
# Trigger-Liste + Variablen-Info fuer den System-Prompt
|
# Trigger-Liste + Variablen-Info fuer den System-Prompt
|
||||||
all_triggers = triggers_mod.list_triggers(active_only=False)
|
all_triggers = triggers_mod.list_triggers(active_only=False)
|
||||||
condition_vars = watcher_mod.describe_variables()
|
condition_vars = watcher_mod.describe_variables()
|
||||||
|
condition_funcs = watcher_mod.describe_functions()
|
||||||
|
|
||||||
# 5. System-Prompt + Window-Messages
|
# 5. System-Prompt + Window-Messages
|
||||||
system_prompt = build_system_prompt(hot, cold, skills=all_skills,
|
system_prompt = build_system_prompt(hot, cold, skills=all_skills,
|
||||||
triggers=all_triggers,
|
triggers=all_triggers,
|
||||||
condition_vars=condition_vars)
|
condition_vars=condition_vars,
|
||||||
|
condition_funcs=condition_funcs)
|
||||||
messages = [ProxyMessage(role="system", content=system_prompt)]
|
messages = [ProxyMessage(role="system", content=system_prompt)]
|
||||||
for t in self.conversation.window():
|
for t in self.conversation.window():
|
||||||
messages.append(ProxyMessage(role=t.role, content=t.content))
|
messages.append(ProxyMessage(role=t.role, content=t.content))
|
||||||
@@ -366,9 +398,22 @@ class Agent:
|
|||||||
out += f"\nstderr:\n{err}"
|
out += f"\nstderr:\n{err}"
|
||||||
return out
|
return out
|
||||||
if name == "trigger_timer":
|
if name == "trigger_timer":
|
||||||
|
fires_at_iso = arguments.get("fires_at")
|
||||||
|
in_seconds = arguments.get("in_seconds")
|
||||||
|
if not fires_at_iso and in_seconds is not None:
|
||||||
|
from datetime import datetime as _dt, timezone as _tz, timedelta as _td
|
||||||
|
try:
|
||||||
|
secs = int(in_seconds)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "FEHLER: in_seconds muss eine ganze Zahl sein."
|
||||||
|
if secs < 1:
|
||||||
|
return "FEHLER: in_seconds muss >= 1 sein."
|
||||||
|
fires_at_iso = (_dt.now(_tz.utc) + _td(seconds=secs)).isoformat(timespec="seconds")
|
||||||
|
if not fires_at_iso:
|
||||||
|
return "FEHLER: entweder `in_seconds` ODER `fires_at` muss gesetzt sein."
|
||||||
t = triggers_mod.create_timer(
|
t = triggers_mod.create_timer(
|
||||||
name=arguments["name"],
|
name=arguments["name"],
|
||||||
fires_at_iso=arguments["fires_at"],
|
fires_at_iso=fires_at_iso,
|
||||||
message=arguments["message"],
|
message=arguments["message"],
|
||||||
author="aria",
|
author="aria",
|
||||||
)
|
)
|
||||||
@@ -399,6 +444,15 @@ class Agent:
|
|||||||
return f"OK — Trigger '{arguments['name']}' geloescht."
|
return f"OK — Trigger '{arguments['name']}' geloescht."
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return f"FEHLER: {e}"
|
return f"FEHLER: {e}"
|
||||||
|
if name == "request_location_tracking":
|
||||||
|
on = bool(arguments.get("on", False))
|
||||||
|
reason = (arguments.get("reason") or "").strip()
|
||||||
|
self._pending_events.append({
|
||||||
|
"type": "location_tracking",
|
||||||
|
"on": on,
|
||||||
|
"reason": reason,
|
||||||
|
})
|
||||||
|
return f"OK — Tracking-Request gesendet (on={on}). App wird in Kuerze umschalten."
|
||||||
if name == "trigger_list":
|
if name == "trigger_list":
|
||||||
items = triggers_mod.list_triggers(active_only=False)
|
items = triggers_mod.list_triggers(active_only=False)
|
||||||
if not items:
|
if not items:
|
||||||
|
|||||||
+7
-2
@@ -470,10 +470,15 @@ def triggers_list(active_only: bool = False):
|
|||||||
|
|
||||||
@app.get("/triggers/conditions")
|
@app.get("/triggers/conditions")
|
||||||
def triggers_conditions():
|
def triggers_conditions():
|
||||||
"""Verfuegbare Variablen fuer Watcher-Conditions (mit aktuellen Werten)."""
|
"""Verfuegbare Variablen + Funktionen fuer Watcher-Conditions
|
||||||
|
(mit aktuellen Werten)."""
|
||||||
|
current = watcher_mod.collect_variables()
|
||||||
|
# near() ist ein callable in vars_ — fuer die UI rausfiltern
|
||||||
|
serializable = {k: v for k, v in current.items() if not callable(v)}
|
||||||
return {
|
return {
|
||||||
"variables": watcher_mod.describe_variables(),
|
"variables": watcher_mod.describe_variables(),
|
||||||
"current": watcher_mod.collect_variables(),
|
"functions": watcher_mod.describe_functions(),
|
||||||
|
"current": serializable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+53
-7
@@ -15,10 +15,34 @@ mit dem Conversation-Loop in spaeteren Phasen.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from memory import MemoryPoint
|
from memory import MemoryPoint
|
||||||
|
|
||||||
|
|
||||||
|
def build_time_section() -> str:
|
||||||
|
"""Aktueller Zeitstempel — damit ARIA Timer korrekt anlegen kann
|
||||||
|
und Watcher-Conditions mit hour_of_day etc. einordenbar bleiben."""
|
||||||
|
now_utc = datetime.now(timezone.utc)
|
||||||
|
# Europa/Berlin: Sommerzeit CEST = UTC+2, Winterzeit CET = UTC+1.
|
||||||
|
# Wir nehmen den simplen Fall (kein zoneinfo-Import noetig im Brain-Image):
|
||||||
|
# Stefans VM laeuft auf UTC, die Bridge in der Wohnung — Anzeige reicht.
|
||||||
|
local_offset_h = 2 if 3 <= now_utc.month <= 10 else 1
|
||||||
|
local = now_utc + timedelta(hours=local_offset_h)
|
||||||
|
lines = [
|
||||||
|
"## Aktuelle Zeit",
|
||||||
|
f"- UTC: {now_utc.isoformat(timespec='seconds')}",
|
||||||
|
f"- Lokal (Europa/Berlin, UTC+{local_offset_h}): "
|
||||||
|
f"{local.strftime('%Y-%m-%d %H:%M:%S')} ({local.strftime('%A')})",
|
||||||
|
"",
|
||||||
|
"Nutze das fuer Trigger-Timestamps und um Watcher-Conditions wie "
|
||||||
|
"`hour_of_day == 8` einzuordnen. Fuer relative Angaben "
|
||||||
|
"('in 10min', 'in 2 Stunden') nutze beim `trigger_timer` den "
|
||||||
|
"`in_seconds`-Parameter — Server rechnet dann selbst.",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
TYPE_HEADINGS = {
|
TYPE_HEADINGS = {
|
||||||
"identity": "## Wer du bist",
|
"identity": "## Wer du bist",
|
||||||
"rule": "## Sicherheitsregeln & Prinzipien",
|
"rule": "## Sicherheitsregeln & Prinzipien",
|
||||||
@@ -115,8 +139,12 @@ def build_skills_section(skills: List[dict]) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def build_triggers_section(triggers: List[dict], condition_vars: List[dict]) -> str:
|
def build_triggers_section(
|
||||||
"""Triggers (passive Aufweck-Quellen) + verfuegbare Condition-Variablen."""
|
triggers: List[dict],
|
||||||
|
condition_vars: List[dict],
|
||||||
|
condition_funcs: List[dict] | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Triggers (passive Aufweck-Quellen) + verfuegbare Condition-Variablen + Funktionen."""
|
||||||
lines = ["## Trigger (passive Aufweck-Quellen)"]
|
lines = ["## Trigger (passive Aufweck-Quellen)"]
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Trigger sind ANDERS als Skills: das System ruft DICH wenn ein Event passiert. "
|
lines.append("Trigger sind ANDERS als Skills: das System ruft DICH wenn ein Event passiert. "
|
||||||
@@ -135,15 +163,32 @@ def build_triggers_section(triggers: List[dict], condition_vars: List[dict]) ->
|
|||||||
lines.append("### Verfuegbare Condition-Variablen (fuer Watcher)")
|
lines.append("### Verfuegbare Condition-Variablen (fuer Watcher)")
|
||||||
for v in condition_vars:
|
for v in condition_vars:
|
||||||
lines.append(f"- `{v['name']}` ({v['type']}) — {v['desc']}")
|
lines.append(f"- `{v['name']}` ({v['type']}) — {v['desc']}")
|
||||||
|
if condition_funcs:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("### Verfuegbare Funktionen in Conditions")
|
||||||
|
for fn in condition_funcs:
|
||||||
|
lines.append(f"- `{fn['signature']}` — {fn['desc']}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Operatoren in Conditions: `<` `>` `<=` `>=` `==` `!=` `and` `or` `not`. "
|
lines.append("Operatoren in Conditions: `<` `>` `<=` `>=` `==` `!=` `and` `or` `not`. "
|
||||||
"Beispiel: `disk_free_gb < 5 and hour_of_day >= 8`. "
|
"Beispiele: `disk_free_gb < 5 and hour_of_day >= 8`, "
|
||||||
"String-Werte in Quotes: `day_of_week == \"mon\"`.")
|
"`day_of_week == \"mon\"`, `near(53.123, 7.456, 500)`. "
|
||||||
|
"Funktionen nur mit Konstanten als Argumenten (keine Variablen, "
|
||||||
|
"keine geschachtelten Funktionen).")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("### Wann welcher Typ?")
|
lines.append("### Wann welcher Typ?")
|
||||||
lines.append("- **Timer** fuer einmalige Erinnerungen mit konkreter Zeit ('in 10min', 'um 14:30').")
|
lines.append("- **Timer** fuer einmalige Erinnerungen mit konkreter Zeit ('in 10min', 'um 14:30').")
|
||||||
lines.append("- **Watcher** fuer 'wenn X passiert' (Disk voll, bestimmte Tageszeit).")
|
lines.append("- **Watcher** fuer 'wenn X passiert' (Disk voll, bestimmte Tageszeit, GPS-Naehe).")
|
||||||
lines.append("- ARIA legt Trigger NUR auf Stefan-Wunsch an, nicht eigenmaechtig.")
|
lines.append("- ARIA legt Trigger NUR auf Stefan-Wunsch an, nicht eigenmaechtig.")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("### GPS-Watcher mit near()")
|
||||||
|
lines.append(
|
||||||
|
"Wenn du einen Watcher mit `near()` anlegst: die App sendet GPS-Position "
|
||||||
|
"nur kontinuierlich wenn Tracking AN ist (Default: AUS, Akku-Schutz). "
|
||||||
|
"Rufe dafuer `request_location_tracking(on=true, reason=\"...\")` auf "
|
||||||
|
"bevor oder gleich nach dem trigger_watcher. Sonst hat current_lat/lon "
|
||||||
|
"veraltete Werte und der Watcher feuert nie. "
|
||||||
|
"Beim Loeschen des letzten GPS-Watchers (trigger_cancel) wieder "
|
||||||
|
"`request_location_tracking(on=false)` aufrufen.")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -153,15 +198,16 @@ def build_system_prompt(
|
|||||||
skills: List[dict] | None = None,
|
skills: List[dict] | None = None,
|
||||||
triggers: List[dict] | None = None,
|
triggers: List[dict] | None = None,
|
||||||
condition_vars: List[dict] | None = None,
|
condition_vars: List[dict] | None = None,
|
||||||
|
condition_funcs: List[dict] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Kompletter System-Prompt: Hot + Cold + Skills + Triggers."""
|
"""Kompletter System-Prompt: Hot + Cold + Skills + Triggers."""
|
||||||
parts = [build_hot_memory_section(pinned)]
|
parts = [build_hot_memory_section(pinned), "", build_time_section()]
|
||||||
if skills:
|
if skills:
|
||||||
parts.append("")
|
parts.append("")
|
||||||
parts.append(build_skills_section(skills))
|
parts.append(build_skills_section(skills))
|
||||||
if condition_vars:
|
if condition_vars:
|
||||||
parts.append("")
|
parts.append("")
|
||||||
parts.append(build_triggers_section(triggers or [], condition_vars))
|
parts.append(build_triggers_section(triggers or [], condition_vars, condition_funcs))
|
||||||
if cold:
|
if cold:
|
||||||
parts.append("")
|
parts.append("")
|
||||||
parts.append(build_cold_memory_section(cold))
|
parts.append(build_cold_memory_section(cold))
|
||||||
|
|||||||
+183
-23
@@ -1,22 +1,25 @@
|
|||||||
"""
|
"""
|
||||||
Built-in Condition-Variablen + sicherer Mini-Parser fuer Watcher-Triggers.
|
Built-in Condition-Variablen + sicherer Mini-Parser fuer Watcher-Triggers.
|
||||||
|
|
||||||
Erlaubte Variablen kommen aus diesem Modul. Condition-Ausdruck ist ein
|
Erlaubte Variablen + die EINE Funktion `near(lat, lon, radius_m)` kommen
|
||||||
sicheres Subset von Python (kein eval, kein exec): nur Vergleiche und
|
aus diesem Modul. Condition-Ausdruck ist ein sicheres Subset von Python
|
||||||
Boolean-Operatoren, nur die hier deklarierten Variablen, nur Zahlen +
|
(kein eval, kein exec): nur Vergleiche, Boolean-Operatoren, Whitelisted
|
||||||
String-Literale als rechte Seite.
|
Funktionen, Variablen aus describe_variables(), Konstanten (Zahl/Bool/Str).
|
||||||
|
|
||||||
Beispiele:
|
Beispiele:
|
||||||
disk_free_gb < 5
|
disk_free_gb < 5
|
||||||
hour_of_day == 8 and day_of_week == "mon"
|
hour_of_day == 8 and day_of_week == "mon"
|
||||||
rvs_connected == False
|
is_weekend and minute_of_hour == 0
|
||||||
(disk_free_pct < 10 and uptime_sec > 3600)
|
near(53.123, 7.456, 500)
|
||||||
|
current_lat and location_age_sec < 120
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
@@ -26,6 +29,20 @@ from typing import Any
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STATE_DIR = Path("/shared/state")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── State-Helfer (gemeinsam mit Bridge: /shared/state/*.json) ──────
|
||||||
|
|
||||||
|
def _read_state(name: str) -> dict | None:
|
||||||
|
f = STATE_DIR / f"{name}.json"
|
||||||
|
if not f.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return json.loads(f.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ─── Variablen-Quellen ──────────────────────────────────────────────
|
# ─── Variablen-Quellen ──────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -50,55 +67,184 @@ def _uptime_sec() -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _rvs_connected() -> bool:
|
def _ram_free_mb() -> int:
|
||||||
"""Liest /shared/config/runtime.json oder ein Bridge-State-File.
|
"""Container-RAM: MemAvailable aus /proc/meminfo (kB → MB)."""
|
||||||
Aktuell: wir koennen das nicht zuverlaessig aus dem Brain-Container
|
try:
|
||||||
bestimmen — gibt False als sicheren Default zurueck.
|
with open("/proc/meminfo", "r") as f:
|
||||||
Spaeter: Bridge schreibt einen Heartbeat-File den wir hier lesen."""
|
for line in f:
|
||||||
return False
|
if line.startswith("MemAvailable:"):
|
||||||
|
return int(line.split()[1]) // 1024
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _cpu_load_1min() -> float:
|
||||||
|
"""load avg ueber 1 Minute (linux). Vorsicht: das ist die HOST-load,
|
||||||
|
nicht container-spezifisch."""
|
||||||
|
try:
|
||||||
|
with open("/proc/loadavg", "r") as f:
|
||||||
|
return float(f.read().split()[0])
|
||||||
|
except Exception:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||||
|
|
||||||
|
|
||||||
|
def _gps_state() -> dict[str, Any]:
|
||||||
|
"""Letzte bekannte Position aus /shared/state/location.json.
|
||||||
|
Returns dict mit current_lat, current_lon (oder None), location_age_sec."""
|
||||||
|
data = _read_state("location") or {}
|
||||||
|
now = int(time.time())
|
||||||
|
age = -1
|
||||||
|
lat = data.get("lat")
|
||||||
|
lon = data.get("lon")
|
||||||
|
ts = data.get("ts_unix")
|
||||||
|
if isinstance(ts, (int, float)):
|
||||||
|
age = int(now - ts)
|
||||||
|
return {
|
||||||
|
"current_lat": float(lat) if isinstance(lat, (int, float)) else None,
|
||||||
|
"current_lon": float(lon) if isinstance(lon, (int, float)) else None,
|
||||||
|
"location_age_sec": age,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _user_activity_age() -> int:
|
||||||
|
"""Sekunden seit letzter User-Aktion (Chat oder Voice). -1 wenn nie."""
|
||||||
|
data = _read_state("activity") or {}
|
||||||
|
ts = data.get("last_user_ts")
|
||||||
|
if not isinstance(ts, (int, float)):
|
||||||
|
return -1
|
||||||
|
return int(time.time() - ts)
|
||||||
|
|
||||||
|
|
||||||
def collect_variables() -> dict[str, Any]:
|
def collect_variables() -> dict[str, Any]:
|
||||||
"""Liefert aktuellen Snapshot aller Built-in-Variablen."""
|
"""Liefert aktuellen Snapshot aller Built-in-Variablen + near()-Helper."""
|
||||||
free_gb, free_pct = _disk_stats()
|
free_gb, free_pct = _disk_stats()
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
# Memory-Count aus der Vector-DB (importiert lazy um zirkulaere Imports
|
gps = _gps_state()
|
||||||
# zu vermeiden — beim Modul-Load gibt's noch keinen Store)
|
|
||||||
|
# Memory-Counts aus der Vector-DB (lazy import, sonst zirkulaer)
|
||||||
memory_count = 0
|
memory_count = 0
|
||||||
|
pinned_count = 0
|
||||||
try:
|
try:
|
||||||
from main import store # type: ignore
|
from main import store # type: ignore
|
||||||
s = store()
|
s = store()
|
||||||
memory_count = s.count()
|
memory_count = s.count()
|
||||||
|
try:
|
||||||
|
pinned_count = len(s.list_pinned())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return {
|
vars_: dict[str, Any] = {
|
||||||
|
# Disk + System
|
||||||
"disk_free_gb": round(free_gb, 2),
|
"disk_free_gb": round(free_gb, 2),
|
||||||
"disk_free_pct": round(free_pct, 1),
|
"disk_free_pct": round(free_pct, 1),
|
||||||
|
"ram_free_mb": _ram_free_mb(),
|
||||||
|
"cpu_load_1min": round(_cpu_load_1min(), 2),
|
||||||
"uptime_sec": _uptime_sec(),
|
"uptime_sec": _uptime_sec(),
|
||||||
|
|
||||||
|
# Zeit
|
||||||
"hour_of_day": now.hour,
|
"hour_of_day": now.hour,
|
||||||
|
"minute_of_hour": now.minute,
|
||||||
|
"day_of_month": now.day,
|
||||||
|
"month": now.month,
|
||||||
|
"year": now.year,
|
||||||
"day_of_week": _DAYS[now.weekday()],
|
"day_of_week": _DAYS[now.weekday()],
|
||||||
"rvs_connected": _rvs_connected(),
|
"is_weekend": now.weekday() >= 5,
|
||||||
|
"unix_timestamp": int(time.time()),
|
||||||
|
|
||||||
|
# GPS
|
||||||
|
"current_lat": gps["current_lat"],
|
||||||
|
"current_lon": gps["current_lon"],
|
||||||
|
"location_age_sec": gps["location_age_sec"],
|
||||||
|
|
||||||
|
# Activity
|
||||||
|
"last_user_message_ago_sec": _user_activity_age(),
|
||||||
|
|
||||||
|
# Memory
|
||||||
"memory_count": memory_count,
|
"memory_count": memory_count,
|
||||||
|
"pinned_count": pinned_count,
|
||||||
|
|
||||||
|
# rvs_connected: kann Brain noch nicht zuverlaessig feststellen
|
||||||
|
# (Bridge muesste eigenen Heartbeat-State schreiben — kommt spaeter)
|
||||||
|
"rvs_connected": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Funktion-Helper — wird vom Parser als ast.Call mit Name "near" erkannt.
|
||||||
|
# Closure ueber die GPS-Werte, damit eval keine extra Variablen braucht.
|
||||||
|
def _near(lat: float, lon: float, radius_m: float) -> bool:
|
||||||
|
"""Haversine-Distanz: True wenn aktuelle Position < radius_m vom Punkt."""
|
||||||
|
cur_lat = vars_.get("current_lat")
|
||||||
|
cur_lon = vars_.get("current_lon")
|
||||||
|
if cur_lat is None or cur_lon is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
R = 6371000.0
|
||||||
|
phi1 = math.radians(float(cur_lat))
|
||||||
|
phi2 = math.radians(float(lat))
|
||||||
|
dphi = math.radians(float(lat) - float(cur_lat))
|
||||||
|
dlam = math.radians(float(lon) - float(cur_lon))
|
||||||
|
a = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlam / 2) ** 2
|
||||||
|
distance = 2 * R * math.asin(math.sqrt(a))
|
||||||
|
return distance < float(radius_m)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
vars_["near"] = _near
|
||||||
|
return vars_
|
||||||
|
|
||||||
|
|
||||||
def describe_variables() -> list[dict]:
|
def describe_variables() -> list[dict]:
|
||||||
"""Liste der verfuegbaren Variablen + Beschreibung — fuer System-Prompt + UI."""
|
"""Beschreibung — fuer System-Prompt + UI."""
|
||||||
return [
|
return [
|
||||||
|
# Disk / System
|
||||||
{"name": "disk_free_gb", "type": "number", "desc": "freier Plattenplatz in GB (auf /shared)"},
|
{"name": "disk_free_gb", "type": "number", "desc": "freier Plattenplatz in GB (auf /shared)"},
|
||||||
{"name": "disk_free_pct", "type": "number", "desc": "freier Plattenplatz in Prozent"},
|
{"name": "disk_free_pct", "type": "number", "desc": "freier Plattenplatz in Prozent"},
|
||||||
|
{"name": "ram_free_mb", "type": "number", "desc": "freier RAM im Brain-Container (MB)"},
|
||||||
|
{"name": "cpu_load_1min", "type": "number", "desc": "Load-Avg 1min (Host)"},
|
||||||
{"name": "uptime_sec", "type": "number", "desc": "Sekunden seit Brain-Start"},
|
{"name": "uptime_sec", "type": "number", "desc": "Sekunden seit Brain-Start"},
|
||||||
|
# Zeit
|
||||||
{"name": "hour_of_day", "type": "number", "desc": "0..23, lokale Zeit"},
|
{"name": "hour_of_day", "type": "number", "desc": "0..23, lokale Zeit"},
|
||||||
|
{"name": "minute_of_hour", "type": "number", "desc": "0..59"},
|
||||||
|
{"name": "day_of_month", "type": "number", "desc": "1..31"},
|
||||||
|
{"name": "month", "type": "number", "desc": "1..12"},
|
||||||
|
{"name": "year", "type": "number", "desc": "z.B. 2026"},
|
||||||
{"name": "day_of_week", "type": "string", "desc": "mon|tue|wed|thu|fri|sat|sun"},
|
{"name": "day_of_week", "type": "string", "desc": "mon|tue|wed|thu|fri|sat|sun"},
|
||||||
{"name": "rvs_connected", "type": "bool", "desc": "True wenn RVS-Verbindung steht"},
|
{"name": "is_weekend", "type": "bool", "desc": "True wenn Samstag oder Sonntag"},
|
||||||
{"name": "memory_count", "type": "number", "desc": "Anzahl Memories in der Vector-DB"},
|
{"name": "unix_timestamp", "type": "number", "desc": "Sekunden seit Epoche (UTC)"},
|
||||||
|
# GPS
|
||||||
|
{"name": "current_lat", "type": "number", "desc": "letzte bekannte Breitengrad (oder None)"},
|
||||||
|
{"name": "current_lon", "type": "number", "desc": "letzte bekannte Laengengrad (oder None)"},
|
||||||
|
{"name": "location_age_sec", "type": "number", "desc": "Sekunden seit letzter Position (-1 = nie)"},
|
||||||
|
# Activity
|
||||||
|
{"name": "last_user_message_ago_sec", "type": "number",
|
||||||
|
"desc": "Sekunden seit letztem User-Input (-1 = nie)"},
|
||||||
|
# Memory
|
||||||
|
{"name": "memory_count", "type": "number", "desc": "Anzahl Memories total"},
|
||||||
|
{"name": "pinned_count", "type": "number", "desc": "Anzahl pinned (Hot Memory)"},
|
||||||
|
{"name": "rvs_connected", "type": "bool", "desc": "RVS-Verbindung (z.Zt. immer False)"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def describe_functions() -> list[dict]:
|
||||||
|
"""Whitelisted Funktionen fuer Conditions."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "near",
|
||||||
|
"signature": "near(lat, lon, radius_m)",
|
||||||
|
"desc": "True wenn die aktuelle GPS-Position innerhalb von radius_m Metern "
|
||||||
|
"vom Punkt (lat, lon) liegt. Haversine. Bei unbekannter Position: False.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
_ALLOWED_FUNCTIONS = {f["name"] for f in describe_functions()}
|
||||||
|
|
||||||
|
|
||||||
# ─── Sicherer Condition-Parser ──────────────────────────────────────
|
# ─── Sicherer Condition-Parser ──────────────────────────────────────
|
||||||
|
|
||||||
_ALLOWED_NODES = (
|
_ALLOWED_NODES = (
|
||||||
@@ -106,6 +252,7 @@ _ALLOWED_NODES = (
|
|||||||
ast.Name, ast.Constant, ast.Load,
|
ast.Name, ast.Constant, ast.Load,
|
||||||
ast.And, ast.Or, ast.Not,
|
ast.And, ast.Or, ast.Not,
|
||||||
ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE,
|
ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE,
|
||||||
|
ast.Call,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -121,13 +268,26 @@ def parse_condition(expr: str) -> ast.Expression:
|
|||||||
tree = ast.parse(expr, mode="eval")
|
tree = ast.parse(expr, mode="eval")
|
||||||
except SyntaxError as e:
|
except SyntaxError as e:
|
||||||
raise ValueError(f"Condition Syntax-Fehler: {e}")
|
raise ValueError(f"Condition Syntax-Fehler: {e}")
|
||||||
# Whitelist-Walk
|
|
||||||
allowed_names = {v["name"] for v in describe_variables()}
|
allowed_names = {v["name"] for v in describe_variables()}
|
||||||
for node in ast.walk(tree):
|
for node in ast.walk(tree):
|
||||||
if not isinstance(node, _ALLOWED_NODES):
|
if not isinstance(node, _ALLOWED_NODES):
|
||||||
raise ValueError(f"Verbotener Ausdruck: {type(node).__name__}")
|
raise ValueError(f"Verbotener Ausdruck: {type(node).__name__}")
|
||||||
|
if isinstance(node, ast.Call):
|
||||||
|
# Nur direkter Funktionsname, kein attribute-access (foo.bar())
|
||||||
|
if not isinstance(node.func, ast.Name):
|
||||||
|
raise ValueError("Funktionsaufruf nur ueber direkten Namen erlaubt")
|
||||||
|
if node.func.id not in _ALLOWED_FUNCTIONS:
|
||||||
|
raise ValueError(f"Verbotene Funktion: {node.func.id}")
|
||||||
|
# Args muessen Constants oder einzelne Names sein
|
||||||
|
for a in node.args:
|
||||||
|
if not isinstance(a, (ast.Constant, ast.Name, ast.UnaryOp)):
|
||||||
|
raise ValueError(f"Argument-Typ in {node.func.id}() nicht erlaubt")
|
||||||
|
if node.keywords:
|
||||||
|
raise ValueError("Keyword-Argumente in Funktionen nicht erlaubt")
|
||||||
if isinstance(node, ast.Name):
|
if isinstance(node, ast.Name):
|
||||||
if node.id not in allowed_names and node.id not in ("True", "False"):
|
if (node.id not in allowed_names
|
||||||
|
and node.id not in _ALLOWED_FUNCTIONS
|
||||||
|
and node.id not in ("True", "False")):
|
||||||
raise ValueError(f"Unbekannte Variable: {node.id}")
|
raise ValueError(f"Unbekannte Variable: {node.id}")
|
||||||
if isinstance(node, ast.Constant):
|
if isinstance(node, ast.Constant):
|
||||||
if not isinstance(node.value, (int, float, str, bool)) and node.value is not None:
|
if not isinstance(node.value, (int, float, str, bool)) and node.value is not None:
|
||||||
@@ -141,7 +301,7 @@ def evaluate(expr: str, variables: dict[str, Any] | None = None) -> bool:
|
|||||||
tree = parse_condition(expr)
|
tree = parse_condition(expr)
|
||||||
vars_ = variables if variables is not None else collect_variables()
|
vars_ = variables if variables is not None else collect_variables()
|
||||||
code = compile(tree, "<condition>", "eval")
|
code = compile(tree, "<condition>", "eval")
|
||||||
# Globals leer, locals nur die erlaubten Variablen → kein Builtin-Zugriff
|
# Globals leer, locals enthalten Variablen + near()-Funktion → kein Builtin-Zugriff
|
||||||
try:
|
try:
|
||||||
result = eval(code, {"__builtins__": {}}, vars_)
|
result = eval(code, {"__builtins__": {}}, vars_)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import ssl
|
import ssl
|
||||||
|
import time
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
@@ -919,6 +920,44 @@ class ARIABridge:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("[rvs] file_from_aria broadcast fehlgeschlagen: %s", e)
|
logger.warning("[rvs] file_from_aria broadcast fehlgeschlagen: %s", e)
|
||||||
|
|
||||||
|
def _persist_state(self, key: str, data: dict) -> None:
|
||||||
|
"""Atomic-Write in /shared/state/<key>.json — fuer Brain-Watcher.
|
||||||
|
Wird genutzt fuer location + activity-Tracking."""
|
||||||
|
try:
|
||||||
|
import time as _time
|
||||||
|
data = dict(data)
|
||||||
|
data["ts_unix"] = int(_time.time())
|
||||||
|
Path("/shared/state").mkdir(parents=True, exist_ok=True)
|
||||||
|
target = Path(f"/shared/state/{key}.json")
|
||||||
|
tmp = target.with_suffix(".tmp")
|
||||||
|
tmp.write_text(json.dumps(data), encoding="utf-8")
|
||||||
|
tmp.replace(target)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("[state] %s schreiben fehlgeschlagen: %s", key, e)
|
||||||
|
|
||||||
|
def _persist_location(self, location: Optional[dict]) -> None:
|
||||||
|
"""Speichert die letzte bekannte GPS-Position fuer Watcher.
|
||||||
|
Erwartet {lat, lon} oder {lat, lng}. Nicht-Dicts und fehlende
|
||||||
|
Koordinaten werden ignoriert."""
|
||||||
|
if not isinstance(location, dict):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
lat = location.get("lat")
|
||||||
|
lon = location.get("lon") or location.get("lng")
|
||||||
|
if lat is None or lon is None:
|
||||||
|
return
|
||||||
|
self._persist_state("location", {
|
||||||
|
"lat": float(lat),
|
||||||
|
"lon": float(lon),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _persist_user_activity(self) -> None:
|
||||||
|
"""Markiert dass der User gerade etwas gemacht hat (Chat/Voice).
|
||||||
|
Watcher: last_user_message_ago_sec basiert darauf."""
|
||||||
|
self._persist_state("activity", {"last_user_ts": int(time.time())})
|
||||||
|
|
||||||
def _append_chat_backup(self, entry: dict) -> None:
|
def _append_chat_backup(self, entry: dict) -> None:
|
||||||
"""Schreibt eine Zeile in /shared/config/chat_backup.jsonl.
|
"""Schreibt eine Zeile in /shared/config/chat_backup.jsonl.
|
||||||
Wird von Diagnostic + App als History-Quelle gelesen.
|
Wird von Diagnostic + App als History-Quelle gelesen.
|
||||||
@@ -1319,6 +1358,18 @@ class ARIABridge:
|
|||||||
})
|
})
|
||||||
logger.info("[brain] ARIA hat einen Trigger angelegt: %s",
|
logger.info("[brain] ARIA hat einen Trigger angelegt: %s",
|
||||||
event.get("trigger", {}).get("name"))
|
event.get("trigger", {}).get("name"))
|
||||||
|
elif etype == "location_tracking":
|
||||||
|
# ARIA bittet die App das GPS-Tracking ein-/auszuschalten
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
logger.info("[brain] location_tracking Request: on=%s (%s)",
|
||||||
|
event.get("on"), event.get("reason", ""))
|
||||||
|
|
||||||
# _process_core_response uebernimmt alles weitere:
|
# _process_core_response uebernimmt alles weitere:
|
||||||
# File-Marker extrahieren + broadcasten, NO_REPLY-Check, Chat-
|
# File-Marker extrahieren + broadcasten, NO_REPLY-Check, Chat-
|
||||||
@@ -1479,6 +1530,9 @@ class ARIABridge:
|
|||||||
if text:
|
if text:
|
||||||
interrupted = bool(payload.get("interrupted", False))
|
interrupted = bool(payload.get("interrupted", False))
|
||||||
location = payload.get("location") or None
|
location = payload.get("location") or None
|
||||||
|
# State persist fuer Brain-Watcher (current_lat, ..., last_user_ts)
|
||||||
|
self._persist_location(location)
|
||||||
|
self._persist_user_activity()
|
||||||
# Wenn Files gerade gepuffert sind (Bild + Text gleichzeitig
|
# Wenn Files gerade gepuffert sind (Bild + Text gleichzeitig
|
||||||
# gesendet), mergen wir sie zu einer einzigen Anfrage statt
|
# gesendet), mergen wir sie zu einer einzigen Anfrage statt
|
||||||
# zwei separater send_to_core-Calls.
|
# zwei separater send_to_core-Calls.
|
||||||
@@ -1872,6 +1926,17 @@ class ARIABridge:
|
|||||||
logger.warning("[rvs] file_delete_request: %s", e)
|
logger.warning("[rvs] file_delete_request: %s", e)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
elif msg_type == "location_update":
|
||||||
|
# Live-GPS-Update von der App (nicht an Chat gekoppelt). Wird in
|
||||||
|
# /shared/state/location.json geschrieben, damit Watcher-Trigger
|
||||||
|
# near()-Conditions auswerten koennen.
|
||||||
|
lat = payload.get("lat")
|
||||||
|
lon = payload.get("lon") or payload.get("lng")
|
||||||
|
if lat is not None and lon is not None:
|
||||||
|
self._persist_location({"lat": lat, "lon": lon})
|
||||||
|
logger.debug("[gps] location_update: %.5f, %.5f", float(lat), float(lon))
|
||||||
|
return
|
||||||
|
|
||||||
elif msg_type == "container_restart":
|
elif msg_type == "container_restart":
|
||||||
# App-Button "Container neu" — leitet generisch an Diagnostic
|
# App-Button "Container neu" — leitet generisch an Diagnostic
|
||||||
# weiter. Whitelist ist im Diagnostic-Server.
|
# weiter. Whitelist ist im Diagnostic-Server.
|
||||||
@@ -1961,6 +2026,9 @@ class ARIABridge:
|
|||||||
interrupted = bool(payload.get("interrupted", False))
|
interrupted = bool(payload.get("interrupted", False))
|
||||||
audio_request_id = payload.get("audioRequestId", "") or ""
|
audio_request_id = payload.get("audioRequestId", "") or ""
|
||||||
location = payload.get("location") or None
|
location = payload.get("location") or None
|
||||||
|
# State persist fuer Brain-Watcher (current_lat etc.)
|
||||||
|
self._persist_location(location)
|
||||||
|
self._persist_user_activity()
|
||||||
logger.info("[rvs] Audio empfangen: %s, %dms, %dKB%s%s%s",
|
logger.info("[rvs] Audio empfangen: %s, %dms, %dKB%s%s%s",
|
||||||
mime_type, duration_ms, len(audio_b64) // 1365,
|
mime_type, duration_ms, len(audio_b64) // 1365,
|
||||||
" [BARGE-IN]" if interrupted else "",
|
" [BARGE-IN]" if interrupted else "",
|
||||||
|
|||||||
@@ -2886,15 +2886,21 @@
|
|||||||
document.getElementById('trigger-message').value = '';
|
document.getElementById('trigger-message').value = '';
|
||||||
document.getElementById('trigger-modal-error').style.display = 'none';
|
document.getElementById('trigger-modal-error').style.display = 'none';
|
||||||
onTriggerTypeChange();
|
onTriggerTypeChange();
|
||||||
// Variablen-Hinweis laden
|
// Variablen + Funktionen-Hinweis laden
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/brain/triggers/conditions');
|
const r = await fetch('/api/brain/triggers/conditions');
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
const info = document.getElementById('trigger-vars-info');
|
const info = document.getElementById('trigger-vars-info');
|
||||||
if (info) {
|
if (info) {
|
||||||
info.innerHTML = '<strong>Variablen:</strong> ' + (d.variables || []).map(v =>
|
const vars = (d.variables || []).map(v =>
|
||||||
`<code>${escapeHtml(v.name)}</code>=${escapeHtml(String(d.current[v.name]))} <span style="color:#444;">(${escapeHtml(v.desc)})</span>`
|
`<code>${escapeHtml(v.name)}</code>=${escapeHtml(String(d.current[v.name]))} <span style="color:#444;">(${escapeHtml(v.desc)})</span>`
|
||||||
).join(' · ');
|
).join(' · ');
|
||||||
|
const fns = (d.functions || []).map(f =>
|
||||||
|
`<code>${escapeHtml(f.signature)}</code> — ${escapeHtml(f.desc)}`
|
||||||
|
).join('<br>');
|
||||||
|
info.innerHTML =
|
||||||
|
'<strong>Variablen:</strong> ' + vars +
|
||||||
|
(fns ? '<br><br><strong>Funktionen:</strong><br>' + fns : '');
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
document.getElementById('trigger-modal').classList.add('open');
|
document.getElementById('trigger-modal').classList.add('open');
|
||||||
|
|||||||
@@ -55,6 +55,15 @@ Wichtige Mechanismen:
|
|||||||
|
|
||||||
### Bugs / Fixes
|
### Bugs / Fixes
|
||||||
|
|
||||||
|
- [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
|
||||||
|
- [x] **STT-Bubble bekommt den Text jetzt sofort** (nicht erst mit ARIAs Antwort): `_process_app_audio` rief erst `send_to_core` (blockt synchron) und DANN STT-Broadcast. Fix: Reihenfolge getauscht — STT raus, dann Core-Call
|
||||||
|
- [x] **ARIA-Antworten landen wieder in der Diagnostic**: `if (sender === 'aria') return;` im `rvs_chat`-Handler war OpenClaw-Leiche und filterte die neuen Brain-Antworten weg. Fix: aria → received-Bubble
|
||||||
|
- [x] **Brain-Card im Main-Tab zeigt jetzt Live-Status**: `updateState` ueberschrieb die Card mit altem `state.gateway`-Text aus OpenClaw-Zeiten. Fix: `updateState` laesst Brain-Card unangetastet, `loadBrainStatus` synchronisiert beide Cards (Main + Gehirn-Tab) alle 15s
|
||||||
|
- [x] **App-Chat-Sync zeigte veralteten Stand**: `since:lastSync` war diff-only — wenn Server geleert war, blieb die App-History stehen. Fix: `since:0, limit:200` komplett-Replace (Server = Source of Truth). Lokal-only Bubbles (Skill-Notifications, laufende Voice ohne STT) bleiben erhalten
|
||||||
|
- [x] **Konversation-Reset leert jetzt beides**: vorher leerte der Button nur das Brain-Memory, `chat_backup.jsonl` blieb. Fix: ein Button feuert `Promise.all` auf `/api/brain/conversation/reset` + `/api/chat-history-clear`, plus `chat_cleared`-Broadcast via RVS damit App + Diagnostic sich live leeren
|
||||||
|
- [x] **JS-Crashes beim Diagnostic-Laden behoben**: Ghost-IDs aus OpenClaw-Zeiten (`gw-dot`, `openclaw-config`, `btn-core-term`, `core-auth`, `perms-status`, `rc-compact-after`) wurden null-referenziert. Fix: null-safe oder Code raus
|
||||||
- [x] Diagnostic: "ARIA denkt..." bleibt nicht mehr stehen
|
- [x] Diagnostic: "ARIA denkt..." bleibt nicht mehr stehen
|
||||||
- [x] App: "ARIA denkt..." Indicator + Abbrechen-Button (Bridge spiegelt agent_activity via RVS)
|
- [x] App: "ARIA denkt..." Indicator + Abbrechen-Button (Bridge spiegelt agent_activity via RVS)
|
||||||
- [x] Textnachrichten werden von ARIA beantwortet (Bridge chat handler fix)
|
- [x] Textnachrichten werden von ARIA beantwortet (Bridge chat handler fix)
|
||||||
@@ -250,6 +259,25 @@ Skills mit Tool-Use.
|
|||||||
- [x] Diagnostic Skills-Tab: Liste, README, Logs pro Run, Activate/Deactivate/Delete, Export/Import als tar.gz
|
- [x] Diagnostic Skills-Tab: Liste, README, Logs pro Run, Activate/Deactivate/Delete, Export/Import als tar.gz
|
||||||
- [x] skill_created Live-Notification: gelbe Bubble in App + Diagnostic sobald ARIA selbst einen Skill anlegt
|
- [x] skill_created Live-Notification: gelbe Bubble in App + Diagnostic sobald ARIA selbst einen Skill anlegt
|
||||||
|
|
||||||
|
### Triggers-System (Phase B Punkt 5)
|
||||||
|
|
||||||
|
- [x] **Filesystem-Layer** unter `/data/triggers/<name>.json` + `logs/<name>.jsonl` pro Trigger
|
||||||
|
- [x] **Timer** (one-shot, ISO-Timestamp) — "erinner mich in 10 Minuten an X" → ARIA legt via `trigger_timer`-Tool an, Background-Loop feuert zum Stichzeitpunkt einmal
|
||||||
|
- [x] **Watcher** (recurring) — feuert wenn `condition` true wird, mit Throttle (min_seconds_between_fires) gegen Spam. Checks alle 30s
|
||||||
|
- [x] **Sicherer Condition-Parser** via Python `ast`-Module (Whitelist statt `eval`): nur `<` `>` `<=` `>=` `==` `!=` `and` `or` `not`, Konstanten + Variablennamen aus Whitelist
|
||||||
|
- [x] **Built-in Variablen**: `disk_free_gb`, `disk_free_pct`, `ram_free_mb`, `cpu_load_1min`, `uptime_sec`, `hour_of_day`, `minute_of_hour`, `day_of_month`, `month`, `year`, `day_of_week`, `is_weekend`, `unix_timestamp`, `current_lat`, `current_lon`, `location_age_sec`, `last_user_message_ago_sec`, `memory_count`, `pinned_count`, `rvs_connected`
|
||||||
|
- [x] **near(lat, lon, radius_m) Funktion** im Parser (Haversine) — GPS-Geofencing fuer Blitzer-Warner / Ankunft-Erinnerungen
|
||||||
|
- [x] **Background-Loop** im Brain-Container (Lifespan async task): laeuft alle 30s, prueft alle aktiven Trigger, ruft bei Match `agent.chat(prompt, source="trigger")` mit System-Praefix → ARIA reagiert wie auf eine Frage von Stefan, kann TTS sprechen / Skills starten / weitere Trigger anlegen
|
||||||
|
- [x] **Diagnostic Trigger-Tab**: Liste aktiver Trigger mit Logs, Anlegen-Modal mit Type-Dropdown, Live-Anzeige aller verfuegbaren Variablen + Funktionen, Beispiele
|
||||||
|
- [x] **App Live-Notification**: `trigger_created`-Bubble (gelb) sobald ARIA selbst einen Trigger anlegt — User sieht sofort dass die Bitte angekommen ist
|
||||||
|
- [x] **GPS-Tracking via App** (`@react-native-community/geolocation` watchPosition, distanceFilter 30m, interval 15s) — Singleton-Service in `gpsTracking.ts`, Toggle in Settings → Standort, persistiert AsyncStorage, Restore beim App-Start
|
||||||
|
- [x] **`request_location_tracking`-Tool**: ARIA kann das Tracking via `location_tracking`-Event an-/ausschalten — Bridge forwarded an App, App startet/stoppt watchPosition. ARIA tut das automatisch wenn sie einen Watcher mit `near()` anlegt
|
||||||
|
- [x] **`location_update`-Forwarding**: App schickt alle 15s/30m ein `location_update {lat,lon}`, Bridge persistiert in `/shared/state/location.json`, Watcher liest beim Check
|
||||||
|
- [x] **Activity-Persistenz**: `/shared/state/activity.json` traegt User-Message-Zeitstempel, damit `last_user_message_ago_sec` als Variable verfuegbar ist
|
||||||
|
- [x] **`trigger_cancel`** + **`trigger_list`** als Tools — ARIA kann eigene Trigger verwalten
|
||||||
|
- [x] **Triggers-Block im System-Prompt**: aktive Trigger + verfuegbare Variablen + Funktionen werden bei jedem Chat-Turn injiziert, dazu Hinweis dass GPS-Watcher `request_location_tracking` mit-aufrufen sollen
|
||||||
|
- [x] **Aktuelle-Zeit-Block im System-Prompt**: UTC + lokale Europa/Berlin-Zeit (Sommer/Winter-Heuristik) wird bei jedem Chat-Turn oben mit-injiziert, damit Timer-fires_at und Watcher mit `hour_of_day` ueberhaupt sinnvoll sind. `trigger_timer` akzeptiert zusaetzlich `in_seconds` (Server rechnet) — ARIA muss bei relativen Angaben ('in 2 Minuten') nicht selbst ISO-rechnen
|
||||||
|
|
||||||
### Diagnostic / App Features (drumherum)
|
### Diagnostic / App Features (drumherum)
|
||||||
|
|
||||||
- [x] Datei-Manager (Diagnostic + App-Modal): /shared/uploads/ verwalten, Multi-Select + Select-All + Bulk-Download als ZIP + Bulk-Delete
|
- [x] Datei-Manager (Diagnostic + App-Modal): /shared/uploads/ verwalten, Multi-Select + Select-All + Bulk-Download als ZIP + Bulk-Delete
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const ALLOWED_TYPES = new Set([
|
|||||||
"xtts_import_voice", "xtts_voice_imported",
|
"xtts_import_voice", "xtts_voice_imported",
|
||||||
"skill_created",
|
"skill_created",
|
||||||
"trigger_created",
|
"trigger_created",
|
||||||
|
"location_update", "location_tracking",
|
||||||
"chat_history_request", "chat_history_response", "chat_cleared",
|
"chat_history_request", "chat_history_response", "chat_cleared",
|
||||||
"file_delete_batch_request", "file_delete_batch_response",
|
"file_delete_batch_request", "file_delete_batch_response",
|
||||||
"file_zip_request", "file_zip_response",
|
"file_zip_request", "file_zip_response",
|
||||||
|
|||||||
Reference in New Issue
Block a user