@livekit/agents-plugin-anam 1.0.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/src/avatar.ts ADDED
@@ -0,0 +1,119 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { log, voice } from '@livekit/agents';
5
+ import type { Room } from '@livekit/rtc-node';
6
+ import { TrackKind } from '@livekit/rtc-node';
7
+ import { AccessToken, type VideoGrant } from 'livekit-server-sdk';
8
+ import { AnamAPI } from './api.js';
9
+ import { type APIConnectOptions, AnamException, type PersonaConfig } from './types.js';
10
+
11
+ export async function mintAvatarJoinToken({
12
+ roomName,
13
+ avatarIdentity,
14
+ publishOnBehalf,
15
+ apiKey = process.env.LIVEKIT_API_KEY!,
16
+ apiSecret = process.env.LIVEKIT_API_SECRET!,
17
+ ttl = '60s',
18
+ }: {
19
+ roomName: string;
20
+ avatarIdentity: string;
21
+ publishOnBehalf: string;
22
+ apiKey?: string;
23
+ apiSecret?: string;
24
+ ttl?: string | number;
25
+ }): Promise<string> {
26
+ const at = new AccessToken(apiKey, apiSecret);
27
+ at.identity = avatarIdentity;
28
+ at.name = 'Anam Avatar';
29
+ at.kind = 'agent';
30
+ at.ttl = ttl;
31
+ at.attributes = { 'lk.publish_on_behalf': publishOnBehalf };
32
+
33
+ at.addGrant({ roomJoin: true, room: roomName } as VideoGrant);
34
+ return at.toJwt();
35
+ }
36
+
37
+ const AVATAR_IDENTITY = 'anam-avatar-agent';
38
+ const _AVATAR_NAME = 'anam-avatar-agent';
39
+
40
+ export class AvatarSession {
41
+ private sessionId?: string;
42
+
43
+ constructor(
44
+ private opts: {
45
+ personaConfig: PersonaConfig;
46
+ apiUrl?: string;
47
+ apiKey?: string;
48
+ avatarParticipantIdentity?: string;
49
+ avatarParticipantName?: string;
50
+ connOptions?: APIConnectOptions;
51
+ },
52
+ ) {}
53
+
54
+ async start(
55
+ agentSession: voice.AgentSession,
56
+ room: Room,
57
+ params?: {
58
+ livekitUrl?: string;
59
+ livekitApiKey?: string;
60
+ livekitApiSecret?: string;
61
+ },
62
+ ) {
63
+ const logger = log().child({ module: 'AnamAvatar' });
64
+ const apiKey = this.opts.apiKey ?? process.env.ANAM_API_KEY;
65
+ if (!apiKey) throw new AnamException('ANAM_API_KEY is required');
66
+
67
+ const apiUrl = this.opts.apiUrl ?? process.env.ANAM_API_URL;
68
+ const livekitUrl = params?.livekitUrl ?? process.env.LIVEKIT_URL;
69
+ const lkKey = params?.livekitApiKey ?? process.env.LIVEKIT_API_KEY;
70
+ const lkSecret = params?.livekitApiSecret ?? process.env.LIVEKIT_API_SECRET;
71
+
72
+ if (!livekitUrl || !lkKey || !lkSecret) {
73
+ throw new AnamException('LIVEKIT_URL/API_KEY/API_SECRET must be set');
74
+ }
75
+
76
+ const localIdentity = (room.localParticipant && room.localParticipant.identity) || 'agent';
77
+
78
+ logger.debug(
79
+ {
80
+ personaName: this.opts.personaConfig?.name,
81
+ avatarId: this.opts.personaConfig?.avatarId,
82
+ apiUrl: apiUrl ?? '(default https://api.anam.ai)',
83
+ livekitUrl,
84
+ avatarParticipantIdentity: this.opts.avatarParticipantIdentity ?? 'anam-avatar-agent',
85
+ publishOnBehalf: localIdentity,
86
+ },
87
+ 'starting Anam avatar session',
88
+ );
89
+
90
+ const jwt = await mintAvatarJoinToken({
91
+ roomName: room.name!,
92
+ avatarIdentity: this.opts.avatarParticipantIdentity ?? AVATAR_IDENTITY,
93
+ publishOnBehalf: localIdentity,
94
+ apiKey: lkKey,
95
+ apiSecret: lkSecret,
96
+ });
97
+
98
+ const anam = new AnamAPI(apiKey, apiUrl, this.opts.connOptions);
99
+ logger.debug({ livekitUrl }, 'requesting Anam session token');
100
+
101
+ const { sessionToken } = await anam.createSessionToken({
102
+ personaConfig: {
103
+ name: this.opts.personaConfig?.name,
104
+ avatarId: this.opts.personaConfig?.avatarId,
105
+ },
106
+ livekitUrl,
107
+ livekitToken: jwt,
108
+ });
109
+ logger.debug('starting Anam engine session');
110
+ const started = await anam.startEngineSession({ sessionToken });
111
+ this.sessionId = started.sessionId;
112
+
113
+ agentSession.output.audio = new voice.DataStreamAudioOutput({
114
+ room,
115
+ destinationIdentity: this.opts.avatarParticipantIdentity ?? AVATAR_IDENTITY,
116
+ waitRemoteTrack: TrackKind.KIND_VIDEO,
117
+ });
118
+ }
119
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { Plugin } from '@livekit/agents';
5
+
6
+ export * from './types.js';
7
+ export * from './api.js';
8
+ export * from './avatar.js';
9
+
10
+ class AnamPlugin extends Plugin {
11
+ constructor() {
12
+ super({
13
+ title: 'anam',
14
+ version: '0.0.1',
15
+ package: '@livekit/agents-plugin-anam',
16
+ });
17
+ }
18
+ }
19
+ Plugin.registerPlugin(new AnamPlugin());
package/src/types.ts ADDED
@@ -0,0 +1,19 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ export type PersonaConfig = {
5
+ /** Optional display name (prod flow) */
6
+ name?: string;
7
+ /** Optional avatar asset id (prod flow) */
8
+ avatarId?: string;
9
+ /** Optional persona id (dev flow) */
10
+ personaId?: string;
11
+ };
12
+
13
+ export type APIConnectOptions = {
14
+ maxRetry?: number;
15
+ retryInterval?: number; // seconds
16
+ timeout?: number; // seconds
17
+ };
18
+
19
+ export class AnamException extends Error {}