Fix WellNuoLLM custom implementation for livekit-agents 1.3.11
- chat() must be synchronous, returning LLMStream directly - LLMStream.__init__() takes 4 params: llm, chat_ctx, tools, conn_options - _run() emits chunks via self._event_ch.send_nowait() - Added model and provider properties required by LLM base class Tested: STT (Deepgram Nova-2), TTS (Deepgram Aura Asteria), VAD (Silero) all working. WellNuo voice_ask API integration confirmed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cb0c83d82a
commit
dcdd06739d
@ -7,8 +7,8 @@ Uses WellNuo voice_ask API for LLM responses, Deepgram for STT/TTS
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
from livekit.agents import (
|
from livekit.agents import (
|
||||||
Agent,
|
Agent,
|
||||||
AgentSession,
|
AgentSession,
|
||||||
@ -19,7 +19,8 @@ from livekit.agents import (
|
|||||||
cli,
|
cli,
|
||||||
llm,
|
llm,
|
||||||
)
|
)
|
||||||
from livekit.plugins import deepgram, silero, noise_cancellation
|
from livekit.agents.types import DEFAULT_API_CONNECT_OPTIONS, APIConnectOptions
|
||||||
|
from livekit.plugins import deepgram, noise_cancellation, silero
|
||||||
|
|
||||||
logger = logging.getLogger("julia-ai")
|
logger = logging.getLogger("julia-ai")
|
||||||
|
|
||||||
@ -30,9 +31,6 @@ WELLNUO_PASSWORD = os.getenv("WELLNUO_PASSWORD", "anandk_8")
|
|||||||
# Hardcoded Ferdinand's deployment_id for testing
|
# Hardcoded Ferdinand's deployment_id for testing
|
||||||
DEPLOYMENT_ID = os.getenv("DEPLOYMENT_ID", "21")
|
DEPLOYMENT_ID = os.getenv("DEPLOYMENT_ID", "21")
|
||||||
|
|
||||||
# Julia's personality for voice synthesis
|
|
||||||
JULIA_GREETING = "Hello! I'm Julia, your AI care assistant. How can I help you today?"
|
|
||||||
|
|
||||||
|
|
||||||
class WellNuoLLM(llm.LLM):
|
class WellNuoLLM(llm.LLM):
|
||||||
"""Custom LLM that uses WellNuo voice_ask API."""
|
"""Custom LLM that uses WellNuo voice_ask API."""
|
||||||
@ -40,21 +38,28 @@ class WellNuoLLM(llm.LLM):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._token = None
|
self._token = None
|
||||||
self._session = None
|
self._model_name = "wellnuo-voice-ask"
|
||||||
|
|
||||||
async def _ensure_token(self):
|
@property
|
||||||
|
def model(self) -> str:
|
||||||
|
return self._model_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def provider(self) -> str:
|
||||||
|
return "wellnuo"
|
||||||
|
|
||||||
|
async def _ensure_token(self) -> str:
|
||||||
"""Get authentication token from WellNuo API."""
|
"""Get authentication token from WellNuo API."""
|
||||||
if self._token:
|
if self._token:
|
||||||
return self._token
|
return self._token
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
# Generate random nonce for request
|
|
||||||
nonce = str(random.randint(0, 999999))
|
nonce = str(random.randint(0, 999999))
|
||||||
data = {
|
data = {
|
||||||
"function": "credentials",
|
"function": "credentials",
|
||||||
"clientId": "001",
|
"clientId": "001",
|
||||||
"user_name": WELLNUO_USER,
|
"user_name": WELLNUO_USER,
|
||||||
"ps": WELLNUO_PASSWORD, # API expects 'ps' not 'password'
|
"ps": WELLNUO_PASSWORD,
|
||||||
"nonce": nonce,
|
"nonce": nonce,
|
||||||
}
|
}
|
||||||
async with session.post(WELLNUO_API_URL, data=data) as resp:
|
async with session.post(WELLNUO_API_URL, data=data) as resp:
|
||||||
@ -67,31 +72,13 @@ class WellNuoLLM(llm.LLM):
|
|||||||
logger.error(f"Failed to get WellNuo token: {result}")
|
logger.error(f"Failed to get WellNuo token: {result}")
|
||||||
raise Exception("Failed to authenticate with WellNuo API")
|
raise Exception("Failed to authenticate with WellNuo API")
|
||||||
|
|
||||||
async def chat(
|
async def get_response(self, user_message: str) -> str:
|
||||||
self,
|
"""Call WellNuo voice_ask API and return response."""
|
||||||
*,
|
|
||||||
chat_ctx: llm.ChatContext,
|
|
||||||
tools: list[llm.FunctionTool] | None = None,
|
|
||||||
tool_choice: llm.ToolChoice | None = None,
|
|
||||||
parallel_tool_calls: bool | None = None,
|
|
||||||
extra_body: dict | None = None,
|
|
||||||
) -> llm.LLMStream:
|
|
||||||
"""Send user question to WellNuo voice_ask API."""
|
|
||||||
# Get the last user message
|
|
||||||
user_message = ""
|
|
||||||
for msg in reversed(chat_ctx.items):
|
|
||||||
if hasattr(msg, 'role') and msg.role == "user":
|
|
||||||
if hasattr(msg, 'content'):
|
|
||||||
user_message = msg.content
|
|
||||||
break
|
|
||||||
|
|
||||||
if not user_message:
|
if not user_message:
|
||||||
# Return a default response if no user message
|
return "I'm here to help. What would you like to know?"
|
||||||
return WellNuoLLMStream("I'm here to help. What would you like to know?")
|
|
||||||
|
|
||||||
logger.info(f"User question: {user_message}")
|
logger.info(f"User question: {user_message}")
|
||||||
|
|
||||||
# Get response from WellNuo API
|
|
||||||
try:
|
try:
|
||||||
token = await self._ensure_token()
|
token = await self._ensure_token()
|
||||||
|
|
||||||
@ -110,49 +97,82 @@ class WellNuoLLM(llm.LLM):
|
|||||||
if result.get("ok"):
|
if result.get("ok"):
|
||||||
response_body = result.get("response", {}).get("body", "")
|
response_body = result.get("response", {}).get("body", "")
|
||||||
logger.info(f"WellNuo response: {response_body}")
|
logger.info(f"WellNuo response: {response_body}")
|
||||||
return WellNuoLLMStream(response_body)
|
return response_body
|
||||||
else:
|
else:
|
||||||
logger.error(f"WellNuo API error: {result}")
|
logger.error(f"WellNuo API error: {result}")
|
||||||
return WellNuoLLMStream("I'm sorry, I couldn't get that information right now.")
|
return "I'm sorry, I couldn't get that information right now."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error calling WellNuo API: {e}")
|
logger.error(f"Error calling WellNuo API: {e}")
|
||||||
return WellNuoLLMStream("I'm having trouble connecting. Please try again.")
|
return "I'm having trouble connecting. Please try again."
|
||||||
|
|
||||||
|
def chat(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
chat_ctx: llm.ChatContext,
|
||||||
|
tools: list[llm.Tool] | None = None,
|
||||||
|
conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
|
||||||
|
parallel_tool_calls=None,
|
||||||
|
tool_choice=None,
|
||||||
|
extra_kwargs=None,
|
||||||
|
) -> "WellNuoLLMStream":
|
||||||
|
"""Return an LLMStream for this chat request."""
|
||||||
|
return WellNuoLLMStream(
|
||||||
|
llm_instance=self,
|
||||||
|
chat_ctx=chat_ctx,
|
||||||
|
tools=tools or [],
|
||||||
|
conn_options=conn_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WellNuoLLMStream(llm.LLMStream):
|
class WellNuoLLMStream(llm.LLMStream):
|
||||||
"""Stream wrapper for WellNuo API response."""
|
"""Stream wrapper for WellNuo API response."""
|
||||||
|
|
||||||
def __init__(self, response_text: str):
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm_instance: WellNuoLLM,
|
||||||
|
chat_ctx: llm.ChatContext,
|
||||||
|
tools: list[llm.Tool],
|
||||||
|
conn_options: APIConnectOptions,
|
||||||
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
llm=None,
|
llm=llm_instance,
|
||||||
chat_ctx=llm.ChatContext(),
|
chat_ctx=chat_ctx,
|
||||||
tools=[],
|
tools=tools,
|
||||||
tool_choice=None,
|
conn_options=conn_options,
|
||||||
parallel_tool_calls=None,
|
|
||||||
extra_body=None,
|
|
||||||
)
|
)
|
||||||
self._response_text = response_text
|
self._wellnuo_llm = llm_instance
|
||||||
self._sent = False
|
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
"""Yield the response as a single chunk."""
|
"""Emit the response as chunks - called by the base class."""
|
||||||
pass
|
# Extract last user message from chat context
|
||||||
|
user_message = ""
|
||||||
|
for item in reversed(self._chat_ctx.items):
|
||||||
|
# ChatMessage has type="message", role, and text_content property
|
||||||
|
is_user_message = (
|
||||||
|
hasattr(item, "type")
|
||||||
|
and item.type == "message"
|
||||||
|
and hasattr(item, "role")
|
||||||
|
and item.role == "user"
|
||||||
|
)
|
||||||
|
if is_user_message:
|
||||||
|
# Use text_content property which handles list[ChatContent]
|
||||||
|
user_message = item.text_content or ""
|
||||||
|
break
|
||||||
|
|
||||||
async def __anext__(self) -> llm.ChatChunk:
|
# Get response from WellNuo API
|
||||||
if self._sent:
|
response_text = await self._wellnuo_llm.get_response(user_message)
|
||||||
raise StopAsyncIteration
|
|
||||||
|
|
||||||
self._sent = True
|
# Emit the response as a single chunk
|
||||||
return llm.ChatChunk(
|
# The base class handles the async iteration
|
||||||
|
self._event_ch.send_nowait(
|
||||||
|
llm.ChatChunk(
|
||||||
id="wellnuo-response",
|
id="wellnuo-response",
|
||||||
delta=llm.ChoiceDelta(
|
delta=llm.ChoiceDelta(
|
||||||
role="assistant",
|
role="assistant",
|
||||||
content=self._response_text,
|
content=response_text,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
def __aiter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
def prewarm(proc: JobProcess):
|
def prewarm(proc: JobProcess):
|
||||||
@ -163,6 +183,9 @@ def prewarm(proc: JobProcess):
|
|||||||
async def entrypoint(ctx: JobContext):
|
async def entrypoint(ctx: JobContext):
|
||||||
"""Main Julia AI voice session handler."""
|
"""Main Julia AI voice session handler."""
|
||||||
|
|
||||||
|
# CRITICAL: Must connect to room first before accessing ctx.room
|
||||||
|
await ctx.connect()
|
||||||
|
|
||||||
logger.info(f"Starting Julia AI session in room {ctx.room.name}")
|
logger.info(f"Starting Julia AI session in room {ctx.room.name}")
|
||||||
logger.info(f"Using WellNuo voice_ask API with deployment_id: {DEPLOYMENT_ID}")
|
logger.info(f"Using WellNuo voice_ask API with deployment_id: {DEPLOYMENT_ID}")
|
||||||
|
|
||||||
@ -198,5 +221,7 @@ if __name__ == "__main__":
|
|||||||
WorkerOptions(
|
WorkerOptions(
|
||||||
entrypoint_fnc=entrypoint,
|
entrypoint_fnc=entrypoint,
|
||||||
prewarm_fnc=prewarm,
|
prewarm_fnc=prewarm,
|
||||||
|
# Agent name must match what token requests (AGENT_NAME in livekit.js)
|
||||||
|
agent_name="julia-ai",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user