Files
jarvis-v3/bot.py
T
bruno 09984d49fa feat: JARVIS v3 Phase 1 — WebSocket relay vers Hermes A.L.E.X.A.
- Serveur WebSocket direct avec websockets 16.0
- Interface HTML/CSS/JS dark theme avec chat
- Relay vers API Hermes sur port 8642
- Historique de conversation (20 messages)
- Clé API via Infisical
- Testé et fonctionnel sur openclaw1.dev.home:7081
2026-06-19 22:46:32 -04:00

173 lines
8.0 KiB
Python

#!/usr/bin/env python3
"""
JARVIS v3 PoC — Pipecat + Hermes API
=====================================
Bot WebSocket texte : transport → LLM → transport
Sans agrégateur — notre processeur custom gère tout.
"""
import json, os, subprocess, sys
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
import uvicorn
import pipecat.frames.frames as pframes
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineTask
from pipecat.processors.frame_processor import FrameProcessor, FrameDirection
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
PORT = int(os.environ.get("JARVIS_PIPECAT_PORT", "7081"))
# ─── Clé API ─────────────────────────────────────────────────
def _get_hermes_key():
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/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)
return r.stdout.strip()
except Exception as e:
print(f"⚠️ Infisical: {e}"); return ""
_hermes_key = _get_hermes_key()
HERMES_URL = "http://localhost:8642/v1"
SYSTEM_PROMPT = (
"Tu es JARVIS, l'assistant vocal de Bruno. "
"Parle en français, sois concis (3 phrases max), chaleureux et utile. "
"Pas de markdown. Réponds comme si tu parlais à l'oral."
)
# ─── Processeur custom ultra-simple ──────────────────────────
class SimpleLLMRelay(FrameProcessor):
"""
Capture les messages texte du transport et les envoie au LLM via
LLMContextFrame. Les réponses du LLM passent directement au transport.
"""
def __init__(self, system_prompt: str):
super().__init__()
self._messages = [{"role": "system", "content": system_prompt}]
async def process_frame(self, frame, direction):
await self.push_frame(frame, direction)
if isinstance(frame, pframes.InputTransportMessageFrame):
text = frame.message
if isinstance(text, str) and text.strip():
self._messages.append({"role": "user", "content": text})
if len(self._messages) > 21:
self._messages = [self._messages[0]] + self._messages[-20:]
print(f"👤 {text[:80]}")
ctx = pframes.LLMContextFrame(context=list(self._messages))
await self.push_frame(ctx)
# ─── Page HTML ───────────────────────────────────────────────
HTML_PAGE = open(os.path.join(os.path.dirname(__file__), "test.html"), "r").read() if os.path.exists("test.html") else """<!DOCTYPE html>
<html lang="fr">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>JARVIS v3</title>
<style>
:root{--bg:#010314;--surface:#060e24;--text:#cdd6f4;--cyan:#06d6f0;--blue:#3b82f6}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex;flex-direction:column;align-items:center;padding:20px}
h1{color:var(--cyan);margin-bottom:10px;font-size:1.3em}
#status{padding:6px 14px;border-radius:8px;margin:8px 0;font-size:.85em}
.on{background:#10b98122;color:#10b981;border:1px solid #10b981}
.off{background:#ef444422;color:#ef4444;border:1px solid #ef4444}
#chat{width:100%;max-width:600px;flex:1;overflow-y:auto;background:var(--surface);border:1px solid #121e3d;border-radius:12px;padding:16px;margin:10px 0}
.msg{margin:8px 0;padding:8px 12px;border-radius:8px;max-width:85%}
.user{background:var(--blue);margin-left:auto;text-align:right}
.jarvis{background:#1a1a3e;margin-right:auto;border-left:2px solid var(--cyan)}
.sys{text-align:center;color:#4a5a7f;font-style:italic;font-size:.85em}
.row{display:flex;width:100%;max-width:600px;gap:8px}
input{flex:1;padding:12px;border-radius:8px;border:1px solid #121e3d;background:var(--surface);color:var(--text);font-size:1em}
button{padding:12px 24px;border-radius:8px;border:none;background:var(--cyan);color:var(--bg);font-weight:bold;cursor:pointer}
</style></head>
<body><h1>⚡ JARVIS v3 — Pipecat PoC</h1>
<div id="status" class="off">🔴 Déconnecté</div><div id="chat"></div>
<div class="row"><input id="inp" placeholder="Pose ta question..." autofocus><button id="btn">Envoyer</button></div>
<script>
const s=document.getElementById('status'),c=document.getElementById('chat'),i=document.getElementById('inp'),b=document.getElementById('btn');
let ws=null;
function m(t,r){const d=document.createElement('div');d.className='msg '+r;d.textContent=t;c.appendChild(d);c.scrollTop=c.scrollHeight}
function co(){ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/ws');
ws.onopen=()=>{s.textContent='🟢 Connecté';s.className='on'};
ws.onclose=()=>{s.textContent='🔴 Déconnecté';s.className='off';setTimeout(co,2000)};
ws.onmessage=e=>{try{let d=JSON.parse(e.data);m(d.content||d.message||e.data,d.content?'jarvis':'sys')}catch(x){m(e.data,'jarvis')}}}
function se(){let t=i.value.trim();if(!t||!ws||ws.readyState!==WebSocket.OPEN)return;m(t,'user');ws.send(t);i.value=''}
b.onclick=se;i.onkeydown=e=>{if(e.key==='Enter')se()};co()
</script></body></html>"""
# ─── FastAPI ─────────────────────────────────────────────────
app = FastAPI(title="JARVIS v3 PoC", version="0.1.0")
@app.get("/")
async def root():
return HTMLResponse(content=HTML_PAGE)
@app.websocket("/ws")
async def ws_endpoint(websocket: WebSocket):
transport = FastAPIWebsocketTransport(
websocket=websocket,
params=FastAPIWebsocketParams(
add_wav_header=False,
session_timeout=None,
# allowed_origins vide = autorise toutes les origines
),
)
relay = SimpleLLMRelay(SYSTEM_PROMPT)
llm = OpenAILLMService(
api_key=_hermes_key,
base_url=HERMES_URL,
settings=OpenAILLMService.Settings(
model="Hermes Agent", temperature=0.8, max_tokens=300,
),
)
pipeline = Pipeline(processors=[transport.input(), relay, llm, transport.output()])
task = PipelineTask(pipeline)
@transport.event_handler("on_client_connected")
async def on_connect(transport, client):
print(f"✅ Connecté")
await transport.output().send_message(
pframes.OutputTransportMessageFrame(
message="👋 Bonjour Bruno ! Je suis JARVIS v3. Pose-moi une question !"
)
)
@transport.event_handler("on_client_disconnected")
async def on_disconnect(transport, client):
print(f"👋 Déconnecté")
await task.cancel()
runner = PipelineRunner()
await runner.add_workers(task)
try:
await runner.run()
except Exception as e:
print(f"Pipeline error: {e}")
# ─── Main ────────────────────────────────────────────────────
if __name__ == "__main__":
if not _hermes_key:
print("❌ Pas de clé API Hermes"); sys.exit(1)
print(f"🔑 {_hermes_key[:15]}...")
print(f"🌐 {HERMES_URL}")
print(f"🚀 http://0.0.0.0:{PORT}")
print(f" ws://0.0.0.0:{PORT}/ws")
uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")