mirror of
https://github.com/Jeuners/agenttwo-tools.git
synced 2026-09-09 15:02:31 +02:00
- lädt öffentliche Websites und extrahiert Hauptinhalt als Markdown (defuddle/node + linkedom) - SSRF-Schutz: nur http/https, private Adressbereiche werden nach DNS-Auflösung abgewiesen, auch über Weiterleitungen (redirect: manual, jede Hop-URL neu geprüft) - Grenzen: 15 s Zeitlimit, 2 MB Fetch-Limit, 25 kB Output-Cap; Inhalt wird dem Modell als nicht vertrauenswürdig markiert - Text-/PDF-Anhänge: Message-Feld files, Validierung über server/src/files.ts, PDF-Textextraktion via pdf-parse - README: Werkzeugliste und Netz-Ausnahme dokumentiert
283 lines
11 KiB
Markdown
283 lines
11 KiB
Markdown
# agenttwo-tools
|
||
|
||
> Fork von [agenttwo](https://github.com/Jeuners/agenttwo). Die Basis bleibt dort
|
||
> unverändert; hier kommen Vision und Werkzeuge (Tool-Calling) dazu. Läuft auf eigenen Ports
|
||
> (Backend 8788, Frontend 5174), damit beide Projekte parallel laufen können.
|
||
> Fixes aus der Basis lassen sich per `git cherry-pick` aus dem Remote
|
||
> `upstream` übernehmen.
|
||
|
||
Voice-Chat-Oberfläche für lokale und Cloud-LLMs: lokales Qwen3 über
|
||
[Ollama](https://ollama.com), optionaler Fallback auf
|
||
[OpenRouter](https://openrouter.ai), Spracheingabe via
|
||
[whisper.cpp](https://github.com/ggml-org/whisper.cpp) und deutsche
|
||
Sprachausgabe via [Piper](https://github.com/rhasspy/piper) (Stimme: Thorsten).
|
||
|
||
## Aufbau
|
||
|
||
npm-Workspace mit zwei Paketen:
|
||
|
||
| Pfad | Inhalt |
|
||
|-----------|------------------------------------------------------------------------|
|
||
| `server/` | Fastify + WebSocket-Backend, Ollama-/OpenRouter-Bridge, STT/TTS, SQLite |
|
||
| `web/` | React 18 + Vite Frontend, Markdown-Rendering, Voice-Recording |
|
||
|
||
## Voraussetzungen
|
||
|
||
- **macOS** mit [Homebrew](https://brew.sh)
|
||
- **Node.js 22+** — `server/src/db.ts` nutzt das eingebaute `node:sqlite`,
|
||
das es unter Node 20 noch nicht gibt. Unter Node 22 erscheint beim Start
|
||
eine `ExperimentalWarning`; ab Node 24 ist das Modul stabil.
|
||
- [Ollama](https://ollama.com) mit einem Qwen3-Modell (`ollama pull qwen3.5`)
|
||
- `ffmpeg` im `PATH`
|
||
- `whisper-cli` im `PATH` (whisper.cpp) inklusive Modell
|
||
- Piper als **Python-Modul** — der Server ruft `python3 -m piper` auf,
|
||
nicht das gleichnamige Homebrew-Binary
|
||
|
||
**RAM-Hinweis (Apple Silicon):** Qwen3.5 belegt ~7 GB im Speicher — mit
|
||
weniger als 16 GB RAM wird es eng. Dann lieber das kleinere
|
||
[`qwen3:8b`](https://ollama.com/library/qwen3) (`ollama pull qwen3:8b`,
|
||
~5 GB) laden und in den Einstellungen unter „Lokales Modell“ wählen bzw.
|
||
`MODEL=qwen3:8b` in die `.env` schreiben.
|
||
|
||
## Schnellstart (Mac)
|
||
|
||
Ein Befehl von null bis zur lauffähigen App — das Skript prüft alle
|
||
Abhängigkeiten und bietet fehlende (Homebrew, Node, ffmpeg, whisper, Piper,
|
||
Ollama, Modell, Sprachdateien) zur Installation an:
|
||
|
||
```bash
|
||
git clone https://github.com/Jeuners/agenttwo-tools.git
|
||
cd agenttwo-tools
|
||
./setup.sh # oder: npm run setup
|
||
npm run dev # → http://localhost:5174
|
||
```
|
||
|
||
Nur prüfen ohne etwas zu ändern: `./setup.sh --check` · Ohne Nachfragen:
|
||
`./setup.sh --yes`
|
||
|
||
Unter macOS:
|
||
|
||
```bash
|
||
brew install ffmpeg whisper-cpp
|
||
pip3 install piper-tts
|
||
```
|
||
|
||
Prüfen, ob alles bereitsteht:
|
||
|
||
```bash
|
||
ffmpeg -version >/dev/null 2>&1 && echo "ffmpeg OK"
|
||
whisper-cli --help >/dev/null 2>&1 && echo "whisper-cli OK"
|
||
python3 -m piper --help >/dev/null 2>&1 && echo "piper OK"
|
||
node -e "require('node:sqlite')" 2>/dev/null && echo "node:sqlite OK"
|
||
```
|
||
|
||
### Whisper-Modell
|
||
|
||
Wird per Default unter `~/whisper-models/ggml-large-v3-turbo.bin` erwartet:
|
||
|
||
```bash
|
||
mkdir -p ~/whisper-models
|
||
curl -L -o ~/whisper-models/ggml-large-v3-turbo.bin \
|
||
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin
|
||
```
|
||
|
||
### Piper-Stimme
|
||
|
||
Die Stimmdatei ist **nicht** im Repo (109 MB, über GitHubs Dateilimit).
|
||
Einmalig herunterladen:
|
||
|
||
```bash
|
||
mkdir -p server/voices
|
||
curl -L -o server/voices/de_DE-thorsten-high.onnx \
|
||
https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE/thorsten/high/de_DE-thorsten-high.onnx
|
||
```
|
||
|
||
Die zugehörige `de_DE-thorsten-high.onnx.json` liegt bereits im Repo.
|
||
|
||
## Konfiguration
|
||
|
||
`.env` im Projekt-Root (wird vom Server eingelesen, ist gitignored):
|
||
|
||
```bash
|
||
OPENROUTER_API_KEY=sk-or-...
|
||
```
|
||
|
||
Weitere optionale Variablen mit ihren Defaults:
|
||
|
||
| Variable | Default |
|
||
|----------------------|--------------------------------------------------|
|
||
| `PORT` | `8788` |
|
||
| `OLLAMA_URL` | `http://localhost:11434` |
|
||
| `MODEL` | `qwen3.5:latest` |
|
||
| `WHISPER_MODEL` | `~/whisper-models/ggml-large-v3-turbo.bin` |
|
||
| `WHISPER_LANG` | `de` |
|
||
| `PIPER_MODEL` | `server/voices/de_DE-thorsten-high.onnx` |
|
||
| `ALLOWED_ORIGINS` | (leer — siehe Sicherheit) |
|
||
|
||
## Bilder (Vision)
|
||
|
||
`qwen3.5` bringt die Fähigkeit `vision` mit, deshalb versteht der Chat Bilder.
|
||
Anhängen geht auf drei Wegen: Button 🖼 im Composer, Einfügen aus der
|
||
Zwischenablage (⌘V) oder Drag & Drop auf die Eingabezeile. Eine Nachricht darf
|
||
auch nur aus einem Bild bestehen.
|
||
|
||
Bilder werden zusammen mit der Nachricht in der SQLite-Datei abgelegt und bei
|
||
Folgefragen erneut mitgeschickt, sodass Rückfragen zum selben Bild funktionieren.
|
||
Für Ollama gehen sie als `images: [base64]` raus, für OpenRouter im
|
||
OpenAI-Format als `image_url` mit data-URL — der MIME-Typ wird dabei aus den
|
||
Magic Bytes bestimmt.
|
||
|
||
Grenzen (`server/src/images.ts`): maximal 4 Bilder pro Nachricht, je 6 MB,
|
||
nur PNG, JPEG, GIF und WebP. Der Typ wird an den Magic Bytes geprüft, nicht am
|
||
angegebenen Dateinamen; der WebSocket hat dafür ein Payload-Limit von 32 MB.
|
||
|
||
Ob das eingestellte Modell Bilder kann, verrät:
|
||
|
||
```bash
|
||
curl -s http://localhost:11434/api/show -d '{"name":"qwen3.5:latest"}' \
|
||
| python3 -c "import json,sys; print(json.load(sys.stdin)['capabilities'])"
|
||
```
|
||
|
||
## Werkzeuge (Tool-Calling)
|
||
|
||
`qwen3.5` meldet die Fähigkeit `tools`. Der Server schickt bei jeder Anfrage
|
||
eine Werkzeugliste mit; will das Modell eines benutzen, wird es ausgeführt und
|
||
das Ergebnis zurückgereicht, bis eine Antwort ohne Werkzeugwunsch entsteht
|
||
(maximal `MAX_TOOL_ROUNDS` = 5 Runden, je 15 s Zeitlimit). Abschalten lässt
|
||
sich das in den Einstellungen.
|
||
|
||
| Werkzeug | Zweck |
|
||
|---|---|
|
||
| `get_time` | Datum, Uhrzeit, Wochentag für eine IANA-Zeitzone |
|
||
| `calculate` | Arithmetik mit eigenem Parser |
|
||
| `read_file` | Textdatei unterhalb des Projektverzeichnisses lesen |
|
||
| `list_files` | Verzeichnis auflisten |
|
||
| `remember` | Wichtigen Punkt als gepinnten Ankerpunkt ins Gedächtnis schreiben |
|
||
| `recall` | Gedächtnis (Ankerpunkte) durchsuchen |
|
||
| `read_webpage` | Öffentliche Website laden, Hauptinhalt als Markdown |
|
||
|
||
Aktuelle Liste: `curl -s http://localhost:8788/api/tools`
|
||
|
||
## Gedächtnis (Chat-Memory)
|
||
|
||
Das Gedächtnis hat drei Schichten:
|
||
|
||
| Schicht | Speicher | Zweck |
|
||
|---|---|---|
|
||
| Arbeitsgedächtnis | Nachrichtenfenster | Einstellbar (Default 10 Schritte), geht ans Modell |
|
||
| Episodisch | `memory_events` | Append-Only-Log: nur INSERT, nie UPDATE/DELETE |
|
||
| Semantisch | `anchors` | Ankerpunkte, destilliert in der Traumphase |
|
||
|
||
Das Fenster ist in den Einstellungen per Slider einstellbar (2–100 Schritte).
|
||
|
||
### Traumphase
|
||
|
||
Die Traumphase konsolidiert neue Log-Einträge in Ankerpunkte — bevorzugt per
|
||
LLM-Extraktion (JSON-Format, Temperatur 0.2), mit einer Regex-Heuristik als
|
||
Fallback. Auslöser:
|
||
|
||
- automatisch nach 3 Minuten Inaktivität oder wenn 10 Schritte unkonsolidiert sind
|
||
- manuell über 🧠 **Gedächtnis** → „Traumphase jetzt"
|
||
|
||
Ankerpunkte haben eine Wichtigkeit (0–1), eine Art (`fact`, `decision`,
|
||
`preference`, `entity`, `open_question`) und einen Ursprung (`dream`, `model`,
|
||
`heuristic`, `test`). Ähnliche Anker werden zusammengeführt (Wort-Ähnlichkeit
|
||
≥ 0.5, `hits` steigt). Bei jedem Traumlauf verfallen ungepinnte Anker
|
||
(Importance × 0.9); unter 0.15 und ohne Treffer werden sie gelöscht. Gepinnte
|
||
(★) bleiben dauerhaft.
|
||
|
||
Die wichtigsten Anker werden als System-Kontext eingespielt (Budget ~1200
|
||
Zeichen) — das Modell beantwortet dann Fragen aus Inhalten, die nie im
|
||
sichtbaren Chatverlauf standen.
|
||
|
||
### Rekonstruktion
|
||
|
||
Weil das Log append-only ist, lässt sich der Zustand jederzeit deterministisch
|
||
aus Seq 0 neu falten: 🧠 **Gedächtnis** → „Aus Log rekonstruieren" (gepinnte
|
||
Anker bleiben erhalten). Die Eval der Heuristik und des Dream-Parsers:
|
||
|
||
```bash
|
||
npm run memory:eval --workspace server
|
||
```
|
||
|
||
Endpunkte: `GET /api/sessions/:id/memory`, `POST /api/sessions/:id/dream`,
|
||
`POST /api/sessions/:id/memory/rebuild`, `PATCH|DELETE /api/anchors/:id`.
|
||
|
||
### Grenzen
|
||
|
||
Alle Werkzeuge sind **ausschließlich lesend**. Es gibt nichts, was schreibt,
|
||
löscht oder Befehle ausführt — entsprechend braucht es noch keine
|
||
Rückfrage pro Aufruf. Das Feld `requiresConfirmation` in
|
||
`tools/types.ts` ist bereits vorgesehen, damit die Bestätigungspflicht nicht
|
||
nachträglich eingezogen werden muss, sobald ein schreibendes Werkzeug dazukommt.
|
||
|
||
Ausnahme Netzwerk: `read_webpage` lädt öffentliche Websites. Der Abruf ist
|
||
geguardet — nur http/https, private Adressbereiche werden nach DNS-Auflösung
|
||
abgewiesen (SSRF-Schutz, auch über Weiterleitungen), 15 s Zeitlimit, 2 MB
|
||
Fetch-Limit, 25 kB Output-Cap. Der Inhalt wird dem Modell als nicht
|
||
vertrauenswürdig markiert (Prompt-Injection aus Webseiten).
|
||
|
||
Der Dateizugriff liegt in einer Sandbox: Jeder Pfad wird über `realpath`
|
||
aufgelöst (löst auch Symlinks auf) und muss danach unterhalb der Wurzel liegen,
|
||
sonst wird abgelehnt. Die Wurzel ist standardmäßig das Projektverzeichnis und
|
||
über `TOOLS_ROOT` einstellbar. `.env`, `.git/` und Schlüsseldateien sind auch
|
||
innerhalb der Wurzel gesperrt.
|
||
|
||
`calculate` benutzt bewusst **kein** `eval` oder `new Function`: der Ausdruck
|
||
stammt aus einer Modellantwort, die von Nutzereingaben beeinflusst wird. Der
|
||
Parser in `tools/calculate.ts` kennt nur Zahlen und Grundrechenarten.
|
||
|
||
Werkzeugaufrufe werden in der Oberfläche über der Antwort angezeigt. Sie leben
|
||
nur im Browser-Zustand und sind nach einem Neuladen weg — im Gegensatz zu
|
||
Bildern, die in der Datenbank landen.
|
||
|
||
## Sicherheit
|
||
|
||
Der Server hat **keine Authentifizierung** und lauscht deshalb bewusst nur auf
|
||
`127.0.0.1`. Das allein genügt aber nicht: Eine beliebige Webseite, die im
|
||
Browser geöffnet ist, kann `localhost` per `fetch()` oder WebSocket erreichen.
|
||
Deshalb prüfen sowohl die HTTP-Endpunkte als auch der WebSocket-Handshake die
|
||
`Origin` gegen eine Allowlist (`server/src/security.ts`) — Standard sind
|
||
`localhost`/`127.0.0.1` auf Port 5174 und 8788.
|
||
|
||
Läuft das Frontend woanders, die Origin ergänzen:
|
||
|
||
```bash
|
||
ALLOWED_ORIGINS=http://192.168.1.50:5174
|
||
```
|
||
|
||
Weitere Maßnahmen: Rate-Limits auf `/api/stt` (10/min) und `/api/tts` (30/min),
|
||
Format-Whitelist per Magic Bytes vor dem `ffmpeg`-Aufruf, und Fehlerdetails
|
||
landen im Server-Log statt in der HTTP-Antwort.
|
||
|
||
Für externen Zugriff reicht ein Reverse-Proxy **nicht** — davor gehört eine
|
||
echte Authentifizierung.
|
||
|
||
## Entwicklung
|
||
|
||
```bash
|
||
npm install
|
||
npm run dev # Server (:8788) und Vite-Dev-Server (:5174) parallel
|
||
```
|
||
|
||
Einzeln:
|
||
|
||
```bash
|
||
npm run dev:server
|
||
npm run dev:web
|
||
```
|
||
|
||
Typecheck über beide Workspaces:
|
||
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
|
||
## Produktion
|
||
|
||
```bash
|
||
npm start # baut das Frontend und startet den Server
|
||
```
|
||
|
||
Der Server bindet nur an `127.0.0.1`. Zum Aussetzen ins Netz siehe
|
||
[Sicherheit](#sicherheit).
|