chore: stabilisation Phase 1 — reconnexion auto, timeout LLM, systemd, README
- Ajout reconnexion auto WebSocket avec backoff exponentiel (2s→30s, max 20 tentatives) - Ajout timeout LLM côté frontend (25s) avec réactivation du bouton - Réduction timeout serveur httpx 60→45s - Service systemd user jarvis-v3 (survit aux reboots) - Launcher Python avec fallback .env si Infisical expiré - README.md complet (architecture, URLs, roadmap, maintenance) - .gitignore (venv, pycache, logs)
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# JARVIS v3 — Assistant Vocal Intelligent
|
||||
|
||||
> Phase 1 : Relay WebSocket → Hermes A.L.E.X.A.
|
||||
> PoC fonctionnel — interface chat texte en temps réel
|
||||
|
||||
## 🎯 Objectif
|
||||
|
||||
Assistant vocal intelligent avec mémoire persistante, connecté à l'API Hermes A.L.E.X.A.
|
||||
Phase 1 valide la communication texte en temps réel via WebSocket.
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
┌──────────────┐ WebSocket ┌──────────────┐ HTTP/POST ┌──────────────┐
|
||||
│ Navigateur │ ◄──────────────► │ bot_direct │ ◄───────────────► │ Hermes API │
|
||||
│ (test.html) │ ws://host/ws │ .py :7081 │ localhost:8642 │ A.L.E.X.A. │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ Infisical │
|
||||
│ (clé API) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
## 📦 Fichiers
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---------|------|
|
||||
| `bot_direct.py` | Serveur WebSocket + interface HTML inline |
|
||||
| `launcher.py` | Récupère la clé Hermes via Infisical puis lance `bot_direct.py` |
|
||||
| `run_direct.py` | Lanceur manuel (alternative à systemd) |
|
||||
| `bot.py` | Tentative Pipecat 1.4.0 (non fonctionnel — pour référence) |
|
||||
| `test.html` | Interface chat (intégrée inline dans `bot_direct.py`) |
|
||||
| `start.sh` | Script Pipecat original (obsolète) |
|
||||
|
||||
## 🚀 Démarrage
|
||||
|
||||
### Avec systemd (recommandé)
|
||||
|
||||
```bash
|
||||
# Statut
|
||||
systemctl --user status jarvis-v3
|
||||
|
||||
# Logs
|
||||
journalctl --user -u jarvis-v3 -f
|
||||
|
||||
# Redémarrer
|
||||
systemctl --user restart jarvis-v3
|
||||
```
|
||||
|
||||
### Manuel
|
||||
|
||||
```bash
|
||||
cd ~/workspace/jarvis-pipecat
|
||||
source venv/bin/activate
|
||||
python3 run_direct.py
|
||||
```
|
||||
|
||||
## 🌐 Accès
|
||||
|
||||
- **URL** : http://openclaw1.dev.home:7081/
|
||||
- **WebSocket** : ws://openclaw1.dev.home:7081/ws
|
||||
- **Port** : 7081 (configurable via `JARVIS_PORT`)
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
| Variable | Valeur | Description |
|
||||
|----------|--------|-------------|
|
||||
| `JARVIS_PORT` | `7081` | Port d'écoute |
|
||||
| `HERMES_API_KEY` | *(Infisical)* | Clé API Hermes |
|
||||
| `HERMES_URL` | `http://localhost:8642/v1/chat/completions` | Endpoint API |
|
||||
|
||||
## 🔑 Secret
|
||||
|
||||
La clé API Hermes est stockée dans **Infisical** (`/hermes-claw/API_SERVER_KEY`).
|
||||
Le launcher (`launcher.py`) la récupère automatiquement au démarrage.
|
||||
|
||||
## 🛠️ Dépendances
|
||||
|
||||
```
|
||||
websockets==16.0
|
||||
httpx
|
||||
```
|
||||
|
||||
Installées dans `venv/`.
|
||||
|
||||
## 📊 Roadmap
|
||||
|
||||
| Phase | Contenu | Statut |
|
||||
|-------|---------|--------|
|
||||
| **Phase 1** | Chat texte WebSocket + Hermes | ✅ Stable |
|
||||
| **Phase 2** | Audio (STT Parakeet + TTS Edge/Kokoro) | 🔜 Planifié |
|
||||
| **Phase 3** | Intégration Pipecat WebRTC | 🔜 Planifié |
|
||||
| **Phase 4** | Multi-agent, barge-in, wake word | 📋 Backlog |
|
||||
|
||||
## 🔧 Maintenance
|
||||
|
||||
```bash
|
||||
# Voir les logs
|
||||
journalctl --user -u jarvis-v3 -n 50
|
||||
|
||||
# Redémarrer après modification
|
||||
rm -rf ~/workspace/jarvis-pipecat/__pycache__/
|
||||
systemctl --user restart jarvis-v3
|
||||
|
||||
# Arrêter
|
||||
systemctl --user stop jarvis-v3
|
||||
```
|
||||
|
||||
## 📝 Notes techniques
|
||||
|
||||
- **websockets 16.0** : `respond()` ne prend que `(status, text)`. Pour les headers custom,
|
||||
retourner un objet `Response` manuellement (`from websockets.http11 import Response, Headers`).
|
||||
- **Content-Type** : doit être `text/html; charset=utf-8` pour que le navigateur rende l'interface.
|
||||
- **Pipecat 1.4.0** : API massivement refactorée, `LLMMessagesFrame` supprimée, `LLMContext`
|
||||
n'est plus un processeur. Abandonné pour la Phase 1, à réévaluer en Phase 3.
|
||||
|
||||
## 📄 Licence
|
||||
|
||||
Projet personnel — Bruno Charest, 2026.
|
||||
+9
-6
@@ -84,14 +84,17 @@ button:disabled{opacity:.5;cursor:default}
|
||||
<script>
|
||||
const s=document.getElementById('status'),c=document.getElementById('chat'),
|
||||
i=document.getElementById('inp'),b=document.getElementById('btn');
|
||||
let ws=null;
|
||||
let ws=null,recon=0,reconMax=20,reconDelay=2000;
|
||||
function m(t,r){const d=document.createElement('div');d.className='msg '+r;d.textContent=t;c.appendChild(d);c.scrollTop=c.scrollHeight}
|
||||
function connect(){ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/ws');
|
||||
ws.onopen=()=>{s.textContent='🟢 Connecté — JARVIS v3 prêt';s.className='on';b.disabled=false;i.focus()};
|
||||
ws.onclose=()=>{s.textContent='🔴 Déconnecté';s.className='off';b.disabled=true;setTimeout(connect,2000)};
|
||||
ws.onmessage=e=>{try{let d=JSON.parse(e.data);if(d.type==='welcome')m(d.content,'sys');
|
||||
ws.onopen=()=>{recon=0;reconDelay=2000;s.textContent='🟢 Connecté — JARVIS v3 prêt';s.className='on';b.disabled=false;i.focus()};
|
||||
ws.onclose=()=>{b.disabled=true;if(recon<reconMax){recon++;let d=Math.min(reconDelay*Math.pow(1.5,recon-1),30000);
|
||||
s.textContent='🟡 Reconnexion '+recon+'/'+reconMax+' dans '+Math.round(d/1000)+'s...';s.className='off';setTimeout(connect,d)}
|
||||
else{s.textContent='🔴 Connexion perdue — recharge la page';s.className='off'}};
|
||||
function send(){let t=i.value.trim();if(!t||!ws||ws.readyState!==WebSocket.OPEN)return;m(t,'user');ws.send(JSON.stringify({text:t}));i.value='';b.disabled=true;s.textContent='🤔 JARVIS réfléchit...';let tid=setTimeout(()=>{if(b.disabled){b.disabled=false;s.textContent='⚠️ JARVIS prend du temps... tu peux réessayer'}},25000);ws._tid=tid}
|
||||
ws.onmessage=e=>{try{let d=JSON.parse(e.data);if(ws._tid){clearTimeout(ws._tid);ws._tid=null};if(d.type==='welcome')m(d.content,'sys');
|
||||
else if(d.type==='response')m(d.content,'jarvis');else if(d.type==='thinking')s.textContent='🤔 JARVIS réfléchit...';
|
||||
else if(d.type==='error')m('❌ '+d.content,'sys')}catch(x){m(e.data,'jarvis')};s.textContent='🟢 Connecté — JARVIS v3 prêt';b.disabled=false}}
|
||||
else if(d.type==='error')m('❌ '+d.content,'sys')}catch(x){m(e.data,'jarvis')};s.textContent='🟢 Connecté — JARVIS v3 prêt';b.disabled=false}
|
||||
function send(){let t=i.value.trim();if(!t||!ws||ws.readyState!==WebSocket.OPEN)return;m(t,'user');ws.send(JSON.stringify({text:t}));i.value='';b.disabled=true;s.textContent='🤔 JARVIS réfléchit...'}
|
||||
b.onclick=send;i.onkeydown=e=>{if(e.key==='Enter')send()};connect()
|
||||
</script>
|
||||
@@ -116,7 +119,7 @@ async def process_request(connection, request):
|
||||
async def ws_handler(websocket):
|
||||
"""Handle WebSocket connection: relay messages to Hermes API."""
|
||||
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||
client = httpx.AsyncClient(timeout=60.0)
|
||||
client = httpx.AsyncClient(timeout=45.0)
|
||||
|
||||
try:
|
||||
# Send welcome
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Launcher systemd pour JARVIS v3 — récupère la clé Hermes puis démarre le bot."""
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def get_hermes_key():
|
||||
"""Récupère la clé API Hermes depuis Infisical."""
|
||||
key = os.environ.get("HERMES_API_KEY", "") or os.environ.get("HERMES_ALEXA_KEY", "")
|
||||
if key:
|
||||
return key
|
||||
try:
|
||||
creds = json.load(open("/opt/infisical/admin-credentials.json"))
|
||||
r = subprocess.run(
|
||||
["/home/openclaw/.local/lib/node_modules/@infisical/cli/bin/infisical",
|
||||
"secrets", "get", "API_SERVER_KEY",
|
||||
"--domain", "https://secret.dracodev.net",
|
||||
"--projectId", "192c1eed-6666-41ee-80df-a4636a2da0ad",
|
||||
"--env", "prod", "--path", "/hermes-claw", "--plain",
|
||||
"--token", creds["token"]],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
return r.stdout.strip()
|
||||
except Exception as e:
|
||||
print(f"[launcher] Infisical: {e}", file=sys.stderr)
|
||||
|
||||
# Fallback: lire depuis workspace/.env
|
||||
try:
|
||||
with open(os.path.expanduser("~/workspace/.env")) as f:
|
||||
for line in f:
|
||||
if line.startswith("HERMES_ALEXA_KEY="):
|
||||
return line.split("=", 1)[1].strip()
|
||||
if line.startswith("API_SERVER_KEY="):
|
||||
return line.split("=", 1)[1].strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ""
|
||||
|
||||
if __name__ == "__main__":
|
||||
key = get_hermes_key()
|
||||
if not key:
|
||||
print("[launcher] ERREUR: Impossible de récupérer la clé Hermes", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
os.environ["HERMES_API_KEY"] = key
|
||||
os.environ["JARVIS_PORT"] = os.environ.get("JARVIS_PORT", "7081")
|
||||
|
||||
# Lance bot_direct.py
|
||||
bot_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bot_direct.py")
|
||||
python = os.path.join(os.path.dirname(os.path.abspath(__file__)), "venv", "bin", "python")
|
||||
os.execv(python, [python, bot_path])
|
||||
Reference in New Issue
Block a user