@livekit/agents-plugin-lemonslice 1.0.49 → 1.0.51
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/README.md +6 -0
- package/dist/avatar.cjs +10 -2
- package/dist/avatar.cjs.map +1 -1
- package/dist/avatar.d.cts +7 -1
- package/dist/avatar.d.ts +7 -1
- package/dist/avatar.d.ts.map +1 -1
- package/dist/avatar.js +10 -2
- package/dist/avatar.js.map +1 -1
- package/dist/avatar.test.cjs +69 -0
- package/dist/avatar.test.cjs.map +1 -0
- package/dist/avatar.test.d.cts +2 -0
- package/dist/avatar.test.d.ts +2 -0
- package/dist/avatar.test.d.ts.map +1 -0
- package/dist/avatar.test.js +68 -0
- package/dist/avatar.test.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/avatar.test.ts +88 -0
- package/src/avatar.ts +18 -4
package/README.md
CHANGED
|
@@ -28,6 +28,9 @@ import { AvatarSession } from '@livekit/agents-plugin-lemonslice';
|
|
|
28
28
|
const avatar = new AvatarSession({
|
|
29
29
|
agentImageUrl: 'publicly-accessible-image-url',
|
|
30
30
|
apiKey: 'your-lemonslice-api-key', // or set LEMONSLICE_API_KEY env var
|
|
31
|
+
extraPayload: {
|
|
32
|
+
aspect_ratio: '9x16',
|
|
33
|
+
},
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
// Start the avatar session after creating your agent session
|
|
@@ -49,12 +52,15 @@ Set `LEMONSLICE_API_KEY` and `LEMONSLICE_IMAGE_URL` to get up and running.
|
|
|
49
52
|
|--------|------|-------------|
|
|
50
53
|
| `agentId` | `string` | The LemonSlice agent ID to use. Either `agentId` or `agentImageUrl` must be provided. |
|
|
51
54
|
| `agentImageUrl` | `AvatarImage` | A publicly accessible url to your avatar image. Either `agentId` or `agentImageUrl` must be provided. |
|
|
55
|
+
| `extraPayload` | `Record<string, unknown>` | Additional LemonSlice session payload fields to forward to LemonSlice. |
|
|
52
56
|
| `apiUrl` | `string` | The LemonSlice API URL. Defaults to `LEMONSLICE_API_URL` env var or the default LemonSlice API endpoint. |
|
|
53
57
|
| `apiKey` | `string` | The LemonSlice API key. Defaults to `LEMONSLICE_API_KEY` env var. |
|
|
54
58
|
| `avatarParticipantIdentity` | `string` | The identity of the avatar participant in the room. Defaults to `'lemonslice-avatar-agent'`. |
|
|
55
59
|
| `avatarParticipantName` | `string` | The name of the avatar participant in the room. Defaults to `'lemonslice-avatar-agent'`. |
|
|
56
60
|
| `connOptions` | `APIConnectOptions` | Connection options for API requests (retry count, timeout, etc.). |
|
|
57
61
|
|
|
62
|
+
Use `extraPayload` for LemonSlice API fields that are not yet modeled directly by the SDK.
|
|
63
|
+
|
|
58
64
|
## Environment Variables
|
|
59
65
|
|
|
60
66
|
| Variable | Description |
|
package/dist/avatar.cjs
CHANGED
|
@@ -42,6 +42,7 @@ class AvatarSession {
|
|
|
42
42
|
agentImageUrl;
|
|
43
43
|
agentPrompt;
|
|
44
44
|
idleTimeout;
|
|
45
|
+
extraPayload;
|
|
45
46
|
apiUrl;
|
|
46
47
|
apiKey;
|
|
47
48
|
avatarParticipantIdentity;
|
|
@@ -65,6 +66,7 @@ class AvatarSession {
|
|
|
65
66
|
}
|
|
66
67
|
this.agentPrompt = options.agentPrompt ?? null;
|
|
67
68
|
this.idleTimeout = options.idleTimeout ?? null;
|
|
69
|
+
this.extraPayload = options.extraPayload ?? null;
|
|
68
70
|
this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;
|
|
69
71
|
this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || "";
|
|
70
72
|
if (!this.apiKey) {
|
|
@@ -88,6 +90,7 @@ class AvatarSession {
|
|
|
88
90
|
* @param room - The LiveKit room where the avatar will join
|
|
89
91
|
* @param options - Optional LiveKit credentials (falls back to environment variables)
|
|
90
92
|
* @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start
|
|
93
|
+
* @returns The session ID of the LemonSlice session
|
|
91
94
|
*/
|
|
92
95
|
async start(agentSession, room, options = {}) {
|
|
93
96
|
var _a;
|
|
@@ -129,13 +132,14 @@ class AvatarSession {
|
|
|
129
132
|
};
|
|
130
133
|
const livekitToken = await at.toJwt();
|
|
131
134
|
this.#logger.debug("starting avatar session");
|
|
132
|
-
await this.startAgent(livekitUrl, livekitToken);
|
|
135
|
+
const sessionId = await this.startAgent(livekitUrl, livekitToken);
|
|
133
136
|
agentSession.output.audio = new import_agents.voice.DataStreamAudioOutput({
|
|
134
137
|
room,
|
|
135
138
|
destinationIdentity: this.avatarParticipantIdentity,
|
|
136
139
|
sampleRate: SAMPLE_RATE,
|
|
137
140
|
waitRemoteTrack: import_rtc_node.TrackKind.KIND_VIDEO
|
|
138
141
|
});
|
|
142
|
+
return sessionId;
|
|
139
143
|
}
|
|
140
144
|
async startAgent(livekitUrl, livekitToken) {
|
|
141
145
|
for (let i = 0; i <= this.connOptions.maxRetry; i++) {
|
|
@@ -159,6 +163,9 @@ class AvatarSession {
|
|
|
159
163
|
if (this.idleTimeout !== null) {
|
|
160
164
|
payload.idle_timeout = this.idleTimeout;
|
|
161
165
|
}
|
|
166
|
+
if (this.extraPayload) {
|
|
167
|
+
Object.assign(payload, this.extraPayload);
|
|
168
|
+
}
|
|
162
169
|
const response = await fetch(this.apiUrl, {
|
|
163
170
|
method: "POST",
|
|
164
171
|
headers: {
|
|
@@ -175,7 +182,8 @@ class AvatarSession {
|
|
|
175
182
|
options: { statusCode: response.status, body: { error: text } }
|
|
176
183
|
});
|
|
177
184
|
}
|
|
178
|
-
|
|
185
|
+
const data = await response.json();
|
|
186
|
+
return data.session_id;
|
|
179
187
|
} catch (e) {
|
|
180
188
|
if (e instanceof import_agents.APIStatusError && !e.retryable) {
|
|
181
189
|
throw e;
|
package/dist/avatar.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/avatar.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n DEFAULT_API_CONNECT_OPTIONS,\n getJobContext,\n intervalForRetry,\n voice,\n} from '@livekit/agents';\nimport type { Room } from '@livekit/rtc-node';\nimport { TrackKind } from '@livekit/rtc-node';\nimport type { VideoGrant } from 'livekit-server-sdk';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { log } from './log.js';\n\nconst ATTRIBUTE_PUBLISH_ON_BEHALF = 'lk.publish_on_behalf';\nconst DEFAULT_API_URL = 'https://lemonslice.com/api/liveai/sessions';\nconst SAMPLE_RATE = 16000;\nconst AVATAR_AGENT_IDENTITY = 'lemonslice-avatar-agent';\nconst AVATAR_AGENT_NAME = 'lemonslice-avatar-agent';\n\n/**\n * Exception thrown when there are errors with the LemonSlice API.\n */\nexport class LemonSliceException extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LemonSliceException';\n }\n}\n\n/**\n * Options for configuring an AvatarSession.\n */\nexport interface AvatarSessionOptions {\n /**\n * The ID of the LemonSlice agent to add to the session.\n * Either agentId or agentImageUrl must be provided.\n */\n agentId?: string | null;\n /**\n * The URL of the image to use as the agent's avatar.\n * Either agentId or agentImageUrl must be provided.\n */\n agentImageUrl?: string | null;\n /**\n * A prompt that subtly influences the avatar's movements and expressions.\n */\n agentPrompt?: string | null;\n /**\n * The idle timeout, in seconds. Defaults to 60 seconds.\n */\n idleTimeout?: number | null;\n /**\n * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions\n * or LEMONSLICE_API_URL environment variable.\n */\n apiUrl?: string;\n /**\n * The LemonSlice API key. Can also be set via LEMONSLICE_API_KEY environment variable.\n */\n apiKey?: string;\n /**\n * The identity of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantIdentity?: string;\n /**\n * The name of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantName?: string;\n /**\n * Connection options for API requests.\n */\n connOptions?: APIConnectOptions;\n}\n\n/**\n * Options for starting an avatar session.\n */\nexport interface StartOptions {\n /**\n * LiveKit server URL. Falls back to LIVEKIT_URL environment variable.\n */\n livekitUrl?: string;\n /**\n * LiveKit API key. Falls back to LIVEKIT_API_KEY environment variable.\n */\n livekitApiKey?: string;\n /**\n * LiveKit API secret. Falls back to LIVEKIT_API_SECRET environment variable.\n */\n livekitApiSecret?: string;\n}\n\n/**\n * A LemonSlice avatar session.\n *\n * This class manages the connection between a LiveKit agent and a LemonSlice avatar,\n * routing agent audio output to the avatar for visual representation.\n *\n * @example\n * ```typescript\n * // Using an agent ID\n * const avatar = new AvatarSession({\n * agentId: 'your-agent-id',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n *\n * // Using a custom avatar image\n * const avatar = new AvatarSession({\n * agentImageUrl: 'your-image-url',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n * ```\n */\nexport class AvatarSession {\n private agentId: string | null;\n private agentImageUrl: string | null;\n private agentPrompt: string | null;\n private idleTimeout: number | null;\n private apiUrl: string;\n private apiKey: string;\n private avatarParticipantIdentity: string;\n private avatarParticipantName: string;\n private connOptions: APIConnectOptions;\n\n #logger = log();\n\n /**\n * Creates a new AvatarSession.\n *\n * @param options - Configuration options for the avatar session\n * @throws LemonSliceException if invalid agentId or agentImageUrl is provided, or if LemonSlice API key is not set\n */\n constructor(options: AvatarSessionOptions = {}) {\n this.agentId = options.agentId ?? null;\n this.agentImageUrl = options.agentImageUrl ?? null;\n\n if (!this.agentId && !this.agentImageUrl) {\n throw new LemonSliceException('Missing agentId or agentImageUrl');\n }\n if (this.agentId && this.agentImageUrl) {\n throw new LemonSliceException('Only one of agentId or agentImageUrl can be provided');\n }\n\n this.agentPrompt = options.agentPrompt ?? null;\n this.idleTimeout = options.idleTimeout ?? null;\n\n this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;\n this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || '';\n\n if (!this.apiKey) {\n throw new LemonSliceException(\n 'The api_key must be set either by passing apiKey to the client or ' +\n 'by setting the LEMONSLICE_API_KEY environment variable',\n );\n }\n\n this.avatarParticipantIdentity = options.avatarParticipantIdentity || AVATAR_AGENT_IDENTITY;\n this.avatarParticipantName = options.avatarParticipantName || AVATAR_AGENT_NAME;\n this.connOptions = options.connOptions || DEFAULT_API_CONNECT_OPTIONS;\n }\n\n /**\n * Starts the avatar session and connects it to the agent.\n *\n * This method:\n * 1. Creates a LiveKit token for the avatar participant\n * 2. Calls the LemonSlice API to start the avatar session\n * 3. Configures the agent's audio output to stream to the avatar\n *\n * @param agentSession - The agent session to connect to the avatar\n * @param room - The LiveKit room where the avatar will join\n * @param options - Optional LiveKit credentials (falls back to environment variables)\n * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start\n */\n async start(\n agentSession: voice.AgentSession,\n room: Room,\n options: StartOptions = {},\n ): Promise<void> {\n const livekitUrl = options.livekitUrl || process.env.LIVEKIT_URL;\n const livekitApiKey = options.livekitApiKey || process.env.LIVEKIT_API_KEY;\n const livekitApiSecret = options.livekitApiSecret || process.env.LIVEKIT_API_SECRET;\n\n if (!livekitUrl || !livekitApiKey || !livekitApiSecret) {\n throw new LemonSliceException(\n 'livekitUrl, livekitApiKey, and livekitApiSecret must be set ' +\n 'by arguments or environment variables',\n );\n }\n\n let localParticipantIdentity: string;\n try {\n const jobCtx = getJobContext();\n localParticipantIdentity = jobCtx.agent?.identity || '';\n if (!localParticipantIdentity && room.localParticipant) {\n localParticipantIdentity = room.localParticipant.identity;\n }\n } catch {\n if (!room.isConnected || !room.localParticipant) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n localParticipantIdentity = room.localParticipant.identity;\n }\n\n if (!localParticipantIdentity) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n\n const at = new AccessToken(livekitApiKey, livekitApiSecret, {\n identity: this.avatarParticipantIdentity,\n name: this.avatarParticipantName,\n });\n at.kind = 'agent';\n\n at.addGrant({\n roomJoin: true,\n room: room.name,\n } as VideoGrant);\n\n // allow the avatar agent to publish audio and video on behalf of your local agent\n at.attributes = {\n [ATTRIBUTE_PUBLISH_ON_BEHALF]: localParticipantIdentity,\n };\n\n const livekitToken = await at.toJwt();\n\n this.#logger.debug('starting avatar session');\n await this.startAgent(livekitUrl, livekitToken);\n\n agentSession.output.audio = new voice.DataStreamAudioOutput({\n room,\n destinationIdentity: this.avatarParticipantIdentity,\n sampleRate: SAMPLE_RATE,\n waitRemoteTrack: TrackKind.KIND_VIDEO,\n });\n }\n\n private async startAgent(livekitUrl: string, livekitToken: string): Promise<void> {\n for (let i = 0; i <= this.connOptions.maxRetry; i++) {\n try {\n const payload: Record<string, any> = {\n transport_type: 'livekit',\n properties: {\n livekit_url: livekitUrl,\n livekit_token: livekitToken,\n },\n };\n\n if (this.agentId) {\n payload.agent_id = this.agentId;\n }\n\n if (this.agentImageUrl) {\n payload.agent_image_url = this.agentImageUrl;\n }\n\n if (this.agentPrompt) {\n payload.agent_prompt = this.agentPrompt;\n }\n\n if (this.idleTimeout !== null) {\n payload.idle_timeout = this.idleTimeout;\n }\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.connOptions.timeoutMs),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new APIStatusError({\n message: 'Server returned an error',\n options: { statusCode: response.status, body: { error: text } },\n });\n }\n return;\n } catch (e) {\n if (e instanceof APIStatusError && !e.retryable) {\n throw e;\n }\n if (e instanceof APIConnectionError) {\n this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api');\n } else {\n this.#logger.error({ error: e }, 'failed to call lemonslice api');\n }\n\n if (i <= this.connOptions.maxRetry - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, intervalForRetry(this.connOptions, i)),\n );\n }\n }\n }\n\n throw new APIConnectionError({\n message: 'Failed to start LemonSlice Avatar Session after all retries',\n });\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/avatar.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n DEFAULT_API_CONNECT_OPTIONS,\n getJobContext,\n intervalForRetry,\n voice,\n} from '@livekit/agents';\nimport type { Room } from '@livekit/rtc-node';\nimport { TrackKind } from '@livekit/rtc-node';\nimport type { VideoGrant } from 'livekit-server-sdk';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { log } from './log.js';\n\nconst ATTRIBUTE_PUBLISH_ON_BEHALF = 'lk.publish_on_behalf';\nconst DEFAULT_API_URL = 'https://lemonslice.com/api/liveai/sessions';\nconst SAMPLE_RATE = 16000;\nconst AVATAR_AGENT_IDENTITY = 'lemonslice-avatar-agent';\nconst AVATAR_AGENT_NAME = 'lemonslice-avatar-agent';\n\n/**\n * Exception thrown when there are errors with the LemonSlice API.\n */\nexport class LemonSliceException extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LemonSliceException';\n }\n}\n\n/**\n * Options for configuring an AvatarSession.\n */\nexport interface AvatarSessionOptions {\n /**\n * The ID of the LemonSlice agent to add to the session.\n * Either agentId or agentImageUrl must be provided.\n */\n agentId?: string | null;\n /**\n * The URL of the image to use as the agent's avatar.\n * Either agentId or agentImageUrl must be provided.\n */\n agentImageUrl?: string | null;\n /**\n * A prompt that subtly influences the avatar's movements and expressions.\n */\n agentPrompt?: string | null;\n /**\n * The idle timeout, in seconds. Defaults to 60 seconds.\n */\n idleTimeout?: number | null;\n /**\n * Additional payload fields to merge into the LemonSlice session creation request.\n */\n extraPayload?: Record<string, unknown> | null;\n /**\n * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions\n * or LEMONSLICE_API_URL environment variable.\n */\n apiUrl?: string;\n /**\n * The LemonSlice API key. Can also be set via LEMONSLICE_API_KEY environment variable.\n */\n apiKey?: string;\n /**\n * The identity of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantIdentity?: string;\n /**\n * The name of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantName?: string;\n /**\n * Connection options for API requests.\n */\n connOptions?: APIConnectOptions;\n}\n\n/**\n * Options for starting an avatar session.\n */\nexport interface StartOptions {\n /**\n * LiveKit server URL. Falls back to LIVEKIT_URL environment variable.\n */\n livekitUrl?: string;\n /**\n * LiveKit API key. Falls back to LIVEKIT_API_KEY environment variable.\n */\n livekitApiKey?: string;\n /**\n * LiveKit API secret. Falls back to LIVEKIT_API_SECRET environment variable.\n */\n livekitApiSecret?: string;\n}\n\n/**\n * A LemonSlice avatar session.\n *\n * This class manages the connection between a LiveKit agent and a LemonSlice avatar,\n * routing agent audio output to the avatar for visual representation.\n *\n * @example\n * ```typescript\n * // Using an agent ID\n * const avatar = new AvatarSession({\n * agentId: 'your-agent-id',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n *\n * // Using a custom avatar image\n * const avatar = new AvatarSession({\n * agentImageUrl: 'your-image-url',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n * ```\n */\nexport class AvatarSession {\n private agentId: string | null;\n private agentImageUrl: string | null;\n private agentPrompt: string | null;\n private idleTimeout: number | null;\n private extraPayload: Record<string, unknown> | null;\n private apiUrl: string;\n private apiKey: string;\n private avatarParticipantIdentity: string;\n private avatarParticipantName: string;\n private connOptions: APIConnectOptions;\n\n #logger = log();\n\n /**\n * Creates a new AvatarSession.\n *\n * @param options - Configuration options for the avatar session\n * @throws LemonSliceException if invalid agentId or agentImageUrl is provided, or if LemonSlice API key is not set\n */\n constructor(options: AvatarSessionOptions = {}) {\n this.agentId = options.agentId ?? null;\n this.agentImageUrl = options.agentImageUrl ?? null;\n\n if (!this.agentId && !this.agentImageUrl) {\n throw new LemonSliceException('Missing agentId or agentImageUrl');\n }\n if (this.agentId && this.agentImageUrl) {\n throw new LemonSliceException('Only one of agentId or agentImageUrl can be provided');\n }\n\n this.agentPrompt = options.agentPrompt ?? null;\n this.idleTimeout = options.idleTimeout ?? null;\n this.extraPayload = options.extraPayload ?? null;\n\n this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;\n this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || '';\n\n if (!this.apiKey) {\n throw new LemonSliceException(\n 'The api_key must be set either by passing apiKey to the client or ' +\n 'by setting the LEMONSLICE_API_KEY environment variable',\n );\n }\n\n this.avatarParticipantIdentity = options.avatarParticipantIdentity || AVATAR_AGENT_IDENTITY;\n this.avatarParticipantName = options.avatarParticipantName || AVATAR_AGENT_NAME;\n this.connOptions = options.connOptions || DEFAULT_API_CONNECT_OPTIONS;\n }\n\n /**\n * Starts the avatar session and connects it to the agent.\n *\n * This method:\n * 1. Creates a LiveKit token for the avatar participant\n * 2. Calls the LemonSlice API to start the avatar session\n * 3. Configures the agent's audio output to stream to the avatar\n *\n * @param agentSession - The agent session to connect to the avatar\n * @param room - The LiveKit room where the avatar will join\n * @param options - Optional LiveKit credentials (falls back to environment variables)\n * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start\n * @returns The session ID of the LemonSlice session\n */\n async start(\n agentSession: voice.AgentSession,\n room: Room,\n options: StartOptions = {},\n ): Promise<string> {\n const livekitUrl = options.livekitUrl || process.env.LIVEKIT_URL;\n const livekitApiKey = options.livekitApiKey || process.env.LIVEKIT_API_KEY;\n const livekitApiSecret = options.livekitApiSecret || process.env.LIVEKIT_API_SECRET;\n\n if (!livekitUrl || !livekitApiKey || !livekitApiSecret) {\n throw new LemonSliceException(\n 'livekitUrl, livekitApiKey, and livekitApiSecret must be set ' +\n 'by arguments or environment variables',\n );\n }\n\n let localParticipantIdentity: string;\n try {\n const jobCtx = getJobContext();\n localParticipantIdentity = jobCtx.agent?.identity || '';\n if (!localParticipantIdentity && room.localParticipant) {\n localParticipantIdentity = room.localParticipant.identity;\n }\n } catch {\n if (!room.isConnected || !room.localParticipant) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n localParticipantIdentity = room.localParticipant.identity;\n }\n\n if (!localParticipantIdentity) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n\n const at = new AccessToken(livekitApiKey, livekitApiSecret, {\n identity: this.avatarParticipantIdentity,\n name: this.avatarParticipantName,\n });\n at.kind = 'agent';\n\n at.addGrant({\n roomJoin: true,\n room: room.name,\n } as VideoGrant);\n\n // allow the avatar agent to publish audio and video on behalf of your local agent\n at.attributes = {\n [ATTRIBUTE_PUBLISH_ON_BEHALF]: localParticipantIdentity,\n };\n\n const livekitToken = await at.toJwt();\n\n this.#logger.debug('starting avatar session');\n const sessionId = await this.startAgent(livekitUrl, livekitToken);\n\n agentSession.output.audio = new voice.DataStreamAudioOutput({\n room,\n destinationIdentity: this.avatarParticipantIdentity,\n sampleRate: SAMPLE_RATE,\n waitRemoteTrack: TrackKind.KIND_VIDEO,\n });\n\n return sessionId;\n }\n\n private async startAgent(livekitUrl: string, livekitToken: string): Promise<string> {\n for (let i = 0; i <= this.connOptions.maxRetry; i++) {\n try {\n const payload: Record<string, any> = {\n transport_type: 'livekit',\n properties: {\n livekit_url: livekitUrl,\n livekit_token: livekitToken,\n },\n };\n\n if (this.agentId) {\n payload.agent_id = this.agentId;\n }\n\n if (this.agentImageUrl) {\n payload.agent_image_url = this.agentImageUrl;\n }\n\n if (this.agentPrompt) {\n payload.agent_prompt = this.agentPrompt;\n }\n\n if (this.idleTimeout !== null) {\n payload.idle_timeout = this.idleTimeout;\n }\n\n if (this.extraPayload) {\n Object.assign(payload, this.extraPayload);\n }\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.connOptions.timeoutMs),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new APIStatusError({\n message: 'Server returned an error',\n options: { statusCode: response.status, body: { error: text } },\n });\n }\n const data = (await response.json()) as { session_id: string };\n return data.session_id;\n } catch (e) {\n if (e instanceof APIStatusError && !e.retryable) {\n throw e;\n }\n if (e instanceof APIConnectionError) {\n this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api');\n } else {\n this.#logger.error({ error: e }, 'failed to call lemonslice api');\n }\n\n if (i <= this.connOptions.maxRetry - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, intervalForRetry(this.connOptions, i)),\n );\n }\n }\n }\n\n throw new APIConnectionError({\n message: 'Failed to start LemonSlice Avatar Session after all retries',\n });\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/avatar.d.cts
CHANGED
|
@@ -28,6 +28,10 @@ export interface AvatarSessionOptions {
|
|
|
28
28
|
* The idle timeout, in seconds. Defaults to 60 seconds.
|
|
29
29
|
*/
|
|
30
30
|
idleTimeout?: number | null;
|
|
31
|
+
/**
|
|
32
|
+
* Additional payload fields to merge into the LemonSlice session creation request.
|
|
33
|
+
*/
|
|
34
|
+
extraPayload?: Record<string, unknown> | null;
|
|
31
35
|
/**
|
|
32
36
|
* The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions
|
|
33
37
|
* or LEMONSLICE_API_URL environment variable.
|
|
@@ -96,6 +100,7 @@ export declare class AvatarSession {
|
|
|
96
100
|
private agentImageUrl;
|
|
97
101
|
private agentPrompt;
|
|
98
102
|
private idleTimeout;
|
|
103
|
+
private extraPayload;
|
|
99
104
|
private apiUrl;
|
|
100
105
|
private apiKey;
|
|
101
106
|
private avatarParticipantIdentity;
|
|
@@ -120,8 +125,9 @@ export declare class AvatarSession {
|
|
|
120
125
|
* @param room - The LiveKit room where the avatar will join
|
|
121
126
|
* @param options - Optional LiveKit credentials (falls back to environment variables)
|
|
122
127
|
* @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start
|
|
128
|
+
* @returns The session ID of the LemonSlice session
|
|
123
129
|
*/
|
|
124
|
-
start(agentSession: voice.AgentSession, room: Room, options?: StartOptions): Promise<
|
|
130
|
+
start(agentSession: voice.AgentSession, room: Room, options?: StartOptions): Promise<string>;
|
|
125
131
|
private startAgent;
|
|
126
132
|
}
|
|
127
133
|
//# sourceMappingURL=avatar.d.ts.map
|
package/dist/avatar.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface AvatarSessionOptions {
|
|
|
28
28
|
* The idle timeout, in seconds. Defaults to 60 seconds.
|
|
29
29
|
*/
|
|
30
30
|
idleTimeout?: number | null;
|
|
31
|
+
/**
|
|
32
|
+
* Additional payload fields to merge into the LemonSlice session creation request.
|
|
33
|
+
*/
|
|
34
|
+
extraPayload?: Record<string, unknown> | null;
|
|
31
35
|
/**
|
|
32
36
|
* The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions
|
|
33
37
|
* or LEMONSLICE_API_URL environment variable.
|
|
@@ -96,6 +100,7 @@ export declare class AvatarSession {
|
|
|
96
100
|
private agentImageUrl;
|
|
97
101
|
private agentPrompt;
|
|
98
102
|
private idleTimeout;
|
|
103
|
+
private extraPayload;
|
|
99
104
|
private apiUrl;
|
|
100
105
|
private apiKey;
|
|
101
106
|
private avatarParticipantIdentity;
|
|
@@ -120,8 +125,9 @@ export declare class AvatarSession {
|
|
|
120
125
|
* @param room - The LiveKit room where the avatar will join
|
|
121
126
|
* @param options - Optional LiveKit credentials (falls back to environment variables)
|
|
122
127
|
* @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start
|
|
128
|
+
* @returns The session ID of the LemonSlice session
|
|
123
129
|
*/
|
|
124
|
-
start(agentSession: voice.AgentSession, room: Room, options?: StartOptions): Promise<
|
|
130
|
+
start(agentSession: voice.AgentSession, room: Room, options?: StartOptions): Promise<string>;
|
|
125
131
|
private startAgent;
|
|
126
132
|
}
|
|
127
133
|
//# sourceMappingURL=avatar.d.ts.map
|
package/dist/avatar.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../src/avatar.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EAMtB,KAAK,EACN,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAY9C;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC;;OAEG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;OAEG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,yBAAyB,CAAS;IAC1C,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,WAAW,CAAoB;IAIvC;;;;;OAKG;gBACS,OAAO,GAAE,oBAAyB;
|
|
1
|
+
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../src/avatar.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EAMtB,KAAK,EACN,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAY9C;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC;;OAEG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;OAEG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,yBAAyB,CAAS;IAC1C,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,WAAW,CAAoB;IAIvC;;;;;OAKG;gBACS,OAAO,GAAE,oBAAyB;IA8B9C;;;;;;;;;;;;;OAaG;IACG,KAAK,CACT,YAAY,EAAE,KAAK,CAAC,YAAY,EAChC,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,MAAM,CAAC;YA6DJ,UAAU;CAwEzB"}
|
package/dist/avatar.js
CHANGED
|
@@ -25,6 +25,7 @@ class AvatarSession {
|
|
|
25
25
|
agentImageUrl;
|
|
26
26
|
agentPrompt;
|
|
27
27
|
idleTimeout;
|
|
28
|
+
extraPayload;
|
|
28
29
|
apiUrl;
|
|
29
30
|
apiKey;
|
|
30
31
|
avatarParticipantIdentity;
|
|
@@ -48,6 +49,7 @@ class AvatarSession {
|
|
|
48
49
|
}
|
|
49
50
|
this.agentPrompt = options.agentPrompt ?? null;
|
|
50
51
|
this.idleTimeout = options.idleTimeout ?? null;
|
|
52
|
+
this.extraPayload = options.extraPayload ?? null;
|
|
51
53
|
this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;
|
|
52
54
|
this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || "";
|
|
53
55
|
if (!this.apiKey) {
|
|
@@ -71,6 +73,7 @@ class AvatarSession {
|
|
|
71
73
|
* @param room - The LiveKit room where the avatar will join
|
|
72
74
|
* @param options - Optional LiveKit credentials (falls back to environment variables)
|
|
73
75
|
* @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start
|
|
76
|
+
* @returns The session ID of the LemonSlice session
|
|
74
77
|
*/
|
|
75
78
|
async start(agentSession, room, options = {}) {
|
|
76
79
|
var _a;
|
|
@@ -112,13 +115,14 @@ class AvatarSession {
|
|
|
112
115
|
};
|
|
113
116
|
const livekitToken = await at.toJwt();
|
|
114
117
|
this.#logger.debug("starting avatar session");
|
|
115
|
-
await this.startAgent(livekitUrl, livekitToken);
|
|
118
|
+
const sessionId = await this.startAgent(livekitUrl, livekitToken);
|
|
116
119
|
agentSession.output.audio = new voice.DataStreamAudioOutput({
|
|
117
120
|
room,
|
|
118
121
|
destinationIdentity: this.avatarParticipantIdentity,
|
|
119
122
|
sampleRate: SAMPLE_RATE,
|
|
120
123
|
waitRemoteTrack: TrackKind.KIND_VIDEO
|
|
121
124
|
});
|
|
125
|
+
return sessionId;
|
|
122
126
|
}
|
|
123
127
|
async startAgent(livekitUrl, livekitToken) {
|
|
124
128
|
for (let i = 0; i <= this.connOptions.maxRetry; i++) {
|
|
@@ -142,6 +146,9 @@ class AvatarSession {
|
|
|
142
146
|
if (this.idleTimeout !== null) {
|
|
143
147
|
payload.idle_timeout = this.idleTimeout;
|
|
144
148
|
}
|
|
149
|
+
if (this.extraPayload) {
|
|
150
|
+
Object.assign(payload, this.extraPayload);
|
|
151
|
+
}
|
|
145
152
|
const response = await fetch(this.apiUrl, {
|
|
146
153
|
method: "POST",
|
|
147
154
|
headers: {
|
|
@@ -158,7 +165,8 @@ class AvatarSession {
|
|
|
158
165
|
options: { statusCode: response.status, body: { error: text } }
|
|
159
166
|
});
|
|
160
167
|
}
|
|
161
|
-
|
|
168
|
+
const data = await response.json();
|
|
169
|
+
return data.session_id;
|
|
162
170
|
} catch (e) {
|
|
163
171
|
if (e instanceof APIStatusError && !e.retryable) {
|
|
164
172
|
throw e;
|
package/dist/avatar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/avatar.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n DEFAULT_API_CONNECT_OPTIONS,\n getJobContext,\n intervalForRetry,\n voice,\n} from '@livekit/agents';\nimport type { Room } from '@livekit/rtc-node';\nimport { TrackKind } from '@livekit/rtc-node';\nimport type { VideoGrant } from 'livekit-server-sdk';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { log } from './log.js';\n\nconst ATTRIBUTE_PUBLISH_ON_BEHALF = 'lk.publish_on_behalf';\nconst DEFAULT_API_URL = 'https://lemonslice.com/api/liveai/sessions';\nconst SAMPLE_RATE = 16000;\nconst AVATAR_AGENT_IDENTITY = 'lemonslice-avatar-agent';\nconst AVATAR_AGENT_NAME = 'lemonslice-avatar-agent';\n\n/**\n * Exception thrown when there are errors with the LemonSlice API.\n */\nexport class LemonSliceException extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LemonSliceException';\n }\n}\n\n/**\n * Options for configuring an AvatarSession.\n */\nexport interface AvatarSessionOptions {\n /**\n * The ID of the LemonSlice agent to add to the session.\n * Either agentId or agentImageUrl must be provided.\n */\n agentId?: string | null;\n /**\n * The URL of the image to use as the agent's avatar.\n * Either agentId or agentImageUrl must be provided.\n */\n agentImageUrl?: string | null;\n /**\n * A prompt that subtly influences the avatar's movements and expressions.\n */\n agentPrompt?: string | null;\n /**\n * The idle timeout, in seconds. Defaults to 60 seconds.\n */\n idleTimeout?: number | null;\n /**\n * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions\n * or LEMONSLICE_API_URL environment variable.\n */\n apiUrl?: string;\n /**\n * The LemonSlice API key. Can also be set via LEMONSLICE_API_KEY environment variable.\n */\n apiKey?: string;\n /**\n * The identity of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantIdentity?: string;\n /**\n * The name of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantName?: string;\n /**\n * Connection options for API requests.\n */\n connOptions?: APIConnectOptions;\n}\n\n/**\n * Options for starting an avatar session.\n */\nexport interface StartOptions {\n /**\n * LiveKit server URL. Falls back to LIVEKIT_URL environment variable.\n */\n livekitUrl?: string;\n /**\n * LiveKit API key. Falls back to LIVEKIT_API_KEY environment variable.\n */\n livekitApiKey?: string;\n /**\n * LiveKit API secret. Falls back to LIVEKIT_API_SECRET environment variable.\n */\n livekitApiSecret?: string;\n}\n\n/**\n * A LemonSlice avatar session.\n *\n * This class manages the connection between a LiveKit agent and a LemonSlice avatar,\n * routing agent audio output to the avatar for visual representation.\n *\n * @example\n * ```typescript\n * // Using an agent ID\n * const avatar = new AvatarSession({\n * agentId: 'your-agent-id',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n *\n * // Using a custom avatar image\n * const avatar = new AvatarSession({\n * agentImageUrl: 'your-image-url',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n * ```\n */\nexport class AvatarSession {\n private agentId: string | null;\n private agentImageUrl: string | null;\n private agentPrompt: string | null;\n private idleTimeout: number | null;\n private apiUrl: string;\n private apiKey: string;\n private avatarParticipantIdentity: string;\n private avatarParticipantName: string;\n private connOptions: APIConnectOptions;\n\n #logger = log();\n\n /**\n * Creates a new AvatarSession.\n *\n * @param options - Configuration options for the avatar session\n * @throws LemonSliceException if invalid agentId or agentImageUrl is provided, or if LemonSlice API key is not set\n */\n constructor(options: AvatarSessionOptions = {}) {\n this.agentId = options.agentId ?? null;\n this.agentImageUrl = options.agentImageUrl ?? null;\n\n if (!this.agentId && !this.agentImageUrl) {\n throw new LemonSliceException('Missing agentId or agentImageUrl');\n }\n if (this.agentId && this.agentImageUrl) {\n throw new LemonSliceException('Only one of agentId or agentImageUrl can be provided');\n }\n\n this.agentPrompt = options.agentPrompt ?? null;\n this.idleTimeout = options.idleTimeout ?? null;\n\n this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;\n this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || '';\n\n if (!this.apiKey) {\n throw new LemonSliceException(\n 'The api_key must be set either by passing apiKey to the client or ' +\n 'by setting the LEMONSLICE_API_KEY environment variable',\n );\n }\n\n this.avatarParticipantIdentity = options.avatarParticipantIdentity || AVATAR_AGENT_IDENTITY;\n this.avatarParticipantName = options.avatarParticipantName || AVATAR_AGENT_NAME;\n this.connOptions = options.connOptions || DEFAULT_API_CONNECT_OPTIONS;\n }\n\n /**\n * Starts the avatar session and connects it to the agent.\n *\n * This method:\n * 1. Creates a LiveKit token for the avatar participant\n * 2. Calls the LemonSlice API to start the avatar session\n * 3. Configures the agent's audio output to stream to the avatar\n *\n * @param agentSession - The agent session to connect to the avatar\n * @param room - The LiveKit room where the avatar will join\n * @param options - Optional LiveKit credentials (falls back to environment variables)\n * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start\n */\n async start(\n agentSession: voice.AgentSession,\n room: Room,\n options: StartOptions = {},\n ): Promise<void> {\n const livekitUrl = options.livekitUrl || process.env.LIVEKIT_URL;\n const livekitApiKey = options.livekitApiKey || process.env.LIVEKIT_API_KEY;\n const livekitApiSecret = options.livekitApiSecret || process.env.LIVEKIT_API_SECRET;\n\n if (!livekitUrl || !livekitApiKey || !livekitApiSecret) {\n throw new LemonSliceException(\n 'livekitUrl, livekitApiKey, and livekitApiSecret must be set ' +\n 'by arguments or environment variables',\n );\n }\n\n let localParticipantIdentity: string;\n try {\n const jobCtx = getJobContext();\n localParticipantIdentity = jobCtx.agent?.identity || '';\n if (!localParticipantIdentity && room.localParticipant) {\n localParticipantIdentity = room.localParticipant.identity;\n }\n } catch {\n if (!room.isConnected || !room.localParticipant) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n localParticipantIdentity = room.localParticipant.identity;\n }\n\n if (!localParticipantIdentity) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n\n const at = new AccessToken(livekitApiKey, livekitApiSecret, {\n identity: this.avatarParticipantIdentity,\n name: this.avatarParticipantName,\n });\n at.kind = 'agent';\n\n at.addGrant({\n roomJoin: true,\n room: room.name,\n } as VideoGrant);\n\n // allow the avatar agent to publish audio and video on behalf of your local agent\n at.attributes = {\n [ATTRIBUTE_PUBLISH_ON_BEHALF]: localParticipantIdentity,\n };\n\n const livekitToken = await at.toJwt();\n\n this.#logger.debug('starting avatar session');\n await this.startAgent(livekitUrl, livekitToken);\n\n agentSession.output.audio = new voice.DataStreamAudioOutput({\n room,\n destinationIdentity: this.avatarParticipantIdentity,\n sampleRate: SAMPLE_RATE,\n waitRemoteTrack: TrackKind.KIND_VIDEO,\n });\n }\n\n private async startAgent(livekitUrl: string, livekitToken: string): Promise<void> {\n for (let i = 0; i <= this.connOptions.maxRetry; i++) {\n try {\n const payload: Record<string, any> = {\n transport_type: 'livekit',\n properties: {\n livekit_url: livekitUrl,\n livekit_token: livekitToken,\n },\n };\n\n if (this.agentId) {\n payload.agent_id = this.agentId;\n }\n\n if (this.agentImageUrl) {\n payload.agent_image_url = this.agentImageUrl;\n }\n\n if (this.agentPrompt) {\n payload.agent_prompt = this.agentPrompt;\n }\n\n if (this.idleTimeout !== null) {\n payload.idle_timeout = this.idleTimeout;\n }\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.connOptions.timeoutMs),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new APIStatusError({\n message: 'Server returned an error',\n options: { statusCode: response.status, body: { error: text } },\n });\n }\n return;\n } catch (e) {\n if (e instanceof APIStatusError && !e.retryable) {\n throw e;\n }\n if (e instanceof APIConnectionError) {\n this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api');\n } else {\n this.#logger.error({ error: e }, 'failed to call lemonslice api');\n }\n\n if (i <= this.connOptions.maxRetry - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, intervalForRetry(this.connOptions, i)),\n );\n }\n }\n }\n\n throw new APIConnectionError({\n message: 'Failed to start LemonSlice Avatar Session after all retries',\n });\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/avatar.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n DEFAULT_API_CONNECT_OPTIONS,\n getJobContext,\n intervalForRetry,\n voice,\n} from '@livekit/agents';\nimport type { Room } from '@livekit/rtc-node';\nimport { TrackKind } from '@livekit/rtc-node';\nimport type { VideoGrant } from 'livekit-server-sdk';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { log } from './log.js';\n\nconst ATTRIBUTE_PUBLISH_ON_BEHALF = 'lk.publish_on_behalf';\nconst DEFAULT_API_URL = 'https://lemonslice.com/api/liveai/sessions';\nconst SAMPLE_RATE = 16000;\nconst AVATAR_AGENT_IDENTITY = 'lemonslice-avatar-agent';\nconst AVATAR_AGENT_NAME = 'lemonslice-avatar-agent';\n\n/**\n * Exception thrown when there are errors with the LemonSlice API.\n */\nexport class LemonSliceException extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LemonSliceException';\n }\n}\n\n/**\n * Options for configuring an AvatarSession.\n */\nexport interface AvatarSessionOptions {\n /**\n * The ID of the LemonSlice agent to add to the session.\n * Either agentId or agentImageUrl must be provided.\n */\n agentId?: string | null;\n /**\n * The URL of the image to use as the agent's avatar.\n * Either agentId or agentImageUrl must be provided.\n */\n agentImageUrl?: string | null;\n /**\n * A prompt that subtly influences the avatar's movements and expressions.\n */\n agentPrompt?: string | null;\n /**\n * The idle timeout, in seconds. Defaults to 60 seconds.\n */\n idleTimeout?: number | null;\n /**\n * Additional payload fields to merge into the LemonSlice session creation request.\n */\n extraPayload?: Record<string, unknown> | null;\n /**\n * The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions\n * or LEMONSLICE_API_URL environment variable.\n */\n apiUrl?: string;\n /**\n * The LemonSlice API key. Can also be set via LEMONSLICE_API_KEY environment variable.\n */\n apiKey?: string;\n /**\n * The identity of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantIdentity?: string;\n /**\n * The name of the avatar participant in the room. Defaults to 'lemonslice-avatar-agent'.\n */\n avatarParticipantName?: string;\n /**\n * Connection options for API requests.\n */\n connOptions?: APIConnectOptions;\n}\n\n/**\n * Options for starting an avatar session.\n */\nexport interface StartOptions {\n /**\n * LiveKit server URL. Falls back to LIVEKIT_URL environment variable.\n */\n livekitUrl?: string;\n /**\n * LiveKit API key. Falls back to LIVEKIT_API_KEY environment variable.\n */\n livekitApiKey?: string;\n /**\n * LiveKit API secret. Falls back to LIVEKIT_API_SECRET environment variable.\n */\n livekitApiSecret?: string;\n}\n\n/**\n * A LemonSlice avatar session.\n *\n * This class manages the connection between a LiveKit agent and a LemonSlice avatar,\n * routing agent audio output to the avatar for visual representation.\n *\n * @example\n * ```typescript\n * // Using an agent ID\n * const avatar = new AvatarSession({\n * agentId: 'your-agent-id',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n *\n * // Using a custom avatar image\n * const avatar = new AvatarSession({\n * agentImageUrl: 'your-image-url',\n * apiKey: 'your-lemonslice-api-key',\n * });\n * await avatar.start(agentSession, room);\n * ```\n */\nexport class AvatarSession {\n private agentId: string | null;\n private agentImageUrl: string | null;\n private agentPrompt: string | null;\n private idleTimeout: number | null;\n private extraPayload: Record<string, unknown> | null;\n private apiUrl: string;\n private apiKey: string;\n private avatarParticipantIdentity: string;\n private avatarParticipantName: string;\n private connOptions: APIConnectOptions;\n\n #logger = log();\n\n /**\n * Creates a new AvatarSession.\n *\n * @param options - Configuration options for the avatar session\n * @throws LemonSliceException if invalid agentId or agentImageUrl is provided, or if LemonSlice API key is not set\n */\n constructor(options: AvatarSessionOptions = {}) {\n this.agentId = options.agentId ?? null;\n this.agentImageUrl = options.agentImageUrl ?? null;\n\n if (!this.agentId && !this.agentImageUrl) {\n throw new LemonSliceException('Missing agentId or agentImageUrl');\n }\n if (this.agentId && this.agentImageUrl) {\n throw new LemonSliceException('Only one of agentId or agentImageUrl can be provided');\n }\n\n this.agentPrompt = options.agentPrompt ?? null;\n this.idleTimeout = options.idleTimeout ?? null;\n this.extraPayload = options.extraPayload ?? null;\n\n this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;\n this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || '';\n\n if (!this.apiKey) {\n throw new LemonSliceException(\n 'The api_key must be set either by passing apiKey to the client or ' +\n 'by setting the LEMONSLICE_API_KEY environment variable',\n );\n }\n\n this.avatarParticipantIdentity = options.avatarParticipantIdentity || AVATAR_AGENT_IDENTITY;\n this.avatarParticipantName = options.avatarParticipantName || AVATAR_AGENT_NAME;\n this.connOptions = options.connOptions || DEFAULT_API_CONNECT_OPTIONS;\n }\n\n /**\n * Starts the avatar session and connects it to the agent.\n *\n * This method:\n * 1. Creates a LiveKit token for the avatar participant\n * 2. Calls the LemonSlice API to start the avatar session\n * 3. Configures the agent's audio output to stream to the avatar\n *\n * @param agentSession - The agent session to connect to the avatar\n * @param room - The LiveKit room where the avatar will join\n * @param options - Optional LiveKit credentials (falls back to environment variables)\n * @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start\n * @returns The session ID of the LemonSlice session\n */\n async start(\n agentSession: voice.AgentSession,\n room: Room,\n options: StartOptions = {},\n ): Promise<string> {\n const livekitUrl = options.livekitUrl || process.env.LIVEKIT_URL;\n const livekitApiKey = options.livekitApiKey || process.env.LIVEKIT_API_KEY;\n const livekitApiSecret = options.livekitApiSecret || process.env.LIVEKIT_API_SECRET;\n\n if (!livekitUrl || !livekitApiKey || !livekitApiSecret) {\n throw new LemonSliceException(\n 'livekitUrl, livekitApiKey, and livekitApiSecret must be set ' +\n 'by arguments or environment variables',\n );\n }\n\n let localParticipantIdentity: string;\n try {\n const jobCtx = getJobContext();\n localParticipantIdentity = jobCtx.agent?.identity || '';\n if (!localParticipantIdentity && room.localParticipant) {\n localParticipantIdentity = room.localParticipant.identity;\n }\n } catch {\n if (!room.isConnected || !room.localParticipant) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n localParticipantIdentity = room.localParticipant.identity;\n }\n\n if (!localParticipantIdentity) {\n throw new LemonSliceException('failed to get local participant identity');\n }\n\n const at = new AccessToken(livekitApiKey, livekitApiSecret, {\n identity: this.avatarParticipantIdentity,\n name: this.avatarParticipantName,\n });\n at.kind = 'agent';\n\n at.addGrant({\n roomJoin: true,\n room: room.name,\n } as VideoGrant);\n\n // allow the avatar agent to publish audio and video on behalf of your local agent\n at.attributes = {\n [ATTRIBUTE_PUBLISH_ON_BEHALF]: localParticipantIdentity,\n };\n\n const livekitToken = await at.toJwt();\n\n this.#logger.debug('starting avatar session');\n const sessionId = await this.startAgent(livekitUrl, livekitToken);\n\n agentSession.output.audio = new voice.DataStreamAudioOutput({\n room,\n destinationIdentity: this.avatarParticipantIdentity,\n sampleRate: SAMPLE_RATE,\n waitRemoteTrack: TrackKind.KIND_VIDEO,\n });\n\n return sessionId;\n }\n\n private async startAgent(livekitUrl: string, livekitToken: string): Promise<string> {\n for (let i = 0; i <= this.connOptions.maxRetry; i++) {\n try {\n const payload: Record<string, any> = {\n transport_type: 'livekit',\n properties: {\n livekit_url: livekitUrl,\n livekit_token: livekitToken,\n },\n };\n\n if (this.agentId) {\n payload.agent_id = this.agentId;\n }\n\n if (this.agentImageUrl) {\n payload.agent_image_url = this.agentImageUrl;\n }\n\n if (this.agentPrompt) {\n payload.agent_prompt = this.agentPrompt;\n }\n\n if (this.idleTimeout !== null) {\n payload.idle_timeout = this.idleTimeout;\n }\n\n if (this.extraPayload) {\n Object.assign(payload, this.extraPayload);\n }\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.connOptions.timeoutMs),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new APIStatusError({\n message: 'Server returned an error',\n options: { statusCode: response.status, body: { error: text } },\n });\n }\n const data = (await response.json()) as { session_id: string };\n return data.session_id;\n } catch (e) {\n if (e instanceof APIStatusError && !e.retryable) {\n throw e;\n }\n if (e instanceof APIConnectionError) {\n this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api');\n } else {\n this.#logger.error({ error: e }, 'failed to call lemonslice api');\n }\n\n if (i <= this.connOptions.maxRetry - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, intervalForRetry(this.connOptions, i)),\n );\n }\n }\n }\n\n throw new APIConnectionError({\n message: 'Failed to start LemonSlice Avatar Session after all retries',\n });\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var import_agents = require("@livekit/agents");
|
|
3
|
+
var import_vitest = require("vitest");
|
|
4
|
+
var import_avatar = require("./avatar.cjs");
|
|
5
|
+
(0, import_vitest.describe)("LemonSlice AvatarSession", () => {
|
|
6
|
+
(0, import_vitest.beforeEach)(() => {
|
|
7
|
+
(0, import_agents.initializeLogger)({ pretty: false });
|
|
8
|
+
global.fetch = import_vitest.vi.fn();
|
|
9
|
+
});
|
|
10
|
+
(0, import_vitest.afterEach)(() => {
|
|
11
|
+
import_vitest.vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
(0, import_vitest.it)("merges extraPayload into the session creation request body", async () => {
|
|
14
|
+
const mockFetch = import_vitest.vi.mocked(fetch);
|
|
15
|
+
mockFetch.mockResolvedValue({
|
|
16
|
+
ok: true,
|
|
17
|
+
json: async () => ({ session_id: "test-session-id" })
|
|
18
|
+
});
|
|
19
|
+
const avatar = new import_avatar.AvatarSession({
|
|
20
|
+
apiKey: "test-api-key",
|
|
21
|
+
agentImageUrl: "https://example.com/avatar.png",
|
|
22
|
+
extraPayload: {
|
|
23
|
+
aspect_ratio: "9x16"
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
await avatar.startAgent("wss://livekit.example.com", "livekit-token");
|
|
27
|
+
(0, import_vitest.expect)(mockFetch).toHaveBeenCalledTimes(1);
|
|
28
|
+
(0, import_vitest.expect)(mockFetch).toHaveBeenCalledWith(
|
|
29
|
+
"https://lemonslice.com/api/liveai/sessions",
|
|
30
|
+
import_vitest.expect.objectContaining({
|
|
31
|
+
method: "POST",
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
transport_type: "livekit",
|
|
34
|
+
properties: {
|
|
35
|
+
livekit_url: "wss://livekit.example.com",
|
|
36
|
+
livekit_token: "livekit-token"
|
|
37
|
+
},
|
|
38
|
+
agent_image_url: "https://example.com/avatar.png",
|
|
39
|
+
aspect_ratio: "9x16"
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
(0, import_vitest.it)("keeps the request body unchanged when extraPayload is omitted", async () => {
|
|
45
|
+
var _a;
|
|
46
|
+
const mockFetch = import_vitest.vi.mocked(fetch);
|
|
47
|
+
mockFetch.mockResolvedValue({
|
|
48
|
+
ok: true,
|
|
49
|
+
json: async () => ({ session_id: "test-session-id" })
|
|
50
|
+
});
|
|
51
|
+
const avatar = new import_avatar.AvatarSession({
|
|
52
|
+
apiKey: "test-api-key",
|
|
53
|
+
agentImageUrl: "https://example.com/avatar.png"
|
|
54
|
+
});
|
|
55
|
+
await avatar.startAgent("wss://livekit.example.com", "livekit-token");
|
|
56
|
+
const requestInit = (_a = mockFetch.mock.calls[0]) == null ? void 0 : _a[1];
|
|
57
|
+
const body = JSON.parse(String(requestInit == null ? void 0 : requestInit.body));
|
|
58
|
+
(0, import_vitest.expect)(body).toEqual({
|
|
59
|
+
transport_type: "livekit",
|
|
60
|
+
properties: {
|
|
61
|
+
livekit_url: "wss://livekit.example.com",
|
|
62
|
+
livekit_token: "livekit-token"
|
|
63
|
+
},
|
|
64
|
+
agent_image_url: "https://example.com/avatar.png"
|
|
65
|
+
});
|
|
66
|
+
(0, import_vitest.expect)(body).not.toHaveProperty("aspect_ratio");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=avatar.test.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/avatar.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { initializeLogger } from '@livekit/agents';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { AvatarSession } from './avatar.js';\n\ndescribe('LemonSlice AvatarSession', () => {\n beforeEach(() => {\n initializeLogger({ pretty: false });\n global.fetch = vi.fn();\n });\n\n afterEach(() => {\n vi.restoreAllMocks();\n });\n\n it('merges extraPayload into the session creation request body', async () => {\n const mockFetch = vi.mocked(fetch);\n mockFetch.mockResolvedValue({\n ok: true,\n json: async () => ({ session_id: 'test-session-id' }),\n } as Response);\n\n const avatar = new AvatarSession({\n apiKey: 'test-api-key',\n agentImageUrl: 'https://example.com/avatar.png',\n extraPayload: {\n aspect_ratio: '9x16',\n },\n });\n\n await (\n avatar as unknown as {\n startAgent(livekitUrl: string, livekitToken: string): Promise<void>;\n }\n ).startAgent('wss://livekit.example.com', 'livekit-token');\n\n expect(mockFetch).toHaveBeenCalledTimes(1);\n expect(mockFetch).toHaveBeenCalledWith(\n 'https://lemonslice.com/api/liveai/sessions',\n expect.objectContaining({\n method: 'POST',\n body: JSON.stringify({\n transport_type: 'livekit',\n properties: {\n livekit_url: 'wss://livekit.example.com',\n livekit_token: 'livekit-token',\n },\n agent_image_url: 'https://example.com/avatar.png',\n aspect_ratio: '9x16',\n }),\n }),\n );\n });\n\n it('keeps the request body unchanged when extraPayload is omitted', async () => {\n const mockFetch = vi.mocked(fetch);\n mockFetch.mockResolvedValue({\n ok: true,\n json: async () => ({ session_id: 'test-session-id' }),\n } as Response);\n\n const avatar = new AvatarSession({\n apiKey: 'test-api-key',\n agentImageUrl: 'https://example.com/avatar.png',\n });\n\n await (\n avatar as unknown as {\n startAgent(livekitUrl: string, livekitToken: string): Promise<void>;\n }\n ).startAgent('wss://livekit.example.com', 'livekit-token');\n\n const requestInit = mockFetch.mock.calls[0]?.[1];\n const body = JSON.parse(String(requestInit?.body));\n\n expect(body).toEqual({\n transport_type: 'livekit',\n properties: {\n livekit_url: 'wss://livekit.example.com',\n livekit_token: 'livekit-token',\n },\n agent_image_url: 'https://example.com/avatar.png',\n });\n expect(body).not.toHaveProperty('aspect_ratio');\n });\n});\n"],"mappings":";AAGA,oBAAiC;AACjC,oBAAgE;AAChE,oBAA8B;AAAA,IAE9B,wBAAS,4BAA4B,MAAM;AACzC,gCAAW,MAAM;AACf,wCAAiB,EAAE,QAAQ,MAAM,CAAC;AAClC,WAAO,QAAQ,iBAAG,GAAG;AAAA,EACvB,CAAC;AAED,+BAAU,MAAM;AACd,qBAAG,gBAAgB;AAAA,EACrB,CAAC;AAED,wBAAG,8DAA8D,YAAY;AAC3E,UAAM,YAAY,iBAAG,OAAO,KAAK;AACjC,cAAU,kBAAkB;AAAA,MAC1B,IAAI;AAAA,MACJ,MAAM,aAAa,EAAE,YAAY,kBAAkB;AAAA,IACrD,CAAa;AAEb,UAAM,SAAS,IAAI,4BAAc;AAAA,MAC/B,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,cAAc;AAAA,QACZ,cAAc;AAAA,MAChB;AAAA,IACF,CAAC;AAED,UACE,OAGA,WAAW,6BAA6B,eAAe;AAEzD,8BAAO,SAAS,EAAE,sBAAsB,CAAC;AACzC,8BAAO,SAAS,EAAE;AAAA,MAChB;AAAA,MACA,qBAAO,iBAAiB;AAAA,QACtB,QAAQ;AAAA,QACR,MAAM,KAAK,UAAU;AAAA,UACnB,gBAAgB;AAAA,UAChB,YAAY;AAAA,YACV,aAAa;AAAA,YACb,eAAe;AAAA,UACjB;AAAA,UACA,iBAAiB;AAAA,UACjB,cAAc;AAAA,QAChB,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAED,wBAAG,iEAAiE,YAAY;AAxDlF;AAyDI,UAAM,YAAY,iBAAG,OAAO,KAAK;AACjC,cAAU,kBAAkB;AAAA,MAC1B,IAAI;AAAA,MACJ,MAAM,aAAa,EAAE,YAAY,kBAAkB;AAAA,IACrD,CAAa;AAEb,UAAM,SAAS,IAAI,4BAAc;AAAA,MAC/B,QAAQ;AAAA,MACR,eAAe;AAAA,IACjB,CAAC;AAED,UACE,OAGA,WAAW,6BAA6B,eAAe;AAEzD,UAAM,eAAc,eAAU,KAAK,MAAM,CAAC,MAAtB,mBAA0B;AAC9C,UAAM,OAAO,KAAK,MAAM,OAAO,2CAAa,IAAI,CAAC;AAEjD,8BAAO,IAAI,EAAE,QAAQ;AAAA,MACnB,gBAAgB;AAAA,MAChB,YAAY;AAAA,QACV,aAAa;AAAA,QACb,eAAe;AAAA,MACjB;AAAA,MACA,iBAAiB;AAAA,IACnB,CAAC;AACD,8BAAO,IAAI,EAAE,IAAI,eAAe,cAAc;AAAA,EAChD,CAAC;AACH,CAAC;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"avatar.test.d.ts","sourceRoot":"","sources":["../src/avatar.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { initializeLogger } from "@livekit/agents";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { AvatarSession } from "./avatar.js";
|
|
4
|
+
describe("LemonSlice AvatarSession", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
initializeLogger({ pretty: false });
|
|
7
|
+
global.fetch = vi.fn();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
it("merges extraPayload into the session creation request body", async () => {
|
|
13
|
+
const mockFetch = vi.mocked(fetch);
|
|
14
|
+
mockFetch.mockResolvedValue({
|
|
15
|
+
ok: true,
|
|
16
|
+
json: async () => ({ session_id: "test-session-id" })
|
|
17
|
+
});
|
|
18
|
+
const avatar = new AvatarSession({
|
|
19
|
+
apiKey: "test-api-key",
|
|
20
|
+
agentImageUrl: "https://example.com/avatar.png",
|
|
21
|
+
extraPayload: {
|
|
22
|
+
aspect_ratio: "9x16"
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
await avatar.startAgent("wss://livekit.example.com", "livekit-token");
|
|
26
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
27
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
28
|
+
"https://lemonslice.com/api/liveai/sessions",
|
|
29
|
+
expect.objectContaining({
|
|
30
|
+
method: "POST",
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
transport_type: "livekit",
|
|
33
|
+
properties: {
|
|
34
|
+
livekit_url: "wss://livekit.example.com",
|
|
35
|
+
livekit_token: "livekit-token"
|
|
36
|
+
},
|
|
37
|
+
agent_image_url: "https://example.com/avatar.png",
|
|
38
|
+
aspect_ratio: "9x16"
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
it("keeps the request body unchanged when extraPayload is omitted", async () => {
|
|
44
|
+
var _a;
|
|
45
|
+
const mockFetch = vi.mocked(fetch);
|
|
46
|
+
mockFetch.mockResolvedValue({
|
|
47
|
+
ok: true,
|
|
48
|
+
json: async () => ({ session_id: "test-session-id" })
|
|
49
|
+
});
|
|
50
|
+
const avatar = new AvatarSession({
|
|
51
|
+
apiKey: "test-api-key",
|
|
52
|
+
agentImageUrl: "https://example.com/avatar.png"
|
|
53
|
+
});
|
|
54
|
+
await avatar.startAgent("wss://livekit.example.com", "livekit-token");
|
|
55
|
+
const requestInit = (_a = mockFetch.mock.calls[0]) == null ? void 0 : _a[1];
|
|
56
|
+
const body = JSON.parse(String(requestInit == null ? void 0 : requestInit.body));
|
|
57
|
+
expect(body).toEqual({
|
|
58
|
+
transport_type: "livekit",
|
|
59
|
+
properties: {
|
|
60
|
+
livekit_url: "wss://livekit.example.com",
|
|
61
|
+
livekit_token: "livekit-token"
|
|
62
|
+
},
|
|
63
|
+
agent_image_url: "https://example.com/avatar.png"
|
|
64
|
+
});
|
|
65
|
+
expect(body).not.toHaveProperty("aspect_ratio");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=avatar.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/avatar.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { initializeLogger } from '@livekit/agents';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { AvatarSession } from './avatar.js';\n\ndescribe('LemonSlice AvatarSession', () => {\n beforeEach(() => {\n initializeLogger({ pretty: false });\n global.fetch = vi.fn();\n });\n\n afterEach(() => {\n vi.restoreAllMocks();\n });\n\n it('merges extraPayload into the session creation request body', async () => {\n const mockFetch = vi.mocked(fetch);\n mockFetch.mockResolvedValue({\n ok: true,\n json: async () => ({ session_id: 'test-session-id' }),\n } as Response);\n\n const avatar = new AvatarSession({\n apiKey: 'test-api-key',\n agentImageUrl: 'https://example.com/avatar.png',\n extraPayload: {\n aspect_ratio: '9x16',\n },\n });\n\n await (\n avatar as unknown as {\n startAgent(livekitUrl: string, livekitToken: string): Promise<void>;\n }\n ).startAgent('wss://livekit.example.com', 'livekit-token');\n\n expect(mockFetch).toHaveBeenCalledTimes(1);\n expect(mockFetch).toHaveBeenCalledWith(\n 'https://lemonslice.com/api/liveai/sessions',\n expect.objectContaining({\n method: 'POST',\n body: JSON.stringify({\n transport_type: 'livekit',\n properties: {\n livekit_url: 'wss://livekit.example.com',\n livekit_token: 'livekit-token',\n },\n agent_image_url: 'https://example.com/avatar.png',\n aspect_ratio: '9x16',\n }),\n }),\n );\n });\n\n it('keeps the request body unchanged when extraPayload is omitted', async () => {\n const mockFetch = vi.mocked(fetch);\n mockFetch.mockResolvedValue({\n ok: true,\n json: async () => ({ session_id: 'test-session-id' }),\n } as Response);\n\n const avatar = new AvatarSession({\n apiKey: 'test-api-key',\n agentImageUrl: 'https://example.com/avatar.png',\n });\n\n await (\n avatar as unknown as {\n startAgent(livekitUrl: string, livekitToken: string): Promise<void>;\n }\n ).startAgent('wss://livekit.example.com', 'livekit-token');\n\n const requestInit = mockFetch.mock.calls[0]?.[1];\n const body = JSON.parse(String(requestInit?.body));\n\n expect(body).toEqual({\n transport_type: 'livekit',\n properties: {\n livekit_url: 'wss://livekit.example.com',\n livekit_token: 'livekit-token',\n },\n agent_image_url: 'https://example.com/avatar.png',\n });\n expect(body).not.toHaveProperty('aspect_ratio');\n });\n});\n"],"mappings":"AAGA,SAAS,wBAAwB;AACjC,SAAS,WAAW,YAAY,UAAU,QAAQ,IAAI,UAAU;AAChE,SAAS,qBAAqB;AAE9B,SAAS,4BAA4B,MAAM;AACzC,aAAW,MAAM;AACf,qBAAiB,EAAE,QAAQ,MAAM,CAAC;AAClC,WAAO,QAAQ,GAAG,GAAG;AAAA,EACvB,CAAC;AAED,YAAU,MAAM;AACd,OAAG,gBAAgB;AAAA,EACrB,CAAC;AAED,KAAG,8DAA8D,YAAY;AAC3E,UAAM,YAAY,GAAG,OAAO,KAAK;AACjC,cAAU,kBAAkB;AAAA,MAC1B,IAAI;AAAA,MACJ,MAAM,aAAa,EAAE,YAAY,kBAAkB;AAAA,IACrD,CAAa;AAEb,UAAM,SAAS,IAAI,cAAc;AAAA,MAC/B,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,cAAc;AAAA,QACZ,cAAc;AAAA,MAChB;AAAA,IACF,CAAC;AAED,UACE,OAGA,WAAW,6BAA6B,eAAe;AAEzD,WAAO,SAAS,EAAE,sBAAsB,CAAC;AACzC,WAAO,SAAS,EAAE;AAAA,MAChB;AAAA,MACA,OAAO,iBAAiB;AAAA,QACtB,QAAQ;AAAA,QACR,MAAM,KAAK,UAAU;AAAA,UACnB,gBAAgB;AAAA,UAChB,YAAY;AAAA,YACV,aAAa;AAAA,YACb,eAAe;AAAA,UACjB;AAAA,UACA,iBAAiB;AAAA,UACjB,cAAc;AAAA,QAChB,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAED,KAAG,iEAAiE,YAAY;AAxDlF;AAyDI,UAAM,YAAY,GAAG,OAAO,KAAK;AACjC,cAAU,kBAAkB;AAAA,MAC1B,IAAI;AAAA,MACJ,MAAM,aAAa,EAAE,YAAY,kBAAkB;AAAA,IACrD,CAAa;AAEb,UAAM,SAAS,IAAI,cAAc;AAAA,MAC/B,QAAQ;AAAA,MACR,eAAe;AAAA,IACjB,CAAC;AAED,UACE,OAGA,WAAW,6BAA6B,eAAe;AAEzD,UAAM,eAAc,eAAU,KAAK,MAAM,CAAC,MAAtB,mBAA0B;AAC9C,UAAM,OAAO,KAAK,MAAM,OAAO,2CAAa,IAAI,CAAC;AAEjD,WAAO,IAAI,EAAE,QAAQ;AAAA,MACnB,gBAAgB;AAAA,MAChB,YAAY;AAAA,QACV,aAAa;AAAA,QACb,eAAe;AAAA,MACjB;AAAA,MACA,iBAAiB;AAAA,IACnB,CAAC;AACD,WAAO,IAAI,EAAE,IAAI,eAAe,cAAc;AAAA,EAChD,CAAC;AACH,CAAC;","names":[]}
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livekit/agents-plugin-lemonslice",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.51",
|
|
4
4
|
"description": "LemonSlice avatar plugin for LiveKit Node Agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"require": "dist/index.cjs",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"pino": "^8.19.0",
|
|
31
31
|
"tsup": "^8.3.5",
|
|
32
32
|
"typescript": "^5.0.0",
|
|
33
|
-
"@livekit/agents": "1.0.
|
|
33
|
+
"@livekit/agents": "1.0.51"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"livekit-server-sdk": "^2.13.3"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@livekit/rtc-node": "^0.13.24",
|
|
40
|
-
"@livekit/agents": "1.0.
|
|
40
|
+
"@livekit/agents": "1.0.51"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsup --onSuccess \"pnpm build:types\"",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { initializeLogger } from '@livekit/agents';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { AvatarSession } from './avatar.js';
|
|
7
|
+
|
|
8
|
+
describe('LemonSlice AvatarSession', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
initializeLogger({ pretty: false });
|
|
11
|
+
global.fetch = vi.fn();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('merges extraPayload into the session creation request body', async () => {
|
|
19
|
+
const mockFetch = vi.mocked(fetch);
|
|
20
|
+
mockFetch.mockResolvedValue({
|
|
21
|
+
ok: true,
|
|
22
|
+
json: async () => ({ session_id: 'test-session-id' }),
|
|
23
|
+
} as Response);
|
|
24
|
+
|
|
25
|
+
const avatar = new AvatarSession({
|
|
26
|
+
apiKey: 'test-api-key',
|
|
27
|
+
agentImageUrl: 'https://example.com/avatar.png',
|
|
28
|
+
extraPayload: {
|
|
29
|
+
aspect_ratio: '9x16',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await (
|
|
34
|
+
avatar as unknown as {
|
|
35
|
+
startAgent(livekitUrl: string, livekitToken: string): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
).startAgent('wss://livekit.example.com', 'livekit-token');
|
|
38
|
+
|
|
39
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
40
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
41
|
+
'https://lemonslice.com/api/liveai/sessions',
|
|
42
|
+
expect.objectContaining({
|
|
43
|
+
method: 'POST',
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
transport_type: 'livekit',
|
|
46
|
+
properties: {
|
|
47
|
+
livekit_url: 'wss://livekit.example.com',
|
|
48
|
+
livekit_token: 'livekit-token',
|
|
49
|
+
},
|
|
50
|
+
agent_image_url: 'https://example.com/avatar.png',
|
|
51
|
+
aspect_ratio: '9x16',
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('keeps the request body unchanged when extraPayload is omitted', async () => {
|
|
58
|
+
const mockFetch = vi.mocked(fetch);
|
|
59
|
+
mockFetch.mockResolvedValue({
|
|
60
|
+
ok: true,
|
|
61
|
+
json: async () => ({ session_id: 'test-session-id' }),
|
|
62
|
+
} as Response);
|
|
63
|
+
|
|
64
|
+
const avatar = new AvatarSession({
|
|
65
|
+
apiKey: 'test-api-key',
|
|
66
|
+
agentImageUrl: 'https://example.com/avatar.png',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await (
|
|
70
|
+
avatar as unknown as {
|
|
71
|
+
startAgent(livekitUrl: string, livekitToken: string): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
).startAgent('wss://livekit.example.com', 'livekit-token');
|
|
74
|
+
|
|
75
|
+
const requestInit = mockFetch.mock.calls[0]?.[1];
|
|
76
|
+
const body = JSON.parse(String(requestInit?.body));
|
|
77
|
+
|
|
78
|
+
expect(body).toEqual({
|
|
79
|
+
transport_type: 'livekit',
|
|
80
|
+
properties: {
|
|
81
|
+
livekit_url: 'wss://livekit.example.com',
|
|
82
|
+
livekit_token: 'livekit-token',
|
|
83
|
+
},
|
|
84
|
+
agent_image_url: 'https://example.com/avatar.png',
|
|
85
|
+
});
|
|
86
|
+
expect(body).not.toHaveProperty('aspect_ratio');
|
|
87
|
+
});
|
|
88
|
+
});
|
package/src/avatar.ts
CHANGED
|
@@ -54,6 +54,10 @@ export interface AvatarSessionOptions {
|
|
|
54
54
|
* The idle timeout, in seconds. Defaults to 60 seconds.
|
|
55
55
|
*/
|
|
56
56
|
idleTimeout?: number | null;
|
|
57
|
+
/**
|
|
58
|
+
* Additional payload fields to merge into the LemonSlice session creation request.
|
|
59
|
+
*/
|
|
60
|
+
extraPayload?: Record<string, unknown> | null;
|
|
57
61
|
/**
|
|
58
62
|
* The LemonSlice API URL. Defaults to https://lemonslice.com/api/liveai/sessions
|
|
59
63
|
* or LEMONSLICE_API_URL environment variable.
|
|
@@ -123,6 +127,7 @@ export class AvatarSession {
|
|
|
123
127
|
private agentImageUrl: string | null;
|
|
124
128
|
private agentPrompt: string | null;
|
|
125
129
|
private idleTimeout: number | null;
|
|
130
|
+
private extraPayload: Record<string, unknown> | null;
|
|
126
131
|
private apiUrl: string;
|
|
127
132
|
private apiKey: string;
|
|
128
133
|
private avatarParticipantIdentity: string;
|
|
@@ -150,6 +155,7 @@ export class AvatarSession {
|
|
|
150
155
|
|
|
151
156
|
this.agentPrompt = options.agentPrompt ?? null;
|
|
152
157
|
this.idleTimeout = options.idleTimeout ?? null;
|
|
158
|
+
this.extraPayload = options.extraPayload ?? null;
|
|
153
159
|
|
|
154
160
|
this.apiUrl = options.apiUrl || process.env.LEMONSLICE_API_URL || DEFAULT_API_URL;
|
|
155
161
|
this.apiKey = options.apiKey || process.env.LEMONSLICE_API_KEY || '';
|
|
@@ -178,12 +184,13 @@ export class AvatarSession {
|
|
|
178
184
|
* @param room - The LiveKit room where the avatar will join
|
|
179
185
|
* @param options - Optional LiveKit credentials (falls back to environment variables)
|
|
180
186
|
* @throws LemonSliceException if LiveKit credentials are not available or if the avatar session fails to start
|
|
187
|
+
* @returns The session ID of the LemonSlice session
|
|
181
188
|
*/
|
|
182
189
|
async start(
|
|
183
190
|
agentSession: voice.AgentSession,
|
|
184
191
|
room: Room,
|
|
185
192
|
options: StartOptions = {},
|
|
186
|
-
): Promise<
|
|
193
|
+
): Promise<string> {
|
|
187
194
|
const livekitUrl = options.livekitUrl || process.env.LIVEKIT_URL;
|
|
188
195
|
const livekitApiKey = options.livekitApiKey || process.env.LIVEKIT_API_KEY;
|
|
189
196
|
const livekitApiSecret = options.livekitApiSecret || process.env.LIVEKIT_API_SECRET;
|
|
@@ -232,7 +239,7 @@ export class AvatarSession {
|
|
|
232
239
|
const livekitToken = await at.toJwt();
|
|
233
240
|
|
|
234
241
|
this.#logger.debug('starting avatar session');
|
|
235
|
-
await this.startAgent(livekitUrl, livekitToken);
|
|
242
|
+
const sessionId = await this.startAgent(livekitUrl, livekitToken);
|
|
236
243
|
|
|
237
244
|
agentSession.output.audio = new voice.DataStreamAudioOutput({
|
|
238
245
|
room,
|
|
@@ -240,9 +247,11 @@ export class AvatarSession {
|
|
|
240
247
|
sampleRate: SAMPLE_RATE,
|
|
241
248
|
waitRemoteTrack: TrackKind.KIND_VIDEO,
|
|
242
249
|
});
|
|
250
|
+
|
|
251
|
+
return sessionId;
|
|
243
252
|
}
|
|
244
253
|
|
|
245
|
-
private async startAgent(livekitUrl: string, livekitToken: string): Promise<
|
|
254
|
+
private async startAgent(livekitUrl: string, livekitToken: string): Promise<string> {
|
|
246
255
|
for (let i = 0; i <= this.connOptions.maxRetry; i++) {
|
|
247
256
|
try {
|
|
248
257
|
const payload: Record<string, any> = {
|
|
@@ -269,6 +278,10 @@ export class AvatarSession {
|
|
|
269
278
|
payload.idle_timeout = this.idleTimeout;
|
|
270
279
|
}
|
|
271
280
|
|
|
281
|
+
if (this.extraPayload) {
|
|
282
|
+
Object.assign(payload, this.extraPayload);
|
|
283
|
+
}
|
|
284
|
+
|
|
272
285
|
const response = await fetch(this.apiUrl, {
|
|
273
286
|
method: 'POST',
|
|
274
287
|
headers: {
|
|
@@ -286,7 +299,8 @@ export class AvatarSession {
|
|
|
286
299
|
options: { statusCode: response.status, body: { error: text } },
|
|
287
300
|
});
|
|
288
301
|
}
|
|
289
|
-
|
|
302
|
+
const data = (await response.json()) as { session_id: string };
|
|
303
|
+
return data.session_id;
|
|
290
304
|
} catch (e) {
|
|
291
305
|
if (e instanceof APIStatusError && !e.retryable) {
|
|
292
306
|
throw e;
|