mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
256 lines
7.7 KiB
TypeScript
256 lines
7.7 KiB
TypeScript
/**
|
|
* API functions for voice assistant integration.
|
|
*
|
|
* Fetches conversation tokens from the hub for ElevenLabs integration.
|
|
* The hub handles authentication with ElevenLabs API, keeping credentials secure.
|
|
*
|
|
* Supports two modes:
|
|
* 1. Default: Hub uses its own ElevenLabs credentials (production)
|
|
* 2. Custom: Client provides their own ElevenLabs agent ID and API key
|
|
*/
|
|
|
|
import type { ApiClient } from './client'
|
|
import {
|
|
ELEVENLABS_API_BASE,
|
|
VOICE_AGENT_NAME,
|
|
buildVoiceAgentConfig
|
|
} from '@hapi/protocol/voice'
|
|
import type { VoiceBackendType } from '@hapi/protocol/voice'
|
|
|
|
export interface VoiceTokenResponse {
|
|
allowed: boolean
|
|
token?: string
|
|
agentId?: string
|
|
error?: string
|
|
}
|
|
|
|
export interface VoiceTokenRequest {
|
|
customAgentId?: string
|
|
customApiKey?: string
|
|
voiceId?: string
|
|
}
|
|
|
|
/**
|
|
* Fetch a conversation token from the hub for ElevenLabs voice sessions.
|
|
*
|
|
* This uses the private agent flow where:
|
|
* 1. Hub holds the ELEVENLABS_API_KEY and ELEVENLABS_AGENT_ID (or uses user-provided ones)
|
|
* 2. Hub fetches a short-lived conversation token from ElevenLabs
|
|
* 3. Client uses this token to establish WebRTC connection
|
|
*/
|
|
export async function fetchVoiceToken(
|
|
api: ApiClient,
|
|
options?: VoiceTokenRequest
|
|
): Promise<VoiceTokenResponse> {
|
|
try {
|
|
return await api.fetchVoiceToken(options)
|
|
} catch (error) {
|
|
return {
|
|
allowed: false,
|
|
error: error instanceof Error ? error.message : 'Network error'
|
|
}
|
|
}
|
|
}
|
|
|
|
export interface VoiceInfo {
|
|
id: string
|
|
name: string
|
|
previewUrl: string
|
|
category: string
|
|
/** Static-catalog hint (Gemini/Qwen); ElevenLabs uses API name only. */
|
|
description?: string
|
|
}
|
|
|
|
export async function fetchVoices(api: ApiClient): Promise<VoiceInfo[]> {
|
|
try {
|
|
const result = await api.fetchVoices()
|
|
return result.voices
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export interface ElevenLabsAgent {
|
|
agent_id: string
|
|
name: string
|
|
}
|
|
|
|
export interface FindAgentResult {
|
|
success: boolean
|
|
agentId?: string
|
|
error?: string
|
|
}
|
|
|
|
export interface CreateAgentResult {
|
|
success: boolean
|
|
agentId?: string
|
|
error?: string
|
|
created?: boolean
|
|
}
|
|
|
|
/**
|
|
* Find an existing "Hapi Voice Assistant" agent using the provided API key.
|
|
*/
|
|
export async function findHapiAgent(apiKey: string): Promise<FindAgentResult> {
|
|
try {
|
|
const response = await fetch(`${ELEVENLABS_API_BASE}/convai/agents`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'xi-api-key': apiKey,
|
|
'Accept': 'application/json'
|
|
}
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({})) as { detail?: { message?: string } | string }
|
|
const errorMessage = typeof errorData.detail === 'string'
|
|
? errorData.detail
|
|
: errorData.detail?.message || `API error: ${response.status}`
|
|
return { success: false, error: errorMessage }
|
|
}
|
|
|
|
const data = await response.json() as { agents?: ElevenLabsAgent[] }
|
|
const agents: ElevenLabsAgent[] = data.agents || []
|
|
|
|
const hapiAgent = agents.find(agent => agent.name === VOICE_AGENT_NAME)
|
|
|
|
if (hapiAgent) {
|
|
return { success: true, agentId: hapiAgent.agent_id }
|
|
} else {
|
|
return { success: false, error: `No agent named "${VOICE_AGENT_NAME}" found` }
|
|
}
|
|
} catch (e) {
|
|
return { success: false, error: e instanceof Error ? e.message : 'Network error' }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create or update the "Hapi Voice Assistant" agent with our default configuration.
|
|
*/
|
|
export async function createOrUpdateHapiAgent(apiKey: string): Promise<CreateAgentResult> {
|
|
try {
|
|
const findResult = await findHapiAgent(apiKey)
|
|
const existingAgentId = findResult.success ? findResult.agentId : null
|
|
|
|
const agentConfig = buildVoiceAgentConfig()
|
|
|
|
let response: Response
|
|
let created = false
|
|
|
|
if (existingAgentId) {
|
|
response = await fetch(`${ELEVENLABS_API_BASE}/convai/agents/${existingAgentId}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'xi-api-key': apiKey,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify(agentConfig)
|
|
})
|
|
} else {
|
|
response = await fetch(`${ELEVENLABS_API_BASE}/convai/agents/create`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'xi-api-key': apiKey,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify(agentConfig)
|
|
})
|
|
created = true
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({})) as { detail?: { message?: string } | string }
|
|
const errorMessage = typeof errorData.detail === 'string'
|
|
? errorData.detail
|
|
: errorData.detail?.message || `API error: ${response.status}`
|
|
return { success: false, error: errorMessage }
|
|
}
|
|
|
|
const data = await response.json() as { agent_id?: string }
|
|
const agentId = existingAgentId || data.agent_id
|
|
|
|
if (!agentId) {
|
|
return { success: false, error: 'Failed to get agent ID from response' }
|
|
}
|
|
|
|
return { success: true, agentId, created }
|
|
} catch (e) {
|
|
return { success: false, error: e instanceof Error ? e.message : 'Network error' }
|
|
}
|
|
}
|
|
|
|
// --- Pluggable voice backend API ---
|
|
|
|
export interface QwenTokenResponse {
|
|
allowed: boolean
|
|
wsUrl?: string
|
|
error?: string
|
|
}
|
|
|
|
/**
|
|
* Fetch a DashScope API key from the hub for Qwen Realtime voice sessions.
|
|
*/
|
|
export async function fetchQwenToken(api: ApiClient): Promise<QwenTokenResponse> {
|
|
try {
|
|
return await api.fetchQwenToken()
|
|
} catch (error) {
|
|
return {
|
|
allowed: false,
|
|
error: error instanceof Error ? error.message : 'Network error'
|
|
}
|
|
}
|
|
}
|
|
|
|
export interface VoiceBackendResponse {
|
|
/** Hub default (VOICE_BACKEND env, validated against configured backends). */
|
|
backend: VoiceBackendType | null
|
|
/** Backends with API keys configured on the hub. */
|
|
backends: VoiceBackendType[]
|
|
}
|
|
|
|
export interface GeminiTokenResponse {
|
|
allowed: boolean
|
|
apiKey?: string
|
|
wsUrl?: string
|
|
baseUrl?: string
|
|
error?: string
|
|
}
|
|
|
|
/**
|
|
* Discover which voice backend the hub is configured to use.
|
|
* Throws on network/server error or unrecognised backend value — callers must handle failures explicitly.
|
|
*/
|
|
function isVoiceBackendType(value: string): value is VoiceBackendType {
|
|
return value === 'elevenlabs' || value === 'gemini-live' || value === 'qwen-realtime'
|
|
}
|
|
|
|
export async function fetchVoiceBackend(api: ApiClient): Promise<VoiceBackendResponse> {
|
|
const result = await api.fetchVoiceBackend()
|
|
const { backend } = result
|
|
if (backend !== null && !isVoiceBackendType(backend)) {
|
|
throw new Error(`Unrecognised voice backend: ${backend}`)
|
|
}
|
|
const rawBackends = Array.isArray(result.backends) ? result.backends : backend !== null ? [backend] : []
|
|
const backends = rawBackends.filter(isVoiceBackendType)
|
|
if (backend !== null && backends.length === 0) {
|
|
backends.push(backend)
|
|
}
|
|
return { backend, backends }
|
|
}
|
|
|
|
/**
|
|
* Fetch a Gemini API key from the hub for Gemini Live voice sessions.
|
|
*/
|
|
export async function fetchGeminiToken(api: ApiClient): Promise<GeminiTokenResponse> {
|
|
try {
|
|
return await api.fetchGeminiToken()
|
|
} catch (error) {
|
|
return {
|
|
allowed: false,
|
|
error: error instanceof Error ? error.message : 'Network error'
|
|
}
|
|
}
|
|
}
|