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
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
venv/
|
||||
.env
|
||||
*.log
|
||||
*.pid
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
cd ~/workspace/jarvis-pipecat
|
||||
echo "=== head ==="
|
||||
head -30 bot.py
|
||||
echo "=== line count ==="
|
||||
wc -l bot.py
|
||||
echo "=== import test ==="
|
||||
./venv/bin/python -c "import bot" 2>&1
|
||||
echo "exit: $?"
|
||||
@@ -0,0 +1,36 @@
|
||||
=== head ===
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
JARVIS v3 PoC — Pipecat bot avec API Hermes
|
||||
============================================
|
||||
Phase 1 : bot texte via WebSocket
|
||||
Version corrigée pour Pipecat 1.4.0 — utilise LLMContext + LLMContextFrame
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from fastapi import FastAPI, WebSocket
|
||||
from fastapi.responses import HTMLResponse
|
||||
import uvicorn
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
Frame,
|
||||
InputTransportMessageFrame,
|
||||
LLMContextFrame,
|
||||
LLMFullResponseEndFrame,
|
||||
LLMFullResponseStartFrame,
|
||||
LLMTextFrame,
|
||||
OutputTransportMessageFrame,
|
||||
OutputTransportMessageUrgentFrame,
|
||||
StartFrame,
|
||||
)
|
||||
from pipecat.pipeline.pipeline import Pipeline
|
||||
from pipecat.pipeline.runner import PipelineRunner
|
||||
=== line count ===
|
||||
354 bot.py
|
||||
=== import test ===
|
||||
2026-06-19 17:14:16.253 | INFO | pipecat:<module>:28 - ᓚᘏᗢ Pipecat 1.4.0 (Python 3.13.5 (main, May 5 2026, 21:05:52) [GCC 14.2.0]) ᓚᘏᗢ
|
||||
exit: 0
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import pipecat.frames.frames as pf
|
||||
for name in sorted(dir(pf)):
|
||||
if 'LLM' in name or 'Context' in name or 'Message' in name:
|
||||
print(name)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import pipecat.frames.frames as pf
|
||||
# LLMContextFrame
|
||||
import inspect
|
||||
print("=== LLMContextFrame ===")
|
||||
print(inspect.getsource(pf.LLMContextFrame))
|
||||
print()
|
||||
print("=== LLMMessagesAppendFrame ===")
|
||||
print(inspect.getsource(pf.LLMMessagesAppendFrame))
|
||||
print()
|
||||
print("=== LLMMessagesUpdateFrame ===")
|
||||
print(inspect.getsource(pf.LLMMessagesUpdateFrame))
|
||||
print()
|
||||
# Also look at LLMContext aggregator
|
||||
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||
print("=== LLMContext.__init__ ===")
|
||||
print(inspect.getsource(LLMContext.__init__))
|
||||
@@ -0,0 +1,28 @@
|
||||
2026-06-19 17:09:12.173 | INFO | pipecat:<module>:28 - ᓚᘏᗢ Pipecat 1.4.0 (Python 3.13.5 (main, May 5 2026, 21:05:52) [GCC 14.2.0]) ᓚᘏᗢ
|
||||
FunctionCallFromLLM
|
||||
InputTransportMessageFrame
|
||||
LLMAssistantPushAggregationFrame
|
||||
LLMConfigureOutputFrame
|
||||
LLMContextAssistantTimestampFrame
|
||||
LLMContextAssistantTurnFrame
|
||||
LLMContextFrame
|
||||
LLMContextSummaryRequestFrame
|
||||
LLMContextSummaryResultFrame
|
||||
LLMEnablePromptCachingFrame
|
||||
LLMFullResponseEndFrame
|
||||
LLMFullResponseStartFrame
|
||||
LLMMarkerFrame
|
||||
LLMMessagesAppendFrame
|
||||
LLMMessagesTransformFrame
|
||||
LLMMessagesUpdateFrame
|
||||
LLMRunFrame
|
||||
LLMSetToolChoiceFrame
|
||||
LLMSetToolsFrame
|
||||
LLMSummarizeContextFrame
|
||||
LLMTextFrame
|
||||
LLMThoughtEndFrame
|
||||
LLMThoughtStartFrame
|
||||
LLMThoughtTextFrame
|
||||
LLMUpdateSettingsFrame
|
||||
OutputTransportMessageFrame
|
||||
OutputTransportMessageUrgentFrame
|
||||
@@ -0,0 +1,74 @@
|
||||
2026-06-19 17:09:30.215 | INFO | pipecat:<module>:28 - ᓚᘏᗢ Pipecat 1.4.0 (Python 3.13.5 (main, May 5 2026, 21:05:52) [GCC 14.2.0]) ᓚᘏᗢ
|
||||
=== LLMContextFrame ===
|
||||
@dataclass
|
||||
class LLMContextFrame(Frame):
|
||||
"""Frame containing a universal LLM context.
|
||||
|
||||
Used as a signal to LLM services to ingest the provided context and
|
||||
generate a response based on it.
|
||||
|
||||
Parameters:
|
||||
context: The LLM context containing messages, tools, and configuration.
|
||||
"""
|
||||
|
||||
context: LLMContext
|
||||
|
||||
|
||||
=== LLMMessagesAppendFrame ===
|
||||
@dataclass
|
||||
class LLMMessagesAppendFrame(DataFrame):
|
||||
"""Frame containing LLM messages to append to current context.
|
||||
|
||||
A frame containing a list of LLM messages that need to be added to the
|
||||
current context.
|
||||
|
||||
Parameters:
|
||||
messages: List of context messages to append.
|
||||
run_llm: Whether the context update should be sent to the LLM.
|
||||
"""
|
||||
|
||||
messages: list[LLMContextMessage]
|
||||
run_llm: bool | None = None
|
||||
|
||||
|
||||
=== LLMMessagesUpdateFrame ===
|
||||
@dataclass
|
||||
class LLMMessagesUpdateFrame(DataFrame):
|
||||
"""Frame containing LLM messages to replace current context.
|
||||
|
||||
A frame containing a list of new LLM messages to replace the current
|
||||
context LLM messages.
|
||||
|
||||
Parameters:
|
||||
messages: List of context messages to replace current context.
|
||||
run_llm: Whether the context update should be sent to the LLM.
|
||||
"""
|
||||
|
||||
messages: list[LLMContextMessage]
|
||||
run_llm: bool | None = None
|
||||
|
||||
|
||||
=== LLMContext.__init__ ===
|
||||
def __init__(
|
||||
self,
|
||||
messages: list[LLMContextMessage] | None = None,
|
||||
tools: ToolsSchema | list[FunctionSchema | DirectFunction] | NotGiven = NOT_GIVEN,
|
||||
tool_choice: LLMContextToolChoice | NotGiven = NOT_GIVEN,
|
||||
):
|
||||
"""Initialize the LLM context.
|
||||
|
||||
Args:
|
||||
messages: Initial list of conversation messages.
|
||||
tools: Available tools for the LLM to use. May be a ``ToolsSchema``
|
||||
or a plain list of direct functions and/or ``FunctionSchema``
|
||||
objects (normalized to a ``ToolsSchema`` internally). Any tool
|
||||
that carries a handler — a direct function, or a
|
||||
``FunctionSchema`` with its ``handler`` set — is registered with
|
||||
the LLM service automatically, so no separate
|
||||
``register_function`` call is needed.
|
||||
tool_choice: Tool selection strategy for the LLM.
|
||||
"""
|
||||
self._messages: list[LLMContextMessage] = messages if messages else []
|
||||
self._tools: ToolsSchema | NotGiven = LLMContext._normalize_and_validate_tools(tools)
|
||||
self._tool_choice: LLMContextToolChoice | NotGiven = tool_choice
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
# Read the raw environ of the hermes gateway process
|
||||
with open('/proc/1359/environ', 'rb') as f:
|
||||
data = f.read()
|
||||
for var in data.split(b'\x00'):
|
||||
if var:
|
||||
decoded = var.decode('utf-8', errors='replace')
|
||||
if any(k in decoded.lower() for k in ['key', 'secret', 'token', 'api_server']):
|
||||
print(decoded)
|
||||
@@ -0,0 +1,5 @@
|
||||
with open('/home/openclaw/workspace/jarvis-pipecat/bot.py') as f:
|
||||
lines = f.readlines()
|
||||
for i, line in enumerate(lines, 1):
|
||||
if 67 <= i <= 100 or 110 <= i <= 175:
|
||||
print(f'{i}: {line}', end='')
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/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")
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
JARVIS v3 Phase 1 — Relay WebSocket ↔ Hermes API
|
||||
==================================================
|
||||
PoC minimal : WebSocket texte → API Hermes → réponse texte.
|
||||
Pas de Pipecat — intégration directe pour valider le concept.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import httpx
|
||||
from websockets.asyncio.server import serve
|
||||
from websockets.http11 import Response, Headers
|
||||
|
||||
PORT = int(os.environ.get("JARVIS_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/chat/completions"
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
# ─── Page HTML ───────────────────────────────────────────────
|
||||
HTML_PAGE = """<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>JARVIS v3 — Phase 1 PoC</title>
|
||||
<style>
|
||||
:root{--bg:#010314;--surface:#060e24;--text:#cdd6f4;--cyan:#06d6f0;--blue:#3b82f6;--green:#10b981;--red:#ef4444;--border:#121e3d}
|
||||
*{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:16px}
|
||||
h1{color:var(--cyan);font-size:1.2em;margin-bottom:6px}
|
||||
.status{font-size:.8em;padding:4px 12px;border-radius:6px;margin-bottom:8px}
|
||||
.on{background:#10b98122;color:var(--green);border:1px solid var(--green)}
|
||||
.off{background:#ef444422;color:var(--red);border:1px solid var(--red)}
|
||||
#chat{width:100%;max-width:600px;flex:1;overflow-y:auto;
|
||||
background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px;margin-bottom:10px}
|
||||
.msg{margin:6px 0;padding:8px 12px;border-radius:8px;max-width:85%;font-size:.95em}
|
||||
.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:.8em}
|
||||
.row{display:flex;width:100%;max-width:600px;gap:8px}
|
||||
input{flex:1;padding:10px 12px;border-radius:8px;border:1px solid var(--border);
|
||||
background:var(--surface);color:var(--text);font-size:1em}
|
||||
button{padding:10px 20px;border-radius:8px;border:none;background:var(--cyan);
|
||||
color:var(--bg);font-weight:bold;cursor:pointer;font-size:.95em}
|
||||
button:hover{opacity:.9}
|
||||
button:disabled{opacity:.5;cursor:default}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>⚡ JARVIS v3 — Phase 1 (Hermes direct)</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 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');
|
||||
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}}
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# ─── HTTP Handler ────────────────────────────────────────────
|
||||
async def http_handler(websocket):
|
||||
"""Serve the test page over raw HTTP (before upgrade)."""
|
||||
# This is handled by serve()'s process_request
|
||||
pass
|
||||
|
||||
async def process_request(connection, request):
|
||||
"""Serve HTML for HTTP requests, upgrade for WebSocket."""
|
||||
if request.path == "/" or request.path == "/test.html":
|
||||
headers = Headers([("Content-Type", "text/html; charset=utf-8")])
|
||||
return Response(200, "OK", headers, HTML_PAGE.encode())
|
||||
return None # Let WebSocket handler take over
|
||||
|
||||
# ─── WebSocket Handler ───────────────────────────────────────
|
||||
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)
|
||||
|
||||
try:
|
||||
# Send welcome
|
||||
await websocket.send(json.dumps({
|
||||
"type": "welcome",
|
||||
"content": "👋 Bonjour Bruno ! Je suis JARVIS v3 (Phase 1). Pose-moi une question !"
|
||||
}))
|
||||
print("✅ Client connecté")
|
||||
|
||||
async for raw in websocket:
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
text = data.get("text", "").strip()
|
||||
except:
|
||||
text = raw.strip()
|
||||
|
||||
if not text:
|
||||
continue
|
||||
|
||||
print(f"👤 {text[:80]}")
|
||||
messages.append({"role": "user", "content": text})
|
||||
# Keep last 20 messages + system prompt
|
||||
if len(messages) > 21:
|
||||
messages = [messages[0]] + messages[-20:]
|
||||
|
||||
# Send thinking indicator
|
||||
await websocket.send(json.dumps({"type": "thinking"}))
|
||||
|
||||
# Call Hermes API
|
||||
try:
|
||||
resp = await client.post(
|
||||
HERMES_URL,
|
||||
headers={
|
||||
"Authorization": f"Bearer {HERMES_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"model": "Hermes Agent",
|
||||
"messages": messages,
|
||||
"temperature": 0.8,
|
||||
"max_tokens": 300,
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
body = resp.json()
|
||||
reply = body["choices"][0]["message"]["content"]
|
||||
|
||||
messages.append({"role": "assistant", "content": reply})
|
||||
print(f"🤖 {reply[:80]}")
|
||||
|
||||
await websocket.send(json.dumps({
|
||||
"type": "response",
|
||||
"content": reply,
|
||||
}))
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ API error: {e}")
|
||||
await websocket.send(json.dumps({
|
||||
"type": "error",
|
||||
"content": f"Erreur API: {str(e)[:200]}",
|
||||
}))
|
||||
|
||||
except Exception as e:
|
||||
print(f"👋 Déconnecté: {e}")
|
||||
finally:
|
||||
await client.aclose()
|
||||
|
||||
|
||||
# ─── Main ────────────────────────────────────────────────────
|
||||
async def 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"🚀 JARVIS v3 PoC → http://0.0.0.0:{PORT}")
|
||||
print(f" WebSocket → ws://0.0.0.0:{PORT}/ws")
|
||||
|
||||
async with serve(ws_handler, "0.0.0.0", PORT, process_request=process_request):
|
||||
print(f"✅ Serveur prêt sur le port {PORT}")
|
||||
await asyncio.get_running_loop().create_future() # run forever
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,16 @@
|
||||
from pipecat.processors.aggregators.llm_context import OpenAIContextAggregatorPair
|
||||
import inspect
|
||||
|
||||
print("OpenAIContextAggregatorPair:")
|
||||
for name, method in inspect.getmembers(OpenAIContextAggregatorPair, inspect.isfunction):
|
||||
if not name.startswith("_"):
|
||||
sig = inspect.signature(method)
|
||||
print(f" {name}{sig}")
|
||||
|
||||
print()
|
||||
# Also check what other classes are exported
|
||||
import pipecat.processors.aggregators.llm_context as m
|
||||
print("All public symbols:")
|
||||
for x in dir(m):
|
||||
if not x.startswith("_"):
|
||||
print(f" {x}")
|
||||
@@ -0,0 +1,11 @@
|
||||
import pipecat.processors.aggregators.llm_context as m
|
||||
import inspect
|
||||
|
||||
print("Has OpenAILLMContext:", hasattr(m, "OpenAILLMContext"))
|
||||
print("All OpenAI classes:", [x for x in dir(m) if "OpenAI" in x or "Context" in x])
|
||||
|
||||
# Try to import from the module's namespace
|
||||
for name in dir(m):
|
||||
if name.startswith("OpenAI"):
|
||||
cls = getattr(m, name)
|
||||
print(f" {name}: {type(cls)}")
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Quick test: start bot, hit WS, see result."""
|
||||
import subprocess, os, json, time, sys, asyncio, threading, signal
|
||||
|
||||
os.chdir("/home/openclaw/workspace/jarvis-pipecat")
|
||||
|
||||
# Get key
|
||||
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)
|
||||
os.environ["HERMES_API_KEY"] = r.stdout.strip()
|
||||
os.environ["PYTHONUNBUFFERED"] = "1"
|
||||
|
||||
# Import bot (this triggers the key check print)
|
||||
import bot
|
||||
|
||||
# Start uvicorn in thread
|
||||
import uvicorn
|
||||
def run_server():
|
||||
uvicorn.run(bot.app, host="0.0.0.0", port=7081, log_level="info")
|
||||
|
||||
t = threading.Thread(target=run_server, daemon=True)
|
||||
t.start()
|
||||
|
||||
# Wait for server
|
||||
for i in range(20):
|
||||
try:
|
||||
import urllib.request
|
||||
urllib.request.urlopen("http://localhost:7081/", timeout=1)
|
||||
break
|
||||
except:
|
||||
time.sleep(0.3)
|
||||
else:
|
||||
print("FAIL: Server didn't start")
|
||||
sys.exit(1)
|
||||
|
||||
# Test WS
|
||||
async def test():
|
||||
import websockets
|
||||
try:
|
||||
async with websockets.connect("ws://localhost:7081/ws", open_timeout=5) as ws:
|
||||
msg = await asyncio.wait_for(ws.recv(), timeout=5)
|
||||
data = json.loads(msg)
|
||||
print(f"WELCOME: {data.get('message', msg)[:120]}")
|
||||
|
||||
await ws.send("Bonjour! Quelle heure est-il?")
|
||||
print("SENT question")
|
||||
|
||||
resp = await asyncio.wait_for(ws.recv(), timeout=20)
|
||||
data2 = json.loads(resp)
|
||||
content = data2.get("content", str(data2))
|
||||
print(f"REPLY: {content[:300]}")
|
||||
print("\n✅✅✅ SUCCESS! JARVIS v3 works! ✅✅✅")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"WS FAIL: {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
result = asyncio.run(test())
|
||||
sys.exit(0 if result else 1)
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Starts JARVIS v3 bot with Hermes API key from Infisical."""
|
||||
import json, subprocess, os, sys
|
||||
|
||||
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=20,
|
||||
)
|
||||
hermes_key = r.stdout.strip()
|
||||
if not hermes_key:
|
||||
print(f"ERROR: {r.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Key: {hermes_key[:15]}...")
|
||||
os.chdir("/home/openclaw/workspace/jarvis-pipecat")
|
||||
os.environ["HERMES_API_KEY"] = hermes_key
|
||||
os.environ["PYTHONUNBUFFERED"] = "1"
|
||||
# PAS de PIPECAT_ALLOWED_ORIGINS = laisse le défaut (liste vide = tout autorisé)
|
||||
if "PIPECAT_ALLOWED_ORIGINS" in os.environ:
|
||||
del os.environ["PIPECAT_ALLOWED_ORIGINS"]
|
||||
os.execvp("./venv/bin/python", ["./venv/bin/python", "bot.py"])
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
pkill -f "bot.py" 2>/dev/null
|
||||
sleep 1
|
||||
|
||||
TOKEN=$(python3 -c "import json; print(json.load(open('/opt/infisical/admin-credentials.json'))['token'])")
|
||||
HERMES_KEY=$(/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 "$TOKEN" 2>/dev/null)
|
||||
|
||||
export HERMES_API_KEY="$HERMES_KEY"
|
||||
cd /home/openclaw/workspace/jarvis-pipecat
|
||||
exec ./venv/bin/python bot.py
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Launcher pour JARVIS v3 Phase 1 (direct WebSocket)."""
|
||||
import json, subprocess, os, sys
|
||||
|
||||
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=20,
|
||||
)
|
||||
hermes_key = r.stdout.strip()
|
||||
if not hermes_key:
|
||||
print(f"ERROR: {r.stderr}", file=sys.stderr); sys.exit(1)
|
||||
|
||||
os.chdir("/home/openclaw/workspace/jarvis-pipecat")
|
||||
os.environ["HERMES_API_KEY"] = hermes_key
|
||||
os.execvp("./venv/bin/python", ["./venv/bin/python", "bot_direct.py"])
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Script de test complet pour JARVIS v3 Pipecat."""
|
||||
import subprocess, os, json, time, sys, asyncio
|
||||
|
||||
# 1. Récupérer la clé API Hermes depuis Infisical
|
||||
print("🔑 Récupération clé API Hermes...")
|
||||
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=20,
|
||||
env={"PATH": "/usr/local/bin:/usr/bin:/bin"}
|
||||
)
|
||||
hermes_key = r.stdout.strip()
|
||||
if not hermes_key:
|
||||
print(f"❌ Erreur Infisical: {r.stderr}")
|
||||
sys.exit(1)
|
||||
print(f"✅ Clé: {hermes_key[:15]}...")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 2. Lancer le bot
|
||||
print("\n🚀 Lancement du bot Pipecat...")
|
||||
os.environ["HERMES_API_KEY"] = hermes_key
|
||||
proc = subprocess.Popen(
|
||||
["./venv/bin/python", "bot.py"],
|
||||
cwd="/home/openclaw/workspace/jarvis-pipecat",
|
||||
env={**os.environ, "HERMES_API_KEY": hermes_key},
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, bufsize=1
|
||||
)
|
||||
|
||||
# 3. Attendre que le serveur démarre
|
||||
print("⏳ Attente démarrage serveur...")
|
||||
import select
|
||||
start = time.time()
|
||||
while time.time() - start < 15:
|
||||
ready, _, _ = select.select([proc.stdout], [], [], 0.5)
|
||||
if ready:
|
||||
line = proc.stdout.readline()
|
||||
if line:
|
||||
print(f" {line.rstrip()}")
|
||||
if "Uvicorn running" in line or "Application startup complete" in line:
|
||||
break
|
||||
# Vérifier si le port écoute
|
||||
try:
|
||||
import socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(1)
|
||||
s.connect(("localhost", 7081))
|
||||
s.close()
|
||||
print("✅ Port 7081 ouvert!")
|
||||
break
|
||||
except:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
|
||||
if proc.poll() is not None:
|
||||
print("❌ Le bot a crashé au démarrage!")
|
||||
sys.exit(1)
|
||||
|
||||
print("✅ Bot démarré!")
|
||||
|
||||
# 4. Test HTTP
|
||||
print("\n🌐 Test HTTP GET /")
|
||||
import urllib.request
|
||||
resp = urllib.request.urlopen("http://localhost:7081/", timeout=5)
|
||||
content = resp.read().decode()
|
||||
if "JARVIS" in content:
|
||||
print("✅ Page HTML OK")
|
||||
else:
|
||||
print(f"⚠️ Page: {content[:100]}")
|
||||
|
||||
# 5. Test WebSocket
|
||||
print("\n🔌 Test WebSocket...")
|
||||
try:
|
||||
import websockets
|
||||
except ImportError:
|
||||
subprocess.run(["./venv/bin/pip", "install", "websockets"], check=True)
|
||||
import websockets
|
||||
|
||||
async def ws_test():
|
||||
async with websockets.connect("ws://localhost:7081/ws") as ws:
|
||||
# Recevoir message de bienvenue
|
||||
msg = await asyncio.wait_for(ws.recv(), timeout=5)
|
||||
print(f"👋 Welcome: {msg[:150]}")
|
||||
|
||||
# Envoyer une question
|
||||
await ws.send("Quelle heure est-il actuellement?")
|
||||
print("📤 Question envoyée")
|
||||
|
||||
# Attendre la réponse
|
||||
try:
|
||||
msg = await asyncio.wait_for(ws.recv(), timeout=15)
|
||||
print(f"🤖 Réponse: {msg[:300]}")
|
||||
print("\n✅✅✅ TEST RÉUSSI! JARVIS v3 répond via Pipecat! ✅✅✅")
|
||||
except asyncio.TimeoutError:
|
||||
print("⚠️ Timeout: pas de réponse du LLM en 15s")
|
||||
print(" (le LLM peut prendre plus de temps)")
|
||||
|
||||
asyncio.run(ws_test())
|
||||
|
||||
print("\n📊 Serveur toujours en cours d'exécution...")
|
||||
print(f" 🌐 http://openclaw1.dev.home:7081/")
|
||||
print(f" 🔌 ws://openclaw1.dev.home:7081/ws")
|
||||
print(f" PID: {proc.pid}")
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# ~/workspace/jarvis-pipecat/start.sh
|
||||
# Lancement JARVIS v3 Pipecat
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Récupération clé API Hermes depuis Infisical
|
||||
echo "🔐 Récupération clé API Hermes..."
|
||||
HERMES_API_KEY=$(
|
||||
./venv/bin/python3 -c "
|
||||
import json, subprocess
|
||||
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'],
|
||||
env={'INFISICAL_TOKEN': creds['token'], 'PATH': '/usr/local/bin:/usr/bin:/bin'},
|
||||
capture_output=True, text=True, timeout=15,
|
||||
)
|
||||
print(r.stdout.strip())
|
||||
"
|
||||
)
|
||||
|
||||
if [ -z "$HERMES_API_KEY" ]; then
|
||||
echo "❌ Impossible de récupérer la clé API Hermes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export HERMES_API_KEY
|
||||
export JARVIS_PIPECAT_PORT="${JARVIS_PIPECAT_PORT:-7081}"
|
||||
# Désactiver la vérification d'origine pour le PoC (client websockets n'envoie pas d'Origin)
|
||||
export PIPECAT_ALLOWED_ORIGINS="http://localhost:7081,http://0.0.0.0:7081"
|
||||
|
||||
echo "🔑 Clé: ${HERMES_API_KEY:0:15}..."
|
||||
echo "🚀 JARVIS v3 Pipecat → http://0.0.0.0:${JARVIS_PIPECAT_PORT}"
|
||||
exec ./venv/bin/python3 bot.py
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
from pipecat.processors.aggregators.llm_context import (
|
||||
LLMUserContextAggregator,
|
||||
LLMAssistantContextAggregator,
|
||||
)
|
||||
import inspect
|
||||
|
||||
print("=== LLMUserContextAggregator ===")
|
||||
sig = inspect.signature(LLMUserContextAggregator.__init__)
|
||||
for n, p in sig.parameters.items():
|
||||
if n == 'self': continue
|
||||
default = "REQUIRED" if p.default is inspect.Parameter.empty else str(p.default)[:60]
|
||||
print(f" {n}: {default}")
|
||||
|
||||
print()
|
||||
print("=== LLMAssistantContextAggregator ===")
|
||||
sig2 = inspect.signature(LLMAssistantContextAggregator.__init__)
|
||||
for n, p in sig2.parameters.items():
|
||||
if n == 'self': continue
|
||||
default = "REQUIRED" if p.default is inspect.Parameter.empty else str(p.default)[:60]
|
||||
print(f" {n}: {default}")
|
||||
|
||||
# Check if they need a context object
|
||||
print()
|
||||
print("=== LLMUserContextAggregator base classes ===")
|
||||
for cls in LLMUserContextAggregator.__mro__:
|
||||
print(f" {cls.__name__}")
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
# Test all imports needed for the bot
|
||||
from pipecat.frames.frames import OutputTransportMessageFrame
|
||||
print("✅ OutputTransportMessageFrame")
|
||||
|
||||
from pipecat.pipeline.pipeline import Pipeline
|
||||
print("✅ Pipeline")
|
||||
|
||||
from pipecat.pipeline.runner import PipelineRunner
|
||||
print("✅ PipelineRunner")
|
||||
|
||||
from pipecat.pipeline.task import PipelineTask
|
||||
print("✅ PipelineTask")
|
||||
|
||||
from pipecat.processors.aggregators.llm_context import OpenAILLMContext
|
||||
print("✅ OpenAILLMContext")
|
||||
|
||||
from pipecat.services.openai.llm import OpenAILLMService
|
||||
print("✅ OpenAILLMService")
|
||||
|
||||
from pipecat.transports.websocket.fastapi import (
|
||||
FastAPIWebsocketParams,
|
||||
FastAPIWebsocketTransport,
|
||||
)
|
||||
print("✅ FastAPIWebsocketTransport")
|
||||
|
||||
# Test create_context_aggregator
|
||||
import inspect
|
||||
if hasattr(OpenAILLMService, 'create_context_aggregator'):
|
||||
print("✅ create_context_aggregator exists")
|
||||
else:
|
||||
print("❌ create_context_aggregator NOT found - checking parent...")
|
||||
for cls in OpenAILLMService.__mro__:
|
||||
if hasattr(cls, 'create_context_aggregator') and 'create_context_aggregator' in cls.__dict__:
|
||||
print(f" Found in {cls.__name__}")
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
|
||||
import pipecat.processors.aggregators.llm_context as ctx_mod
|
||||
import inspect
|
||||
|
||||
# Check OpenAIContextAggregatorPair
|
||||
print("=== OpenAIContextAggregatorPair ===")
|
||||
cls = ctx_mod.OpenAIContextAggregatorPair
|
||||
sig = inspect.signature(cls.__init__)
|
||||
print(f"__init__ params: {list(sig.parameters.keys())}")
|
||||
|
||||
# Check OpenAILLMContextAggregatorPair
|
||||
print()
|
||||
print("=== OpenAILLMContextAggregatorPair ===")
|
||||
cls2 = ctx_mod.OpenAILLMContextAggregatorPair
|
||||
sig2 = inspect.signature(cls2.__init__)
|
||||
print(f"__init__ params: {list(sig2.parameters.keys())}")
|
||||
|
||||
# Check methods
|
||||
print()
|
||||
print("=== OpenAIContextAggregatorPair methods ===")
|
||||
for name in dir(ctx_mod.OpenAIContextAggregatorPair):
|
||||
if not name.startswith('_'):
|
||||
print(f" {name}")
|
||||
|
||||
# Check LLMUserContextAggregator
|
||||
print()
|
||||
print("=== LLMUserContextAggregator __init__ ===")
|
||||
cls3 = ctx_mod.LLMUserContextAggregator
|
||||
sig3 = inspect.signature(cls3.__init__)
|
||||
print(f"params: {list(sig3.parameters.keys())}")
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test complet JARVIS v3 Pipecat : démarre le bot, teste WebSocket."""
|
||||
import subprocess, os, json, time, sys, asyncio, signal
|
||||
|
||||
# 1. Kill existing bots
|
||||
print("🧹 Nettoyage...")
|
||||
subprocess.run(["pkill", "-f", "bot.py"], capture_output=True)
|
||||
time.sleep(0.5)
|
||||
|
||||
# 2. Get Hermes key
|
||||
print("🔑 Récupération clé API...")
|
||||
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=20,
|
||||
env={"PATH": "/usr/local/bin:/usr/bin:/bin"}
|
||||
)
|
||||
hermes_key = r.stdout.strip()
|
||||
if not hermes_key:
|
||||
print(f"❌ Erreur Infisical: {r.stderr}")
|
||||
sys.exit(1)
|
||||
print(f"✅ Clé: {hermes_key[:12]}...")
|
||||
|
||||
# 3. Start bot
|
||||
print("🚀 Démarrage du bot...")
|
||||
os.chdir("/home/openclaw/workspace/jarvis-pipecat")
|
||||
bot_env = os.environ.copy()
|
||||
bot_env["HERMES_API_KEY"] = hermes_key
|
||||
bot_env["PYTHONUNBUFFERED"] = "1"
|
||||
|
||||
bot_proc = subprocess.Popen(
|
||||
["./venv/bin/python", "bot.py"],
|
||||
env=bot_env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, bufsize=1
|
||||
)
|
||||
|
||||
# 4. Wait for server
|
||||
print("⏳ Attente démarrage...")
|
||||
import select, socket
|
||||
start = time.time()
|
||||
ready = False
|
||||
while time.time() - start < 15:
|
||||
# Check port
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(1)
|
||||
s.connect(("localhost", 7081))
|
||||
s.close()
|
||||
ready = True
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check for crash
|
||||
if bot_proc.poll() is not None:
|
||||
out = bot_proc.stdout.read() if bot_proc.stdout else ""
|
||||
print(f"❌ Bot crashed!")
|
||||
print(f"Output: {out[-500:]}")
|
||||
sys.exit(1)
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
if not ready:
|
||||
print("❌ Timeout: serveur pas prêt")
|
||||
bot_proc.kill()
|
||||
sys.exit(1)
|
||||
|
||||
print("✅ Port 7081 ouvert!")
|
||||
|
||||
# Allow extra startup time for pipeline init
|
||||
time.sleep(2)
|
||||
|
||||
# 5. Test WS
|
||||
print("🔌 Test WebSocket...")
|
||||
try:
|
||||
import websockets
|
||||
except ImportError:
|
||||
subprocess.run(["./venv/bin/pip", "install", "websockets"], check=True)
|
||||
import websockets
|
||||
|
||||
async def test_ws():
|
||||
uri = "ws://localhost:7081/ws"
|
||||
async with websockets.connect(uri, open_timeout=10) as ws:
|
||||
# Welcome message
|
||||
msg = await asyncio.wait_for(ws.recv(), timeout=10)
|
||||
try:
|
||||
data = json.loads(msg)
|
||||
print(f"👋 Welcome: {data.get('message', data.get('content', msg))[:120]}")
|
||||
except:
|
||||
print(f"👋 Welcome (raw): {msg[:120]}")
|
||||
|
||||
# Send question
|
||||
await ws.send("Bonjour JARVIS, quelle heure est-il actuellement?")
|
||||
print("📤 Question envoyée")
|
||||
|
||||
# Wait for response
|
||||
for i in range(3):
|
||||
try:
|
||||
resp = await asyncio.wait_for(ws.recv(), timeout=30)
|
||||
try:
|
||||
data = json.loads(resp)
|
||||
print(f"🤖 Réponse [{i+1}]: {json.dumps(data, ensure_ascii=False)[:300]}")
|
||||
if data.get("content"):
|
||||
print("\n✅✅✅ SUCCÈS! JARVIS v3 répond via Pipecat! ✅✅✅")
|
||||
return True
|
||||
except:
|
||||
print(f"🤖 Raw [{i+1}]: {resp[:200]}")
|
||||
except asyncio.TimeoutError:
|
||||
print(f"⏰ Timeout - pas de réponse après 30s (message {i+1})")
|
||||
break
|
||||
return False
|
||||
|
||||
success = asyncio.run(test_ws())
|
||||
|
||||
# 6. Report
|
||||
print(f"\n{'='*50}")
|
||||
print(f"🌐 URL: http://openclaw1.dev.home:7081/")
|
||||
print(f"🔌 WS: ws://openclaw1.dev.home:7081/ws")
|
||||
print(f"📋 Bot PID: {bot_proc.pid}")
|
||||
if success:
|
||||
print("✅ TEST RÉUSSI!")
|
||||
else:
|
||||
print("⚠️ Test partiel - vérifie le navigateur")
|
||||
|
||||
# Kill bot (clean exit)
|
||||
bot_proc.send_signal(signal.SIGTERM)
|
||||
try:
|
||||
bot_proc.wait(timeout=5)
|
||||
except:
|
||||
bot_proc.kill()
|
||||
print("🛑 Bot arrêté")
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
|
||||
import pipecat.processors.aggregators.llm_context as ctx_mod
|
||||
print("Module imported OK")
|
||||
print("Public symbols:", [x for x in dir(ctx_mod) if not x.startswith('_')])
|
||||
|
||||
# Try to access classes
|
||||
for name in ['OpenAILLMContext', 'OpenAILLMContextAggregatorPair', 'LLMContext']:
|
||||
if hasattr(ctx_mod, name):
|
||||
cls = getattr(ctx_mod, name)
|
||||
print(f' {name}: {cls}')
|
||||
else:
|
||||
print(f' {name}: NOT FOUND')
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
from pipecat.services.openai.llm import OpenAILLMService
|
||||
import inspect
|
||||
|
||||
# Get the create_context_aggregator method
|
||||
method = getattr(OpenAILLMService, 'create_context_aggregator', None)
|
||||
if method:
|
||||
print("Method found!")
|
||||
print(f"Type: {type(method)}")
|
||||
sig = inspect.signature(method)
|
||||
print(f"Signature: {sig}")
|
||||
# Try to see what params it takes
|
||||
for name, param in sig.parameters.items():
|
||||
print(f" {name}: {param.annotation if param.annotation is not inspect.Parameter.empty else 'Any'} = {param.default if param.default is not inspect.Parameter.empty else 'REQUIRED'}")
|
||||
else:
|
||||
print("Method NOT found!")
|
||||
|
||||
# Check if it's created by a metaclass or __init_subclass__
|
||||
for cls in OpenAILLMService.__mro__:
|
||||
if hasattr(cls, 'create_context_aggregator'):
|
||||
print(f" Found in {cls.__name__}")
|
||||
m = getattr(cls, 'create_context_aggregator')
|
||||
print(f" Type: {type(m)}")
|
||||
@@ -0,0 +1,17 @@
|
||||
import pipecat.processors.aggregators.llm_context as m
|
||||
|
||||
# Access directly from __dict__
|
||||
user_cls = m.__dict__.get('LLMUserContextAggregator')
|
||||
asst_cls = m.__dict__.get('LLMAssistantContextAggregator')
|
||||
|
||||
print(f"LLMUserContextAggregator: {user_cls}")
|
||||
print(f"LLMAssistantContextAggregator: {asst_cls}")
|
||||
|
||||
if user_cls:
|
||||
import inspect
|
||||
sig = inspect.signature(user_cls.__init__)
|
||||
print(f"User params: {list(sig.parameters.keys())}")
|
||||
|
||||
if asst_cls:
|
||||
sig = inspect.signature(asst_cls.__init__)
|
||||
print(f"Asst params: {list(sig.parameters.keys())}")
|
||||
@@ -0,0 +1,13 @@
|
||||
import pipecat.frames.frames as fm
|
||||
|
||||
# Try module attribute access
|
||||
try:
|
||||
cls = fm.LLMMessagesFrame
|
||||
print(f"LLMMessagesFrame via module: {cls}")
|
||||
except AttributeError as e:
|
||||
print(f"ERROR via module: {e}")
|
||||
|
||||
# Try __dict__ access
|
||||
for key in sorted(fm.__dict__.keys()):
|
||||
if 'LLM' in key and 'Frame' in key:
|
||||
print(f" fm.{key}: {type(fm.__dict__[key]).__name__}")
|
||||
@@ -0,0 +1,8 @@
|
||||
import pipecat.processors.aggregators.llm_context as m
|
||||
try:
|
||||
Ctx = m.OpenAILLMContext
|
||||
print(f"OpenAILLMContext: {Ctx}")
|
||||
except Exception as e:
|
||||
print(f"ERROR: {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@@ -0,0 +1,6 @@
|
||||
import traceback
|
||||
try:
|
||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||
print("SUCCESS:", OpenAILLMContext)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
import traceback, sys
|
||||
tests = [
|
||||
("pipecat.pipeline.pipeline", "Pipeline"),
|
||||
("pipecat.pipeline.runner", "PipelineRunner"),
|
||||
("pipecat.pipeline.task", "PipelineTask"),
|
||||
("pipecat.processors.aggregators.llm_context", "OpenAILLMContext"),
|
||||
("pipecat.services.openai.llm", "OpenAILLMService"),
|
||||
("pipecat.transports.websocket.fastapi", "FastAPIWebsocketTransport"),
|
||||
("pipecat.transports.websocket.fastapi", "FastAPIWebsocketParams"),
|
||||
("pipecat.frames.frames", "OutputTransportMessageFrame"),
|
||||
]
|
||||
for mod, cls in tests:
|
||||
try:
|
||||
m = __import__(mod, fromlist=[cls])
|
||||
getattr(m, cls)
|
||||
print(f" ✅ {mod}.{cls}")
|
||||
except Exception as e:
|
||||
print(f" ❌ {mod}.{cls}: {e}")
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
import sys
|
||||
import pipecat.processors.aggregators.llm_context as m
|
||||
|
||||
# Try to trigger __getattr__ if it's lazy
|
||||
print("Module:", m)
|
||||
print("Module file:", getattr(m, '__file__', 'none'))
|
||||
|
||||
# Check if __getattr__ is defined
|
||||
print("Has __getattr__:", hasattr(m, '__getattr__'))
|
||||
|
||||
# Try to access via module __dict__
|
||||
for key in sorted(m.__dict__.keys()):
|
||||
if not key.startswith('_'):
|
||||
print(f" m.{key}: {type(m.__dict__[key]).__name__}")
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
|
||||
from pipecat.services.openai.llm import OpenAILLMService
|
||||
import inspect
|
||||
|
||||
# Check if create_context_aggregator exists
|
||||
print("Has create_context_aggregator:", hasattr(OpenAILLMService, "create_context_aggregator"))
|
||||
|
||||
# Check all methods
|
||||
methods = [m for m in dir(OpenAILLMService) if not m.startswith('_')]
|
||||
print("Methods:", methods)
|
||||
|
||||
# Check the class signature
|
||||
sig = inspect.signature(OpenAILLMService.__init__)
|
||||
print()
|
||||
print("__init__ params:")
|
||||
for name, param in sig.parameters.items():
|
||||
if name not in ('self', 'kwargs'):
|
||||
default = "REQUIRED" if param.default is inspect.Parameter.empty else str(param.default)[:50]
|
||||
print(f" {name}: {default}")
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
# Try importing LLMContextAggregatorPair
|
||||
from pipecat.processors.aggregators.llm_context import LLMContextAggregatorPair
|
||||
print("LLMContextAggregatorPair imported OK!")
|
||||
import inspect
|
||||
sig = inspect.signature(LLMContextAggregatorPair.__init__)
|
||||
print("__init__ params:", list(sig.parameters.keys()))
|
||||
|
||||
# Check if it has user() and assistant() methods
|
||||
print()
|
||||
print("Methods:", [m for m in dir(LLMContextAggregatorPair) if not m.startswith('_')])
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
import pipecat.processors.aggregators.llm_context as m
|
||||
|
||||
# Access via module attribute
|
||||
try:
|
||||
UserCtx = m.LLMUserContextAggregator
|
||||
print(f"LLMUserContextAggregator accessed: {UserCtx}")
|
||||
except AttributeError as e:
|
||||
print(f"ERROR accessing LLMUserContextAggregator: {e}")
|
||||
|
||||
try:
|
||||
AsstCtx = m.LLMAssistantContextAggregator
|
||||
print(f"LLMAssistantContextAggregator accessed: {AsstCtx}")
|
||||
except AttributeError as e:
|
||||
print(f"ERROR accessing LLMAssistantContextAggregator: {e}")
|
||||
|
||||
# Try the module's __getattr__
|
||||
print()
|
||||
print("Testing __getattr__...")
|
||||
try:
|
||||
x = m.OpenAILLMContext
|
||||
print(f"OpenAILLMContext: {x}")
|
||||
except Exception as e:
|
||||
print(f"OpenAILLMContext ERROR: {type(e).__name__}: {e}")
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
# Test 1: Access via module getattr
|
||||
import pipecat.processors.aggregators.llm_context as m
|
||||
try:
|
||||
Ctx = m.OpenAILLMContext
|
||||
print(f"✅ OpenAILLMContext accessed: {Ctx}")
|
||||
except Exception as e:
|
||||
print(f"❌ OpenAILLMContext: {e}")
|
||||
|
||||
# Test 2: from import
|
||||
try:
|
||||
from pipecat.processors.aggregators.llm_context import OpenAILLMContext
|
||||
print("✅ from-import works!")
|
||||
except ImportError as e:
|
||||
print(f"❌ from-import: {e}")
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
import sys
|
||||
|
||||
async def test():
|
||||
print("🔌 Connexion WebSocket à ws://localhost:7081/ws...")
|
||||
try:
|
||||
async with websockets.connect('ws://localhost:7081/ws', open_timeout=10) as ws:
|
||||
print("✅ Connecté!")
|
||||
|
||||
# Receive welcome message
|
||||
try:
|
||||
msg = await asyncio.wait_for(ws.recv(), timeout=15)
|
||||
print(f"📩 Message de bienvenue: {msg[:300]}")
|
||||
except asyncio.TimeoutError:
|
||||
print("⚠️ Pas de message de bienvenue (normal si pas d'event handler)")
|
||||
|
||||
# Send a question
|
||||
print("📤 Envoi: 'Quelle heure est-il ?'")
|
||||
await ws.send("Quelle heure est-il ?")
|
||||
|
||||
# Get response
|
||||
try:
|
||||
response = await asyncio.wait_for(ws.recv(), timeout=60)
|
||||
print(f"📩 Réponse: {response[:500]}")
|
||||
except asyncio.TimeoutError:
|
||||
print("❌ Timeout: pas de réponse du LLM en 60s")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur: {e}")
|
||||
|
||||
asyncio.run(test())
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
import time
|
||||
|
||||
async def test():
|
||||
t0 = time.time()
|
||||
print("🔌 Connexion WebSocket à ws://localhost:7081/ws...")
|
||||
try:
|
||||
async with websockets.connect('ws://localhost:7081/ws', open_timeout=10) as ws:
|
||||
t_connect = time.time() - t0
|
||||
print(f"✅ Connecté en {t_connect:.1f}s!")
|
||||
|
||||
# Receive welcome message
|
||||
try:
|
||||
msg = await asyncio.wait_for(ws.recv(), timeout=15)
|
||||
print(f"📩 Bienvenue: {msg[:200]}")
|
||||
except asyncio.TimeoutError:
|
||||
print("⚠️ Pas de message de bienvenue")
|
||||
|
||||
# Send a question
|
||||
t1 = time.time()
|
||||
print("📤 Envoi: 'Quelle heure est-il ?'")
|
||||
await ws.send("Quelle heure est-il ?")
|
||||
|
||||
# Get response
|
||||
try:
|
||||
response = await asyncio.wait_for(ws.recv(), timeout=90)
|
||||
t_resp = time.time() - t1
|
||||
print(f"📩 Réponse ({t_resp:.1f}s): {response[:500]}")
|
||||
except asyncio.TimeoutError:
|
||||
print("❌ Timeout: pas de réponse du LLM en 90s")
|
||||
|
||||
# Send second question
|
||||
t2 = time.time()
|
||||
print("📤 Envoi: 'Quel temps fait-il aujourd''hui ?'")
|
||||
await ws.send("Quel temps fait-il aujourd'hui ?")
|
||||
|
||||
try:
|
||||
response2 = await asyncio.wait_for(ws.recv(), timeout=90)
|
||||
t_resp2 = time.time() - t2
|
||||
print(f"📩 Réponse 2 ({t_resp2:.1f}s): {response2[:500]}")
|
||||
except asyncio.TimeoutError:
|
||||
print("❌ Timeout: pas de réponse 2 en 90s")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur: {type(e).__name__}: {e}")
|
||||
|
||||
asyncio.run(test())
|
||||
Reference in New Issue
Block a user