feat: connect to beaver

This commit is contained in:
0Xiao0
2026-06-03 17:25:08 +08:00
parent 0a50f25dfa
commit 52e6d3cd9c
3 changed files with 47 additions and 32 deletions

View File

@ -5,17 +5,10 @@ LIVEKIT_API_SECRET=
CUSTOM_AGENT_NAME=my-agent
# Beaver terminal text WebSocket
BEAVER_WS_URL=ws://127.0.0.1:8080/api/channels/terminal-dev/ws
BEAVER_WS_URL=ws://terminaltest.1localhost.nip.io:8088/api/channels/terminal-dev/ws
TERMINAL_PEER_ID=device-001
TERMINAL_DEVICE_NAME=desk-terminal
# Beaver as a LiveKit LLM backend: asr -> beaver -> tts.
# CUSTOM_LLM_PROVIDER=beaver
# CUSTOM_BEAVER_WS_URL=ws://127.0.0.1:8080/api/channels/terminal-dev/ws
# CUSTOM_BEAVER_PEER_ID=livekit-agent-001
# CUSTOM_BEAVER_DEVICE_NAME=livekit-custom-agent
# CUSTOM_BEAVER_MODEL=beaver-terminal
# ASR blackbox
CUSTOM_ASR_URL=http://localhost:5000/asr-blackbox
CUSTOM_ASR_MODEL=qwen
@ -26,35 +19,27 @@ CUSTOM_ASR_ITN=
CUSTOM_ASR_CHUNK_MODE=
# LLM backend: openai/openai-compatible or hermes_gateway/openclaw.
CUSTOM_LLM_PROVIDER=openai
CUSTOM_LLM_PROVIDER=beaver
CUSTOM_BEAVER_WARMUP_TEXT=初始化连接,请简短回复 ready
# OpenAI-compatible LLM
# CUSTOM_LLM_BASE_URL=https://oai.bwgdi.com/v1
# CUSTOM_LLM_MODEL=Qwen3.6-35B
# CUSTOM_LLM_API_KEY=
# CUSTOM_LLM_API_KEY=sk-
# CUSTOM_LLM_VERIFY_SSL=false
CUSTOM_LLM_BASE_URL=http://localhost/v1
CUSTOM_LLM_MODEL=Qwen-VL
CUSTOM_LLM_API_KEY=
CUSTOM_LLM_BASE_URL=http:/localhost/v1
CUSTOM_LLM_MODEL=Mistral-Medium-3.5-128B
CUSTOM_LLM_API_KEY=sk-
CUSTOM_LLM_VERIFY_SSL=false
CUSTOM_SAVE_MODEL_IMAGES=false
CUSTOM_SAVE_MODEL_IMAGES=true
# CUSTOM_TEXT_LLM_MODEL=
# CUSTOM_VISION_LLM_MODEL=
# Hermes Agent via OpenClaw Gateway WebSocket, one Gateway session per LiveKit room.
# CUSTOM_LLM_PROVIDER=hermes_gateway
# CUSTOM_HERMES_GATEWAY_URL=ws://localhost:1977/ws
# CUSTOM_HERMES_AGENT_ID=
# CUSTOM_HERMES_API_KEY=
# CUSTOM_HERMES_SESSION_MODE=per_room
# CUSTOM_HERMES_MODEL=hermes-agent
# CUSTOM_HERMES_REQUEST_TIMEOUT=30
# CUSTOM_LLM_BASE_URL=https://api.deepseek.com
# CUSTOM_LLM_MODEL=deepseek-v4-flash
# CUSTOM_LLM_API_KEY=
# CUSTOM_LLM_API_KEY=sk-
# CUSTOM_LLM_VERIFY_SSL=false
@ -95,4 +80,4 @@ CUSTOM_MEMORY_URL=http://localhost:8766/api/room_graph
CUSTOM_MEMORY_TIMEOUT=2
CUSTOM_MEMORY_MAX_CHARS=2000
CUSTOM_MEMORY_API_KEY=
CUSTOM_PREEMPTIVE_GENERATION=true
CUSTOM_PREEMPTIVE_GENERATION=false

