@livekit/agents 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +47 -0
- package/LICENSE +201 -0
- package/dist/audio.d.ts +9 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +54 -0
- package/dist/audio.js.map +1 -0
- package/dist/cli.d.ts +12 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +102 -19
- package/dist/cli.js.map +1 -1
- package/dist/generator.d.ts +17 -6
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +20 -3
- package/dist/generator.js.map +1 -1
- package/dist/http_server.d.ts +1 -1
- package/dist/http_server.d.ts.map +1 -1
- package/dist/http_server.js +5 -3
- package/dist/http_server.js.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/ipc/job_executor.d.ts +19 -0
- package/dist/ipc/job_executor.d.ts.map +1 -0
- package/dist/ipc/job_executor.js +8 -0
- package/dist/ipc/job_executor.js.map +1 -0
- package/dist/ipc/job_main.d.ts +7 -4
- package/dist/ipc/job_main.d.ts.map +1 -1
- package/dist/ipc/job_main.js +102 -59
- package/dist/ipc/job_main.js.map +1 -1
- package/dist/ipc/message.d.ts +41 -0
- package/dist/ipc/message.d.ts.map +1 -0
- package/dist/ipc/message.js +2 -0
- package/dist/ipc/message.js.map +1 -0
- package/dist/ipc/proc_job_executor.d.ts +15 -0
- package/dist/ipc/proc_job_executor.d.ts.map +1 -0
- package/dist/ipc/proc_job_executor.js +150 -0
- package/dist/ipc/proc_job_executor.js.map +1 -0
- package/dist/ipc/proc_pool.d.ts +26 -0
- package/dist/ipc/proc_pool.d.ts.map +1 -0
- package/dist/ipc/proc_pool.js +83 -0
- package/dist/ipc/proc_pool.js.map +1 -0
- package/dist/job.d.ts +100 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +213 -0
- package/dist/job.js.map +1 -0
- package/dist/llm/function_context.d.ts +20 -0
- package/dist/llm/function_context.d.ts.map +1 -0
- package/dist/llm/function_context.js +37 -0
- package/dist/llm/function_context.js.map +1 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +6 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/log.d.ts +12 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +28 -11
- package/dist/log.js.map +1 -1
- package/dist/multimodal/agent_playout.d.ts +34 -0
- package/dist/multimodal/agent_playout.d.ts.map +1 -0
- package/dist/multimodal/agent_playout.js +221 -0
- package/dist/multimodal/agent_playout.js.map +1 -0
- package/dist/multimodal/index.d.ts +3 -0
- package/dist/multimodal/index.d.ts.map +1 -0
- package/dist/multimodal/index.js +6 -0
- package/dist/multimodal/index.js.map +1 -0
- package/dist/multimodal/multimodal_agent.d.ts +47 -0
- package/dist/multimodal/multimodal_agent.d.ts.map +1 -0
- package/dist/multimodal/multimodal_agent.js +331 -0
- package/dist/multimodal/multimodal_agent.js.map +1 -0
- package/dist/plugin.js +20 -7
- package/dist/plugin.js.map +1 -1
- package/dist/stt/index.d.ts +1 -1
- package/dist/stt/index.d.ts.map +1 -1
- package/dist/stt/index.js.map +1 -1
- package/dist/stt/stream_adapter.d.ts +2 -11
- package/dist/stt/stream_adapter.d.ts.map +1 -1
- package/dist/stt/stream_adapter.js +47 -33
- package/dist/stt/stream_adapter.js.map +1 -1
- package/dist/stt/stt.d.ts +27 -0
- package/dist/stt/stt.d.ts.map +1 -1
- package/dist/stt/stt.js +32 -5
- package/dist/stt/stt.js.map +1 -1
- package/dist/transcription.d.ts +22 -0
- package/dist/transcription.d.ts.map +1 -0
- package/dist/transcription.js +111 -0
- package/dist/transcription.js.map +1 -0
- package/dist/tts/stream_adapter.d.ts +4 -11
- package/dist/tts/stream_adapter.d.ts.map +1 -1
- package/dist/tts/stream_adapter.js +66 -32
- package/dist/tts/stream_adapter.js.map +1 -1
- package/dist/tts/tts.d.ts +10 -0
- package/dist/tts/tts.d.ts.map +1 -1
- package/dist/tts/tts.js +48 -7
- package/dist/tts/tts.js.map +1 -1
- package/dist/utils.d.ts +59 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +212 -6
- package/dist/utils.js.map +1 -1
- package/dist/vad.d.ts +29 -0
- package/dist/vad.d.ts.map +1 -1
- package/dist/vad.js.map +1 -1
- package/dist/worker.d.ts +69 -50
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +414 -213
- package/dist/worker.js.map +1 -1
- package/package.json +12 -10
- package/src/audio.ts +62 -0
- package/src/cli.ts +108 -20
- package/src/generator.ts +27 -7
- package/src/http_server.ts +5 -0
- package/src/index.ts +15 -3
- package/src/ipc/job_executor.ts +25 -0
- package/src/ipc/job_main.ts +141 -61
- package/src/ipc/message.ts +39 -0
- package/src/ipc/proc_job_executor.ts +162 -0
- package/src/ipc/proc_pool.ts +109 -0
- package/src/job.ts +278 -0
- package/src/llm/function_context.ts +61 -0
- package/src/llm/index.ts +11 -0
- package/src/log.ts +40 -8
- package/src/multimodal/agent_playout.ts +254 -0
- package/src/multimodal/index.ts +5 -0
- package/src/multimodal/multimodal_agent.ts +428 -0
- package/src/stt/index.ts +1 -1
- package/src/stt/stream_adapter.ts +32 -32
- package/src/stt/stt.ts +27 -0
- package/src/transcription.ts +128 -0
- package/src/tts/stream_adapter.ts +32 -31
- package/src/tts/tts.ts +10 -0
- package/src/utils.ts +257 -3
- package/src/vad.ts +29 -0
- package/src/worker.ts +465 -172
- package/tsconfig.json +7 -1
- package/dist/ipc/job_process.d.ts +0 -22
- package/dist/ipc/job_process.d.ts.map +0 -1
- package/dist/ipc/job_process.js +0 -73
- package/dist/ipc/job_process.js.map +0 -1
- package/dist/ipc/protocol.d.ts +0 -40
- package/dist/ipc/protocol.d.ts.map +0 -1
- package/dist/ipc/protocol.js +0 -14
- package/dist/ipc/protocol.js.map +0 -1
- package/dist/job_context.d.ts +0 -16
- package/dist/job_context.d.ts.map +0 -1
- package/dist/job_context.js +0 -31
- package/dist/job_context.js.map +0 -1
- package/dist/job_request.d.ts +0 -42
- package/dist/job_request.d.ts.map +0 -1
- package/dist/job_request.js +0 -79
- package/dist/job_request.js.map +0 -1
- package/src/ipc/job_process.ts +0 -96
- package/src/ipc/protocol.ts +0 -51
- package/src/job_context.ts +0 -49
- package/src/job_request.ts +0 -118
package/src/job.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type * as proto from '@livekit/protocol';
|
|
5
|
+
import type {
|
|
6
|
+
E2EEOptions,
|
|
7
|
+
LocalParticipant,
|
|
8
|
+
RemoteParticipant,
|
|
9
|
+
Room,
|
|
10
|
+
RtcConfiguration,
|
|
11
|
+
} from '@livekit/rtc-node';
|
|
12
|
+
import { ParticipantKind, RoomEvent, TrackKind } from '@livekit/rtc-node';
|
|
13
|
+
import type { Logger } from 'pino';
|
|
14
|
+
import { log } from './log.js';
|
|
15
|
+
|
|
16
|
+
/** Which tracks, if any, should the agent automatically subscribe to? */
|
|
17
|
+
export enum AutoSubscribe {
|
|
18
|
+
SUBSCRIBE_ALL,
|
|
19
|
+
SUBSCRIBE_NONE,
|
|
20
|
+
VIDEO_ONLY,
|
|
21
|
+
AUDIO_ONLY,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type JobAcceptArguments = {
|
|
25
|
+
name: string;
|
|
26
|
+
identity: string;
|
|
27
|
+
metadata: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RunningJobInfo = {
|
|
31
|
+
acceptArguments: JobAcceptArguments;
|
|
32
|
+
job: proto.Job;
|
|
33
|
+
url: string;
|
|
34
|
+
token: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Attempted to add a function callback, but the function already exists. */
|
|
38
|
+
export class FunctionExistsError extends Error {
|
|
39
|
+
constructor(msg?: string) {
|
|
40
|
+
super(msg);
|
|
41
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The job and environment context as seen by the agent, accessible by the entrypoint function. */
|
|
46
|
+
export class JobContext {
|
|
47
|
+
#proc: JobProcess;
|
|
48
|
+
#info: RunningJobInfo;
|
|
49
|
+
#room: Room;
|
|
50
|
+
#onConnect: () => void;
|
|
51
|
+
#onShutdown: (s: string) => void;
|
|
52
|
+
/** @internal */
|
|
53
|
+
shutdownCallbacks: (() => Promise<void>)[] = [];
|
|
54
|
+
#participantEntrypoints: ((job: JobContext, p: RemoteParticipant) => Promise<void>)[] = [];
|
|
55
|
+
#participantTasks: {
|
|
56
|
+
[id: string]: {
|
|
57
|
+
callback: (job: JobContext, p: RemoteParticipant) => Promise<void>;
|
|
58
|
+
result: Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
} = {};
|
|
61
|
+
#logger: Logger;
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
proc: JobProcess,
|
|
65
|
+
info: RunningJobInfo,
|
|
66
|
+
room: Room,
|
|
67
|
+
onConnect: () => void,
|
|
68
|
+
onShutdown: (s: string) => void,
|
|
69
|
+
) {
|
|
70
|
+
this.#proc = proc;
|
|
71
|
+
this.#info = info;
|
|
72
|
+
this.#room = room;
|
|
73
|
+
this.#onConnect = onConnect;
|
|
74
|
+
this.#onShutdown = onShutdown;
|
|
75
|
+
this.onParticipantConnected = this.onParticipantConnected.bind(this);
|
|
76
|
+
this.#room.on(RoomEvent.ParticipantConnected, this.onParticipantConnected);
|
|
77
|
+
this.#logger = log().child({ info: this.#info });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get proc(): JobProcess {
|
|
81
|
+
return this.#proc;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get job(): proto.Job {
|
|
85
|
+
return this.#info.job;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @returns The room the agent was called into */
|
|
89
|
+
get room(): Room {
|
|
90
|
+
return this.#room;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @returns The agent's participant if connected to the room, otherwise `undefined` */
|
|
94
|
+
get agent(): LocalParticipant | undefined {
|
|
95
|
+
return this.#room.localParticipant;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Adds a promise to be awaited when {@link JobContext.shutdown | shutdown} is called. */
|
|
99
|
+
addShutdownCallback(callback: () => Promise<void>) {
|
|
100
|
+
this.shutdownCallbacks.push(callback);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async waitForParticipant(identity?: string): Promise<RemoteParticipant> {
|
|
104
|
+
if (!this.#room.isConnected) {
|
|
105
|
+
throw new Error('room is not connected');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(this.#room.remoteParticipants.values());
|
|
109
|
+
|
|
110
|
+
for (const p of this.#room.remoteParticipants.values()) {
|
|
111
|
+
if ((!identity || p.identity === identity) && p.info.kind != ParticipantKind.AGENT) {
|
|
112
|
+
return p;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
this.#room.once(RoomEvent.ParticipantConnected, () => {
|
|
118
|
+
resolve(this.#room.remoteParticipants.values().next().value);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Connects the agent to the room.
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* It is recommended to run this command as early in the function as possible, as executing it
|
|
128
|
+
* later may cause noticeable delay between user and agent joins.
|
|
129
|
+
*
|
|
130
|
+
* @see {@link https://github.com/livekit/node-sdks/tree/main/packages/livekit-rtc#readme |
|
|
131
|
+
* @livekit/rtc-node} for more information about the parameters.
|
|
132
|
+
*/
|
|
133
|
+
async connect(
|
|
134
|
+
e2ee?: E2EEOptions,
|
|
135
|
+
autoSubscribe: AutoSubscribe = AutoSubscribe.SUBSCRIBE_ALL,
|
|
136
|
+
rtcConfig?: RtcConfiguration,
|
|
137
|
+
) {
|
|
138
|
+
const opts = {
|
|
139
|
+
e2ee,
|
|
140
|
+
autoSubscribe: autoSubscribe == AutoSubscribe.SUBSCRIBE_ALL,
|
|
141
|
+
rtcConfig,
|
|
142
|
+
dynacast: false,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await this.#room.connect(this.#info.url, this.#info.token, opts);
|
|
146
|
+
this.#onConnect();
|
|
147
|
+
|
|
148
|
+
this.#room.remoteParticipants.forEach(this.onParticipantConnected);
|
|
149
|
+
|
|
150
|
+
if ([AutoSubscribe.AUDIO_ONLY, AutoSubscribe.VIDEO_ONLY].includes(autoSubscribe)) {
|
|
151
|
+
this.#room.remoteParticipants.forEach((p) => {
|
|
152
|
+
p.trackPublications.forEach((pub) => {
|
|
153
|
+
if (
|
|
154
|
+
(autoSubscribe === AutoSubscribe.AUDIO_ONLY && pub.kind === TrackKind.KIND_AUDIO) ||
|
|
155
|
+
(autoSubscribe === AutoSubscribe.VIDEO_ONLY && pub.kind === TrackKind.KIND_VIDEO)
|
|
156
|
+
) {
|
|
157
|
+
pub.setSubscribed(true);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Gracefully shuts down the job, and runs all shutdown promises.
|
|
166
|
+
*
|
|
167
|
+
* @param reason - Optional reason for shutdown
|
|
168
|
+
*/
|
|
169
|
+
shutdown(reason = '') {
|
|
170
|
+
this.#onShutdown(reason);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** @internal */
|
|
174
|
+
onParticipantConnected(p: RemoteParticipant) {
|
|
175
|
+
for (const callback of this.#participantEntrypoints) {
|
|
176
|
+
if (
|
|
177
|
+
p.identity in this.#participantTasks &&
|
|
178
|
+
this.#participantTasks[p.identity].callback == callback
|
|
179
|
+
) {
|
|
180
|
+
this.#logger.warn(
|
|
181
|
+
'a participant has joined before a prior prticipant task matching the same identity has finished:',
|
|
182
|
+
p.identity,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const result = callback(this, p);
|
|
186
|
+
result.finally(() => delete this.#participantTasks[p.identity]);
|
|
187
|
+
this.#participantTasks[p.identity] = { callback, result };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Adds a promise to be awaited whenever a new participant joins the room.
|
|
193
|
+
*
|
|
194
|
+
* @throws {@link FunctionExistsError} if an entrypoint already exists
|
|
195
|
+
*/
|
|
196
|
+
addParticipantEntrypoint(callback: (job: JobContext, p: RemoteParticipant) => Promise<void>) {
|
|
197
|
+
if (this.#participantEntrypoints.includes(callback)) {
|
|
198
|
+
throw new FunctionExistsError('entrypoints cannot be added more than once');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.#participantEntrypoints.push(callback);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export class JobProcess {
|
|
206
|
+
#pid = process.pid;
|
|
207
|
+
#userData: { [id: string]: unknown } = {};
|
|
208
|
+
|
|
209
|
+
get pid(): number {
|
|
210
|
+
return this.#pid;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
get userData(): { [id: string]: unknown } {
|
|
214
|
+
return this.#userData;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A request sent by the server to spawn a new agent job.
|
|
220
|
+
*
|
|
221
|
+
* @remarks
|
|
222
|
+
* For most applications, this is best left to the default, which simply accepts the job and
|
|
223
|
+
* handles the logic inside the entrypoint function. This class is useful for vetting which
|
|
224
|
+
* requests should fill idle processes and which should be outright rejected.
|
|
225
|
+
*/
|
|
226
|
+
export class JobRequest {
|
|
227
|
+
#job: proto.Job;
|
|
228
|
+
#onReject: () => Promise<void>;
|
|
229
|
+
#onAccept: (args: JobAcceptArguments) => Promise<void>;
|
|
230
|
+
|
|
231
|
+
/** @internal */
|
|
232
|
+
constructor(
|
|
233
|
+
job: proto.Job,
|
|
234
|
+
onReject: () => Promise<void>,
|
|
235
|
+
onAccept: (args: JobAcceptArguments) => Promise<void>,
|
|
236
|
+
) {
|
|
237
|
+
this.#job = job;
|
|
238
|
+
this.#onReject = onReject;
|
|
239
|
+
this.#onAccept = onAccept;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @returns The ID of the job, set by the LiveKit server */
|
|
243
|
+
get id(): string {
|
|
244
|
+
return this.#job.id;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */
|
|
248
|
+
get job(): proto.Job {
|
|
249
|
+
return this.#job;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */
|
|
253
|
+
get room(): proto.Room | undefined {
|
|
254
|
+
return this.#job.room;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */
|
|
258
|
+
get publisher(): proto.ParticipantInfo | undefined {
|
|
259
|
+
return this.#job.participant;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** @returns The agent's name, as set in {@link WorkerOptions} */
|
|
263
|
+
get agentName(): string {
|
|
264
|
+
return this.#job.agentName;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Rejects the job. */
|
|
268
|
+
async reject() {
|
|
269
|
+
await this.#onReject();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Accepts the job, launching it on an idle child process. */
|
|
273
|
+
async accept(name = '', identity = '', metadata = '') {
|
|
274
|
+
if (identity === '') identity = 'agent-' + this.id;
|
|
275
|
+
|
|
276
|
+
this.#onAccept({ name, identity, metadata });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
// heavily inspired by Vercel AI's `tool()`:
|
|
7
|
+
// https://github.com/vercel/ai/blob/3b0983b/packages/ai/core/tool/tool.ts
|
|
8
|
+
|
|
9
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
10
|
+
|
|
11
|
+
/** Type reinforcement for the callable function's execute parameters. */
|
|
12
|
+
export type inferParameters<P extends z.ZodTypeAny> = z.infer<P>;
|
|
13
|
+
|
|
14
|
+
/** A definition for a function callable by the LLM. */
|
|
15
|
+
export interface CallableFunction<P extends z.ZodTypeAny = any, R = any> {
|
|
16
|
+
description: string;
|
|
17
|
+
parameters: P;
|
|
18
|
+
execute: (args: inferParameters<P>) => PromiseLike<R>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** An object containing callable functions and their names */
|
|
22
|
+
export type FunctionContext = {
|
|
23
|
+
[name: string]: CallableFunction;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** @internal */
|
|
27
|
+
export const oaiParams = (p: z.AnyZodObject) => {
|
|
28
|
+
const properties: Record<string, any> = {};
|
|
29
|
+
const required_properties: string[] = [];
|
|
30
|
+
|
|
31
|
+
for (const key in p.shape) {
|
|
32
|
+
const field = p.shape[key];
|
|
33
|
+
const description = field._def.description || undefined;
|
|
34
|
+
let type: string;
|
|
35
|
+
let enumValues: any[] | undefined;
|
|
36
|
+
|
|
37
|
+
if (field instanceof z.ZodEnum) {
|
|
38
|
+
enumValues = field._def.values;
|
|
39
|
+
type = typeof enumValues![0];
|
|
40
|
+
} else {
|
|
41
|
+
type = field._def.typeName.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
properties[key] = {
|
|
45
|
+
type: type.includes('zod') ? type.substring(3) : type,
|
|
46
|
+
description,
|
|
47
|
+
enum: enumValues,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (!field._def.defaultValue) {
|
|
51
|
+
required_properties.push(key);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const type = 'object' as const;
|
|
56
|
+
return {
|
|
57
|
+
type,
|
|
58
|
+
properties,
|
|
59
|
+
required_properties,
|
|
60
|
+
};
|
|
61
|
+
};
|
package/src/llm/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import {
|
|
5
|
+
type CallableFunction,
|
|
6
|
+
type FunctionContext,
|
|
7
|
+
type inferParameters,
|
|
8
|
+
oaiParams,
|
|
9
|
+
} from './function_context.js';
|
|
10
|
+
|
|
11
|
+
export { CallableFunction, FunctionContext, inferParameters, oaiParams };
|
package/src/log.ts
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { Logger } from 'pino';
|
|
4
5
|
import { pino } from 'pino';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
/** @internal */
|
|
8
|
+
export type LoggerOptions = {
|
|
9
|
+
pretty: boolean;
|
|
10
|
+
level?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** @internal */
|
|
14
|
+
export let loggerOptions: LoggerOptions;
|
|
15
|
+
|
|
16
|
+
/** @internal */
|
|
17
|
+
let logger: Logger | undefined = undefined;
|
|
18
|
+
|
|
19
|
+
/** @internal */
|
|
20
|
+
export const log = () => {
|
|
21
|
+
if (!logger) {
|
|
22
|
+
throw new TypeError('logger not initialized. did you forget to run initializeLogger()?');
|
|
23
|
+
}
|
|
24
|
+
return logger;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** @internal */
|
|
28
|
+
export const initializeLogger = ({ pretty, level }: LoggerOptions) => {
|
|
29
|
+
loggerOptions = { pretty, level };
|
|
30
|
+
logger = pino(
|
|
31
|
+
pretty
|
|
32
|
+
? {
|
|
33
|
+
transport: {
|
|
34
|
+
target: 'pino-pretty',
|
|
35
|
+
options: {
|
|
36
|
+
colorize: true,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
: {},
|
|
41
|
+
);
|
|
42
|
+
if (level) {
|
|
43
|
+
logger.level = level;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { AudioFrame } from '@livekit/rtc-node';
|
|
5
|
+
import { type AudioSource } from '@livekit/rtc-node';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import { AudioByteStream } from '../audio.js';
|
|
8
|
+
import type { TranscriptionForwarder } from '../transcription.js';
|
|
9
|
+
import { type AsyncIterableQueue, CancellablePromise, Future, gracefullyCancel } from '../utils.js';
|
|
10
|
+
|
|
11
|
+
export const proto = {};
|
|
12
|
+
|
|
13
|
+
export class PlayoutHandle extends EventEmitter {
|
|
14
|
+
#audioSource: AudioSource;
|
|
15
|
+
#sampleRate: number;
|
|
16
|
+
#itemId: string;
|
|
17
|
+
#contentIndex: number;
|
|
18
|
+
/** @internal */
|
|
19
|
+
transcriptionFwd: TranscriptionForwarder;
|
|
20
|
+
/** @internal */
|
|
21
|
+
doneFut: Future;
|
|
22
|
+
/** @internal */
|
|
23
|
+
intFut: Future;
|
|
24
|
+
/** @internal */
|
|
25
|
+
#interrupted: boolean;
|
|
26
|
+
/** @internal */
|
|
27
|
+
pushedDuration: number;
|
|
28
|
+
/** @internal */
|
|
29
|
+
totalPlayedTime: number | undefined; // Set when playout is done
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
audioSource: AudioSource,
|
|
33
|
+
sampleRate: number,
|
|
34
|
+
itemId: string,
|
|
35
|
+
contentIndex: number,
|
|
36
|
+
transcriptionFwd: TranscriptionForwarder,
|
|
37
|
+
) {
|
|
38
|
+
super();
|
|
39
|
+
this.#audioSource = audioSource;
|
|
40
|
+
this.#sampleRate = sampleRate;
|
|
41
|
+
this.#itemId = itemId;
|
|
42
|
+
this.#contentIndex = contentIndex;
|
|
43
|
+
this.transcriptionFwd = transcriptionFwd;
|
|
44
|
+
this.doneFut = new Future();
|
|
45
|
+
this.intFut = new Future();
|
|
46
|
+
this.#interrupted = false;
|
|
47
|
+
this.pushedDuration = 0;
|
|
48
|
+
this.totalPlayedTime = undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get itemId(): string {
|
|
52
|
+
return this.#itemId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get audioSamples(): number {
|
|
56
|
+
if (this.totalPlayedTime !== undefined) {
|
|
57
|
+
return Math.floor(this.totalPlayedTime * this.#sampleRate);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Math.floor(this.pushedDuration - this.#audioSource.queuedDuration * this.#sampleRate);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get textChars(): number {
|
|
64
|
+
return this.transcriptionFwd.currentCharacterIndex;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get contentIndex(): number {
|
|
68
|
+
return this.#contentIndex;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get interrupted(): boolean {
|
|
72
|
+
return this.#interrupted;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get done(): boolean {
|
|
76
|
+
return this.doneFut.done || this.#interrupted;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interrupt() {
|
|
80
|
+
if (this.doneFut.done) return;
|
|
81
|
+
this.intFut.resolve();
|
|
82
|
+
this.#interrupted = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class AgentPlayout {
|
|
87
|
+
#audioSource: AudioSource;
|
|
88
|
+
#playoutTask: CancellablePromise<void> | null;
|
|
89
|
+
#sampleRate: number;
|
|
90
|
+
#numChannels: number;
|
|
91
|
+
#inFrameSize: number;
|
|
92
|
+
#outFrameSize: number;
|
|
93
|
+
constructor(
|
|
94
|
+
audioSource: AudioSource,
|
|
95
|
+
sampleRate: number,
|
|
96
|
+
numChannels: number,
|
|
97
|
+
inFrameSize: number,
|
|
98
|
+
outFrameSize: number,
|
|
99
|
+
) {
|
|
100
|
+
this.#audioSource = audioSource;
|
|
101
|
+
this.#playoutTask = null;
|
|
102
|
+
this.#sampleRate = sampleRate;
|
|
103
|
+
this.#numChannels = numChannels;
|
|
104
|
+
this.#inFrameSize = inFrameSize;
|
|
105
|
+
this.#outFrameSize = outFrameSize;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
play(
|
|
109
|
+
itemId: string,
|
|
110
|
+
contentIndex: number,
|
|
111
|
+
transcriptionFwd: TranscriptionForwarder,
|
|
112
|
+
textStream: AsyncIterableQueue<string>,
|
|
113
|
+
audioStream: AsyncIterableQueue<AudioFrame>,
|
|
114
|
+
): PlayoutHandle {
|
|
115
|
+
const handle = new PlayoutHandle(
|
|
116
|
+
this.#audioSource,
|
|
117
|
+
this.#sampleRate,
|
|
118
|
+
itemId,
|
|
119
|
+
contentIndex,
|
|
120
|
+
transcriptionFwd,
|
|
121
|
+
);
|
|
122
|
+
this.#playoutTask = this.#makePlayoutTask(this.#playoutTask, handle, textStream, audioStream);
|
|
123
|
+
return handle;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#makePlayoutTask(
|
|
127
|
+
oldTask: CancellablePromise<void> | null,
|
|
128
|
+
handle: PlayoutHandle,
|
|
129
|
+
textStream: AsyncIterableQueue<string>,
|
|
130
|
+
audioStream: AsyncIterableQueue<AudioFrame>,
|
|
131
|
+
): CancellablePromise<void> {
|
|
132
|
+
return new CancellablePromise<void>((resolve, reject, onCancel) => {
|
|
133
|
+
let cancelled = false;
|
|
134
|
+
onCancel(() => {
|
|
135
|
+
cancelled = true;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
(async () => {
|
|
139
|
+
try {
|
|
140
|
+
if (oldTask) {
|
|
141
|
+
await gracefullyCancel(oldTask);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let firstFrame = true;
|
|
145
|
+
|
|
146
|
+
const readText = () =>
|
|
147
|
+
new CancellablePromise<void>((resolveText, rejectText, onCancelText) => {
|
|
148
|
+
let cancelledText = false;
|
|
149
|
+
onCancelText(() => {
|
|
150
|
+
cancelledText = true;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
(async () => {
|
|
154
|
+
try {
|
|
155
|
+
for await (const text of textStream) {
|
|
156
|
+
if (cancelledText || cancelled) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
handle.transcriptionFwd.pushText(text);
|
|
160
|
+
}
|
|
161
|
+
resolveText();
|
|
162
|
+
} catch (error) {
|
|
163
|
+
rejectText(error);
|
|
164
|
+
}
|
|
165
|
+
})();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const capture = () =>
|
|
169
|
+
new CancellablePromise<void>((resolveCapture, rejectCapture, onCancelCapture) => {
|
|
170
|
+
let cancelledCapture = false;
|
|
171
|
+
onCancelCapture(() => {
|
|
172
|
+
cancelledCapture = true;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
(async () => {
|
|
176
|
+
try {
|
|
177
|
+
const samplesPerChannel = this.#outFrameSize;
|
|
178
|
+
const bstream = new AudioByteStream(
|
|
179
|
+
this.#sampleRate,
|
|
180
|
+
this.#numChannels,
|
|
181
|
+
samplesPerChannel,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
for await (const frame of audioStream) {
|
|
185
|
+
if (cancelledCapture || cancelled) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
if (firstFrame) {
|
|
189
|
+
handle.transcriptionFwd.start();
|
|
190
|
+
firstFrame = false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
handle.transcriptionFwd.pushAudio(frame);
|
|
194
|
+
|
|
195
|
+
for (const f of bstream.write(frame.data.buffer)) {
|
|
196
|
+
handle.pushedDuration += f.samplesPerChannel / f.sampleRate;
|
|
197
|
+
await this.#audioSource.captureFrame(f);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!cancelledCapture && !cancelled) {
|
|
202
|
+
for (const f of bstream.flush()) {
|
|
203
|
+
handle.pushedDuration += f.samplesPerChannel / f.sampleRate;
|
|
204
|
+
await this.#audioSource.captureFrame(f);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
handle.transcriptionFwd.markAudioComplete();
|
|
208
|
+
|
|
209
|
+
await this.#audioSource.waitForPlayout();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
resolveCapture();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
rejectCapture(error);
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const readTextTask = readText();
|
|
220
|
+
const captureTask = capture();
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await Promise.race([captureTask, handle.intFut.await]);
|
|
224
|
+
} finally {
|
|
225
|
+
if (!captureTask.isCancelled) {
|
|
226
|
+
await gracefullyCancel(captureTask);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
handle.totalPlayedTime = handle.pushedDuration - this.#audioSource.queuedDuration;
|
|
230
|
+
|
|
231
|
+
if (handle.interrupted || captureTask.error) {
|
|
232
|
+
this.#audioSource.clearQueue(); // make sure to remove any queued frames
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!readTextTask.isCancelled) {
|
|
236
|
+
await gracefullyCancel(readTextTask);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!firstFrame && !handle.interrupted) {
|
|
240
|
+
handle.transcriptionFwd.markTextComplete();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
handle.doneFut.resolve();
|
|
244
|
+
await handle.transcriptionFwd.close(handle.interrupted);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
resolve();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
reject(error);
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|