From ba8711f28fe94d6b47c1aff466a21e508694a24b Mon Sep 17 00:00:00 2001 From: aoife cassidy Date: Fri, 12 Apr 2024 03:49:58 +0300 Subject: [PATCH 1/2] Add worker, fill out CLI, + various lints --- agents/build.ts | 2 +- agents/package.json | 6 +- agents/src/cli.ts | 107 +++++---- agents/src/index.ts | 3 + agents/src/ipc/job_process.ts | 9 +- agents/src/ipc/protocol.ts | 2 +- agents/src/job_context.ts | 2 +- agents/src/job_request.ts | 8 +- agents/src/stt/stream_adapter.ts | 2 +- agents/src/stt/stt.ts | 2 +- agents/src/tokenize.ts | 2 +- agents/src/tts/tts.ts | 4 +- agents/src/worker.ts | 357 +++++++++++++++++++++++++++++++ bun.lockb | Bin 76983 -> 78113 bytes examples/minimal.ts | 17 ++ 15 files changed, 466 insertions(+), 57 deletions(-) create mode 100644 agents/src/worker.ts create mode 100644 examples/minimal.ts diff --git a/agents/build.ts b/agents/build.ts index d339112f..eb56ab74 100644 --- a/agents/build.ts +++ b/agents/build.ts @@ -5,7 +5,7 @@ import dts from 'bun-plugin-dts'; await Bun.build({ - entrypoints: ['./src/index.ts', './src/tts/index.ts', './src/stt/index.ts'], + entrypoints: ['./src/index.ts', './src/tts/index.ts', './src/stt/index.ts', './src/cli.ts'], outdir: './dist', target: 'bun', // https://github.com/oven-sh/bun/blob/main/src/bundler/bundle_v2.zig#L2667 sourcemap: 'external', diff --git a/agents/package.json b/agents/package.json index 82ebe2cd..f661dc68 100644 --- a/agents/package.json +++ b/agents/package.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/ws": "^8.5.10", "bun-plugin-dts": "^0.2.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -25,10 +26,11 @@ "typescript": "^5.0.0" }, "dependencies": { - "@livekit/protocol": "^1.12.0", + "@livekit/protocol": "^1.13.0", "commander": "^12.0.0", "livekit-server-sdk": "^2.1.2", "pino": "^8.19.0", - "pino-pretty": "^11.0.0" + "pino-pretty": "^11.0.0", + "ws": "^8.16.0" } } diff --git a/agents/src/cli.ts b/agents/src/cli.ts index c7610abf..5388110e 100644 --- a/agents/src/cli.ts +++ b/agents/src/cli.ts @@ -2,44 +2,77 @@ // // SPDX-License-Identifier: Apache-2.0 -import { version } from './index'; +import { version } from '.'; import { Option, Command } from 'commander'; +import { WorkerOptions, Worker } from './worker'; +import { EventEmitter } from 'events'; +import { log } from './log'; -const program = new Command(); -program - .name('agents') - .description('LiveKit Agents CLI') - .version(version) - .addOption( - new Option('--log-level', 'Set the logging level').choices([ - 'DEBUG', - 'INFO', - 'WARNING', - 'ERROR', - 'CRITICAL', - ]), - ); - -program - .command('start') - .description('Start the worker') - .addOption( - new Option('--url ', 'LiveKit server or Cloud project websocket URL') - .makeOptionMandatory(true) - .env('LIVEKIT_URL'), - ) - .addOption( - new Option('--api-key ', "LiveKit server or Cloud project's API key") - .makeOptionMandatory(true) - .env('LIVEKIT_API_KEY'), - ) - .addOption( - new Option('--api-secret ', "LiveKit server or Cloud project's API secret") - .makeOptionMandatory(true) - .env('LIVEKIT_API_SECRET'), - ) - .action(() => { - return; +type CliArgs = { + opts: WorkerOptions; + logLevel: string; + production: boolean; + watch: boolean; + event?: EventEmitter; +}; + +const runWorker = async (args: CliArgs) => { + log.level = args.logLevel; + const worker = new Worker(args.opts); + + process.on('SIGINT', async () => { + await worker.close(); + process.exit(130); // SIGINT exit code }); -program.parse(); + try { + await worker.run(); + } catch { + log.fatal('worker failed'); + } +}; + +export const runApp = (opts: WorkerOptions) => { + const program = new Command() + .name('agents') + .description('LiveKit Agents CLI') + .version(version) + .addOption( + new Option('--log-level ', 'Set the logging level') + .choices(['trace', 'debug', 'info', 'warn', 'error', 'fatal']) + .default('trace'), + ) + .addOption( + new Option('--url ', 'LiveKit server or Cloud project websocket URL') + .makeOptionMandatory(true) + .env('LIVEKIT_URL'), + ) + .addOption( + new Option('--api-key ', "LiveKit server or Cloud project's API key") + .makeOptionMandatory(true) + .env('LIVEKIT_API_KEY'), + ) + .addOption( + new Option('--api-secret ', "LiveKit server or Cloud project's API secret") + .makeOptionMandatory(true) + .env('LIVEKIT_API_SECRET'), + ) + + program + .command('start') + .description('Start the worker in production mode') + .action(() => { + const options = program.optsWithGlobals() + opts.wsURL = options.url || opts.wsURL; + opts.apiKey = options.apiKey || opts.apiKey; + opts.apiSecret = options.apiSecret || opts.apiSecret; + runWorker({ + opts, + production: true, + watch: false, + logLevel: options.logLevel, + }); + }); + + program.parse(); +}; diff --git a/agents/src/index.ts b/agents/src/index.ts index 4bb69641..6db2e8fd 100644 --- a/agents/src/index.ts +++ b/agents/src/index.ts @@ -5,3 +5,6 @@ export * from './vad'; export * from './plugin'; export * from './version'; +export * from './job_context'; +export * from './job_request'; +export * from './worker'; diff --git a/agents/src/ipc/job_process.ts b/agents/src/ipc/job_process.ts index ad4bed7b..58f6ef60 100644 --- a/agents/src/ipc/job_process.ts +++ b/agents/src/ipc/job_process.ts @@ -20,10 +20,10 @@ import { runJob } from './job_main'; import { EventEmitter } from 'events'; import { log } from '../log'; -const START_TIMEOUT = 90; -const PING_INTERVAL = 5; -const PING_TIMEOUT = 90; -const HIGH_PING_THRESHOLD = 10; // milliseconds +const START_TIMEOUT = 90 * 1000; +const PING_INTERVAL = 5 * 1000; +const PING_TIMEOUT = 90 * 1000; +const HIGH_PING_THRESHOLD = 10; export class JobProcess { #job: Job; @@ -103,6 +103,7 @@ export class JobProcess { const delay = Date.now() - msg.timestamp; if (delay > HIGH_PING_THRESHOLD) { this.logger.warn(`job is unresponsive (${delay}ms delay)`); + // @ts-expect-error: this actually works fine types/bun doesn't have a typedecl for it yet pongTimeout.refresh(); } } else if (msg instanceof UserExit || msg instanceof ShutdownResponse) { diff --git a/agents/src/ipc/protocol.ts b/agents/src/ipc/protocol.ts index d176e0cd..1137f7b9 100644 --- a/agents/src/ipc/protocol.ts +++ b/agents/src/ipc/protocol.ts @@ -31,7 +31,7 @@ export class StartJobRequest implements Message { export class StartJobResponse implements Message { static MSG_ID = 1; - err: Error | undefined; + err?: Error; get MSG_ID(): number { return StartJobResponse.MSG_ID; diff --git a/agents/src/job_context.ts b/agents/src/job_context.ts index 1a39775e..13abf41a 100644 --- a/agents/src/job_context.ts +++ b/agents/src/job_context.ts @@ -9,7 +9,7 @@ import { EventEmitter } from 'events'; export class JobContext { #job: Job; #room: Room; - #publisher: RemoteParticipant | undefined; + #publisher?: RemoteParticipant; tx: EventEmitter; constructor( diff --git a/agents/src/job_request.ts b/agents/src/job_request.ts index 4f2d9def..a2ecb08c 100644 --- a/agents/src/job_request.ts +++ b/agents/src/job_request.ts @@ -3,7 +3,6 @@ // SPDX-License-Identifier: Apache-2.0 import { JobContext } from './job_context'; -import { VideoGrant } from 'livekit-server-sdk'; import { Job, ParticipantInfo, Room } from '@livekit/protocol'; import { log } from './log'; import { EventEmitter } from 'events'; @@ -35,16 +34,15 @@ export type AcceptData = { entry: AgentEntry; autoSubscribe: AutoSubscribe; autoDisconnect: AutoDisconnect; - grants: VideoGrant; name: string; identity: string; metadata: string; assign: EventEmitter; }; -type AvailRes = { +export type AvailRes = { avail: boolean; - data: AcceptData | undefined; + data?: AcceptData; }; export class JobRequest { @@ -91,7 +89,6 @@ export class JobRequest { entry: AgentEntry, autoSubscribe: AutoSubscribe = AutoSubscribe.SUBSCRIBE_ALL, autoDisconnect: AutoDisconnect = AutoDisconnect.ROOM_EMPTY, - grants: VideoGrant, name: string = '', identity: string = '', metadata: string = '', @@ -110,7 +107,6 @@ export class JobRequest { entry, autoSubscribe, autoDisconnect, - grants, name, identity, metadata, diff --git a/agents/src/stt/stream_adapter.ts b/agents/src/stt/stream_adapter.ts index 29dd9733..e1dc8487 100644 --- a/agents/src/stt/stream_adapter.ts +++ b/agents/src/stt/stream_adapter.ts @@ -12,7 +12,7 @@ export class StreamAdapterWrapper extends SpeechStream { stt: STT; vadStream: VADStream; eventQueue: (SpeechEvent | undefined)[]; - language: string | undefined; + language?: string; task: { run: Promise; cancel: () => void; diff --git a/agents/src/stt/stt.ts b/agents/src/stt/stt.ts index 8b920411..c58051be 100644 --- a/agents/src/stt/stt.ts +++ b/agents/src/stt/stt.ts @@ -49,7 +49,7 @@ export abstract class STT { this.#streamingSupported = streamingSupported; } - abstract recognize(buffer: AudioBuffer, language: string | undefined): Promise; + abstract recognize(buffer: AudioBuffer, language?: string): Promise; abstract stream(language: string | undefined): SpeechStream; diff --git a/agents/src/tokenize.ts b/agents/src/tokenize.ts index ff7f658d..0dee52c2 100644 --- a/agents/src/tokenize.ts +++ b/agents/src/tokenize.ts @@ -7,7 +7,7 @@ export interface SegmentedSentence { } export abstract class SentenceTokenizer { - abstract tokenize(text: string, language: string | undefined): SegmentedSentence[]; + abstract tokenize(text: string, language?: string): SegmentedSentence[]; abstract stream(language: string | undefined): SentenceStream; } diff --git a/agents/src/tts/tts.ts b/agents/src/tts/tts.ts index ddb18ff7..c352f61a 100644 --- a/agents/src/tts/tts.ts +++ b/agents/src/tts/tts.ts @@ -17,7 +17,7 @@ export enum SynthesisEventType { export class SynthesisEvent { type: SynthesisEventType; - audio: SynthesizedAudio | undefined; + audio?: SynthesizedAudio; constructor(type: SynthesisEventType, audio: SynthesizedAudio | undefined = undefined) { this.type = type; @@ -26,7 +26,7 @@ export class SynthesisEvent { } export abstract class SynthesizeStream implements IterableIterator { - abstract pushText(token: string | undefined): void; + abstract pushText(token?: string): void; markSegmentEnd() { this.pushText(undefined); diff --git a/agents/src/worker.ts b/agents/src/worker.ts new file mode 100644 index 00000000..028d506f --- /dev/null +++ b/agents/src/worker.ts @@ -0,0 +1,357 @@ +// SPDX-FileCopyrightText: 2024 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +import os from 'os'; +import { WebSocket } from 'ws'; +import { AvailRes, JobRequest } from './job_request'; +import { + JobType, + Job, + WorkerMessage, + ParticipantPermission, + ServerMessage, + JobAssignment, +} from '@livekit/protocol'; +import { AcceptData } from './job_request'; +import { HTTPServer } from './http_server'; +import { log } from './log'; +import { version } from './version'; +import { AccessToken } from 'livekit-server-sdk'; +import { EventEmitter } from 'events'; +import { JobProcess } from './ipc/job_process'; + +const MAX_RECONNECT_ATTEMPTS = 10; +const ASSIGNMENT_TIMEOUT = 15 * 1000; +const LOAD_INTERVAL = 5 * 1000; + +const cpuLoad = (): number => + (os + .cpus() + .reduce( + (acc, x) => acc + x.times.user / Object.values(x.times).reduce((acc, x) => acc + x, 0), + 0, + ) / + os.cpus().length) * + 100; + +class WorkerPermissions { + canPublish: boolean; + canSubscribe: boolean; + canPublishData: boolean; + canUpdateMetadata: boolean; + hidden: boolean; + + constructor( + canPublish = true, + canSubscribe = true, + canPublishData = true, + canUpdateMetadata = true, + hidden = false, + ) { + this.canPublish = canPublish; + this.canSubscribe = canSubscribe; + this.canPublishData = canPublishData; + this.canUpdateMetadata = canUpdateMetadata; + this.hidden = hidden; + } +} + +export class WorkerOptions { + requestFunc: (arg: JobRequest) => Promise; + cpuLoadFunc: () => number; + namespace: string; + permissions: WorkerPermissions; + workerType: JobType; + maxRetry: number; + wsURL: string; + apiKey?: string; + apiSecret?: string; + host: string; + port: number; + + constructor({ + requestFunc, + cpuLoadFunc = cpuLoad, + namespace = 'default', + permissions = new WorkerPermissions(), + workerType = JobType.JT_PUBLISHER, + maxRetry = MAX_RECONNECT_ATTEMPTS, + wsURL = 'ws://localhost:7880', + apiKey = undefined, + apiSecret = undefined, + host = 'localhost', + port = 8081, + }: { + requestFunc: (arg: JobRequest) => Promise; + cpuLoadFunc?: () => number; + namespace?: string; + permissions?: WorkerPermissions; + workerType?: JobType; + maxRetry?: number; + wsURL?: string; + apiKey?: string; + apiSecret?: string; + host?: string; + port?: number; + }) { + this.requestFunc = requestFunc; + this.cpuLoadFunc = cpuLoadFunc; + this.namespace = namespace; + this.permissions = permissions; + this.workerType = workerType; + this.maxRetry = maxRetry; + this.wsURL = wsURL; + this.apiKey = apiKey; + this.apiSecret = apiSecret; + this.host = host; + this.port = port; + } +} + +class ActiveJob { + job: Job; + acceptData: AcceptData; + + constructor(job: Job, acceptData: AcceptData) { + this.job = job; + this.acceptData = acceptData; + } +} + +export class Worker { + opts: WorkerOptions; + #id = 'unregistered'; + session: WebSocket | undefined = undefined; + closed = false; + httpServer: HTTPServer; + logger = log.child({ version }); + event = new EventEmitter(); + pending: { [id: string]: { value?: JobAssignment } } = {}; + processes: { [id: string]: { proc: JobProcess; activeJob: ActiveJob } } = {}; + + constructor(opts: WorkerOptions) { + opts.wsURL = opts.wsURL || process.env.LIVEKIT_URL || ''; + opts.apiKey = opts.apiKey || process.env.LIVEKIT_API_KEY || ''; + opts.apiSecret = opts.apiSecret || process.env.LIVEKIT_API_SECRET || ''; + + this.opts = opts; + this.httpServer = new HTTPServer(opts.host, opts.port); + } + + get id(): string { + return this.#id; + } + + async run() { + this.logger.info('starting worker'); + + if (this.opts.wsURL === '') throw new Error('--url is required, or set LIVEKIT_URL env var'); + if (this.opts.apiKey === '') + throw new Error('--api-key is required, or set LIVEKIT_API_KEY env var'); + if (this.opts.apiSecret === '') + throw new Error('--api-secret is required, or set LIVEKIT_API_SECRET env var'); + + const workerWS = async () => { + // const retries = 0; + while (!this.closed) { + const token = new AccessToken(this.opts.apiKey, this.opts.apiSecret); + token.addGrant({ agent: true }); + const jwt = await token.toJwt(); + + const url = new URL(this.opts.wsURL); + url.protocol = url.protocol.replace('http', 'ws'); + this.session = new WebSocket(url + 'agent', { + headers: { authorization: 'Bearer ' + jwt }, + }); + this.session.on('open', () => { + this.session!.removeAllListeners('close'); + this.runWS(this.session!); + }); + return; + + // TODO(nbsp): retries that actually work + // if (this.session.readyState !== WebSocket.OPEN) { + // if (this.closed) return; + // if (retries >= this.opts.maxRetry) { + // throw new Error(`failed to connect to LiveKit server after ${retries} attempts: ${e}`); + // } + + // const delay = Math.min(retries * 2, 10); + // retries++; + + // this.logger.warn( + // `failed to connect to LiveKit server, retrying in ${delay} seconds: ${e} (${retries}/${this.opts.maxRetry})`, + // ); + // await new Promise((resolve) => setTimeout(resolve, delay)); + // } + } + }; + + await Promise.all([workerWS(), this.httpServer.run()]); + } + + startProcess(job: Job, url: string, token: string, acceptData: AcceptData) { + const proc = new JobProcess(job, url, token, acceptData.entry); + this.processes[job.id] = { proc, activeJob: new ActiveJob(job, acceptData) }; + new Promise((_, reject) => { + try { + proc.run(); + } catch (e) { + proc.logger.error(`error running job process ${proc.job.id}`); + reject(e); + } finally { + delete this.processes[job.id]; + } + }); + } + + runWS(ws: WebSocket) { + let closingWS = false; + + const send = (msg: WorkerMessage) => { + if (closingWS) { + this.event.off('worker_msg', send); + return; + } + ws.send(msg.toBinary()); + }; + this.event.on('worker_msg', send); + + ws.addEventListener('close', () => { + closingWS = true; + if (!this.closed) throw new Error('worker connection closed unexpectedly'); + }); + + ws.addEventListener('message', (event) => { + if (event.type !== 'message') { + this.logger.warn('unexpected message type: ' + event.type); + return; + } + + const msg = new ServerMessage(); + msg.fromBinary(event.data as Uint8Array); + switch (msg.message.case) { + case 'register': { + this.#id = msg.message.value.workerId; + log + .child({ id: this.id, server_info: msg.message.value.serverInfo }) + .info('registered worker'); + break; + } + case 'availability': { + const tx = new EventEmitter(); + const req = new JobRequest(msg.message.value.job!, tx); + this.event.on('recv', (av: AvailRes) => { + const msg = new WorkerMessage({ + message: { + case: 'availability', + value: { + available: av.avail, + jobId: req.id, + participantIdentity: av.data?.identity, + participantName: av.data?.name, + participantMetadata: av.data?.metadata, + }, + }, + }); + + this.pending[req.id] = { value: undefined }; + this.event.emit('worker_msg', msg); + + new Promise((_, reject) => { + const timer = setTimeout(() => { + reject(new Error(`assignment for job ${req.id} timed out`)); + }, ASSIGNMENT_TIMEOUT); + Promise.resolve(this.pending[req.id].value).then((value) => { + clearTimeout(timer); + const url = value?.url || this.opts.wsURL; + + try { + this.opts.requestFunc(req); + } catch (e) { + log.child({ req }).error(`user request hadnler for job ${req.id} failed`); + } finally { + if (!req.answered) { + log + .child({ req }) + .error(`no answer for job ${req.id}, automatically rejecting the job`); + this.event.emit( + 'worker_msg', + new WorkerMessage({ + message: { + case: 'availability', + value: { + available: false, + }, + }, + }), + ); + } + } + + this.startProcess(value!.job!, url, value!.token, av.data!); + }); + }); + }); + + break; + } + case 'assignment': { + const job = msg.message.value.job!; + if (job.id in this.pending) { + const task = this.pending[job.id]; + delete this.pending[job.id]; + task.value = msg.message.value; + } else { + log.child({ job }).warn('received assignment for unknown job ' + job.id); + } + break; + } + } + }); + + this.event.emit( + 'worker_msg', + new WorkerMessage({ + message: { + case: 'register', + value: { + type: this.opts.workerType, + namespace: this.opts.namespace, + allowedPermissions: new ParticipantPermission({ + canPublish: this.opts.permissions.canPublish, + canSubscribe: this.opts.permissions.canSubscribe, + canPublishData: this.opts.permissions.canPublishData, + hidden: this.opts.permissions.hidden, + agent: true, + }), + version, + }, + }, + }), + ); + + const loadMonitor = setInterval(() => { + if (closingWS) clearInterval(loadMonitor); + this.event.emit( + 'worker_msg', + new WorkerMessage({ + message: { + case: 'updateWorker', + value: { + load: cpuLoad(), + }, + }, + }), + ); + }, LOAD_INTERVAL); + } + + async close() { + if (this.closed) return; + this.logger.info('shutting down worker'); + await this.httpServer.close(); + this.session; + } +} diff --git a/bun.lockb b/bun.lockb index f1bd9a6df6cf21a38f47ff720eec5a094bdfca87..42a048d4d74337bb1a8ce5f59679c9f9c22bab80 100755 GIT binary patch delta 13867 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 delta 13254 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 diff --git a/examples/minimal.ts b/examples/minimal.ts new file mode 100644 index 00000000..b7867133 --- /dev/null +++ b/examples/minimal.ts @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +import { runApp } from '../agents/src/cli' +import { JobContext, JobRequest, WorkerOptions } from '../agents/src' + +const requestFunc = async (req: JobRequest) => { + console.log('received request', req) + await req.accept(async (_: JobContext) => { + console.log('starting voice assistant...') + + // etc + }) +} + +runApp(new WorkerOptions({ requestFunc })) From 2af7327d5e18f96927b01c4d81a7fde2f63c97ba Mon Sep 17 00:00:00 2001 From: aoife cassidy Date: Fri, 12 Apr 2024 09:01:28 +0300 Subject: [PATCH 2/2] worker: check for CPU idle instead of user --- agents/src/worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agents/src/worker.ts b/agents/src/worker.ts index 028d506f..339b1e48 100644 --- a/agents/src/worker.ts +++ b/agents/src/worker.ts @@ -29,7 +29,7 @@ const cpuLoad = (): number => (os .cpus() .reduce( - (acc, x) => acc + x.times.user / Object.values(x.times).reduce((acc, x) => acc + x, 0), + (acc, x) => acc + x.times.idle / Object.values(x.times).reduce((acc, x) => acc + x, 0), 0, ) / os.cpus().length) *