View File

@ -1,10 +1,14 @@
from __future__ import annotations
import asyncio
import logging
from collections.abc import Sequence
from typing import Any
from beaver_terminal_client import BeaverTerminalClient
try:
from beaver_terminal_client import BeaverTerminalClient
except ModuleNotFoundError:
from custom.beaver_terminal_client import BeaverTerminalClient
from livekit.agents import llm
from livekit.agents.types import (
DEFAULT_API_CONNECT_OPTIONS,
@ -14,6 +18,9 @@ from livekit.agents.types import (
)
from livekit.agents.utils import shortuuid
logger = logging.getLogger("beaver-llm")
def latest_user_text(chat_ctx: llm.ChatContext) -> str:
for message in reversed(chat_ctx.messages()):
if message.role != "user":
@ -49,6 +56,27 @@ class BeaverLLM(llm.LLM):
def provider(self) -> str:
return "beaver"
@property
def session_id(self) -> str | None:
return self._client.session_id
async def connect(self, *, warmup_text: str | None = None) -> str | None:
warmup_reply: str | None = None
async with self._lock:
await self._client.connect()
if warmup_text and warmup_text.strip():
warmup_reply = await self._client.send_text(warmup_text.strip())
if warmup_reply is None:
logger.info("Beaver handshake completed session_id=%s", self.session_id)
else:
logger.info(
"Beaver handshake warmup completed session_id=%s reply_len=%s",
self.session_id,
len(warmup_reply),
)
return warmup_reply
def chat(
self,
*,

View File

@ -690,10 +690,7 @@ async def entrypoint(ctx: JobContext) -> None:
if not beaver_url:
raise RuntimeError(f"CUSTOM_BEAVER_WS_URL or BEAVER_WS_URL is not set in {CUSTOM_ENV_PATH}")
beaver_peer_id = (
_first_env("CUSTOM_BEAVER_PEER_ID", "BEAVER_PEER_ID", "TERMINAL_PEER_ID")
or f"livekit-{ctx.room.name}"
)
beaver_peer_id = _first_env("CUSTOM_BEAVER_PEER_ID", "BEAVER_PEER_ID") or f"livekit-{ctx.room.name}"
beaver_device_name = (
_first_env("CUSTOM_BEAVER_DEVICE_NAME", "BEAVER_DEVICE_NAME", "TERMINAL_DEVICE_NAME")
or "livekit-custom-agent"
@ -704,14 +701,19 @@ async def entrypoint(ctx: JobContext) -> None:
device_name=beaver_device_name,
model_name=os.getenv("CUSTOM_BEAVER_MODEL", "beaver-terminal"),
)
beaver_warmup_text = os.getenv("CUSTOM_BEAVER_WARMUP_TEXT")
warmup_reply = await base_llm.connect(warmup_text=beaver_warmup_text)
text_llm = base_llm
vision_llm = base_llm
logger.info(
"Using Beaver gateway url=%s peer_id=%s device_name=%s room=%s",
"Using Beaver gateway url=%s peer_id=%s device_name=%s room=%s session_id=%s warmup=%s warmup_reply_len=%s",
beaver_url,
beaver_peer_id,
beaver_device_name,
ctx.room.name,
base_llm.session_id,
bool(beaver_warmup_text and beaver_warmup_text.strip()),
len(warmup_reply) if warmup_reply is not None else 0,
)
elif LLM_PROVIDER in {"hermes", "hermes_gateway", "openclaw"}:
gateway_url = os.getenv("CUSTOM_HERMES_GATEWAY_URL", "").strip()
@ -809,7 +811,7 @@ async def entrypoint(ctx: JobContext) -> None:
"false_interruption_timeout": 1.0,
},
),
preemptive_generation=_env_bool("CUSTOM_PREEMPTIVE_GENERATION", True),
preemptive_generation=_env_bool("CUSTOM_PREEMPTIVE_GENERATION", LLM_PROVIDER != "beaver"),
aec_warmup_duration=3.0,
tts_text_transforms=[
"filter_emoji",