Skip to content

Commit

Permalink
Support for room configuration inside tokens (#324)
Browse files Browse the repository at this point in the history
* Support for room configuration inside tokens

* changeset

* add dispatch APIs via AgentDispatch client

* some updates

* docs

* some docs changes
  • Loading branch information
davidzhao authored Nov 8, 2024
1 parent a79a333 commit e389115
Show file tree
Hide file tree
Showing 18 changed files with 343 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-shrimps-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'livekit-server-sdk': minor
---

Explicit agent dispatch via API and token
54 changes: 54 additions & 0 deletions examples/agent-dispatch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { RoomAgentDispatch, RoomConfiguration } from '@livekit/protocol';
import { AccessToken, AgentDispatchClient } from 'livekit-server-sdk';

const roomName = 'my-room';
const agentName = 'test-agent';

/**
* This example demonstrates how to have an agent join a room without using
* the automatic dispatch.
* In order to use this feature, you must have an agent running with `agentName` set
* when defining your WorkerOptions.
*
* A dispatch requests the agent to enter a specific room with optional metadata.
*/
async function createExplicitDispatch() {
const agentDispatchClient = new AgentDispatchClient(process.env.LIVEKIT_URL);

// this will create invoke an agent with agentName: test-agent to join `my-room`
const dispatch = await agentDispatchClient.createDispatch(roomName, agentName, {
metadata: '{"mydata": "myvalue"}',
});
console.log('created dispatch', dispatch);

const dispatches = await agentDispatchClient.listDispatches(roomName);
console.log(`there are ${dispatches.length} dispatches in ${roomName}`);
}

/**
* When agent name is set, the agent will no longer be automatically dispatched
* to new rooms. If you want that agent to be dispatched to a new room as soon as
* the participant connects, you can set the roomConfig with the agent
* definition in the access token.
*/
async function createTokenWithAgentDispatch(): Promise<string> {
const at = new AccessToken();
at.identity = 'my-participant';
at.addGrant({ roomJoin: true, room: roomName });
at.roomConfig = new RoomConfiguration({
agents: [
new RoomAgentDispatch({
agentName: agentName,
metadata: '{"mydata": "myvalue"}',
}),
],
});
return await at.toJwt();
}

createTokenWithAgentDispatch().then((token) => {
console.log('created participant token', token);
});

console.log('creating explicit dispatch');
createExplicitDispatch();
23 changes: 23 additions & 0 deletions examples/agent-dispatch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "agent-dispatch",
"version": "0.0.1",
"description": "An example demonstrating dispatching agents with an API call",
"private": "true",
"author": "LiveKit",
"license": "Apache-2.0",
"type": "module",
"main": "index.ts",
"scripts": {
"build": "tsc --incremental",
"lint": "eslint -f unix \"**/*.ts\""
},
"dependencies": {
"dotenv": "^16.4.5",
"livekit-server-sdk": "workspace:*",
"@livekit/protocol": "^1.27.1"
},
"devDependencies": {
"@types/node": "^20.10.4",
"tsx": "^4.7.1"
}
}
11 changes: 11 additions & 0 deletions examples/agent-dispatch/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "ES2022",
"esModuleInterop": true,
"target": "es2022",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist"
},
"lib": ["es2022"]
}
1 change: 1 addition & 0 deletions examples/receive-audio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"type": "module",
"main": "index.ts",
"scripts": {
"build": "tsc --incremental",
"lint": "eslint -f unix \"**/*.ts\""
},
"keywords": [],
Expand Down
11 changes: 11 additions & 0 deletions examples/receive-audio/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "ES2022",
"esModuleInterop": true,
"target": "es2022",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist"
},
"lib": ["es2022"]
}
2 changes: 1 addition & 1 deletion packages/livekit-server-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"test:edge": "vitest --environment edge-runtime run"
},
"dependencies": {
"@livekit/protocol": "^1.27.0",
"@livekit/protocol": "^1.27.1",
"camelcase-keys": "^9.0.0",
"jose": "^5.1.2"
},
Expand Down
52 changes: 52 additions & 0 deletions packages/livekit-server-sdk/src/AccessToken.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import {
RoomAgentDispatch,
RoomCompositeEgressRequest,
RoomConfiguration,
RoomEgress,
} from '@livekit/protocol';
import * as jose from 'jose';
import { describe, expect, it } from 'vitest';
import { AccessToken, TokenVerifier } from './AccessToken';
Expand Down Expand Up @@ -98,3 +104,49 @@ describe('adding grants should not overwrite existing grants', () => {
expect(payload.video?.roomJoin).toBeTruthy();
});
});

describe('room configuration with agents and egress', () => {
it('should set agents and egress in room configuration', async () => {
const t = new AccessToken(testApiKey, testSecret, {
identity: 'test-identity',
});

const roomConfig = new RoomConfiguration({
name: 'test-room',
maxParticipants: 10,
});

const agents: RoomAgentDispatch[] = [
new RoomAgentDispatch({
agentName: 'agent1',
metadata: 'metadata-1',
}),
new RoomAgentDispatch({
agentName: 'agent2',
metadata: 'metadata-2',
}),
];

const egress = new RoomEgress({
room: new RoomCompositeEgressRequest({ roomName: 'test-room' }),
});

roomConfig.agents = agents;
roomConfig.egress = egress;

t.roomConfig = roomConfig;

const v = new TokenVerifier(testApiKey, testSecret);
const decoded = await v.verify(await t.toJwt());

expect(decoded.roomConfig).toBeDefined();
expect(decoded.roomConfig?.name).toEqual('test-room');
expect(decoded.roomConfig?.maxParticipants).toEqual(10);
expect(decoded.roomConfig?.agents).toHaveLength(2);
expect(decoded.roomConfig?.agents?.[0].agentName).toEqual('agent1');
expect(decoded.roomConfig?.agents?.[0].metadata).toEqual('metadata-1');
expect(decoded.roomConfig?.agents?.[1].agentName).toEqual('agent2');
expect(decoded.roomConfig?.agents?.[1].metadata).toEqual('metadata-2');
expect(decoded.roomConfig?.egress?.room?.roomName).toEqual('test-room');
});
});
25 changes: 25 additions & 0 deletions packages/livekit-server-sdk/src/AccessToken.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import type { RoomConfiguration } from '@livekit/protocol';
import * as jose from 'jose';
import type { ClaimGrants, SIPGrant, VideoGrant } from './grants.js';
import { claimsToJwtPayload } from './grants.js';
Expand Down Expand Up @@ -30,6 +31,11 @@ export interface AccessTokenOptions {
* custom metadata to be passed to participants
*/
metadata?: string;

/**
* custom attributes to be passed to participants
*/
attributes?: Record<string, string>;
}

export class AccessToken {
Expand Down Expand Up @@ -76,6 +82,9 @@ export class AccessToken {
if (options?.metadata) {
this.metadata = options.metadata;
}
if (options?.attributes) {
this.attributes = options.attributes;
}
if (options?.name) {
this.name = options.name;
}
Expand Down Expand Up @@ -140,6 +149,22 @@ export class AccessToken {
this.grants.sha256 = sha;
}

get roomPreset(): string | undefined {
return this.grants.roomPreset;
}

set roomPreset(preset: string | undefined) {
this.grants.roomPreset = preset;
}

get roomConfig(): RoomConfiguration | undefined {
return this.grants.roomConfig;
}

set roomConfig(config: RoomConfiguration | undefined) {
this.grants.roomConfig = config;
}

/**
* @returns JWT encoded token
*/
Expand Down
125 changes: 125 additions & 0 deletions packages/livekit-server-sdk/src/AgentDispatchClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import {
AgentDispatch,
CreateAgentDispatchRequest,
DeleteAgentDispatchRequest,
ListAgentDispatchRequest,
ListAgentDispatchResponse,
} from '@livekit/protocol';
import ServiceBase from './ServiceBase';
import { type Rpc, TwirpRpc, livekitPackage } from './TwirpRPC';

interface CreateDispatchOptions {
// any custom data to send along with the job.
// note: this is different from room and participant metadata
metadata?: string;
}

const svc = 'AgentDispatchService';

/**
* Client to access Agent APIs
*/
export class AgentDispatchClient extends ServiceBase {
private readonly rpc: Rpc;

/**
* @param host - hostname including protocol. i.e. 'https://<project>.livekit.cloud'
* @param apiKey - API Key, can be set in env var LIVEKIT_API_KEY
* @param secret - API Secret, can be set in env var LIVEKIT_API_SECRET
*/
constructor(host: string, apiKey?: string, secret?: string) {
super(apiKey, secret);
this.rpc = new TwirpRpc(host, livekitPackage);
}

/**
* Create an explicit dispatch for an agent to join a room. To use explicit
* dispatch, your agent must be registered with an `agentName`.
* @param roomName - name of the room to dispatch to
* @param agentName - name of the agent to dispatch
* @param options - optional metadata to send along with the dispatch
* @returns the dispatch that was created
*/
async createDispatch(
roomName: string,
agentName: string,
options?: CreateDispatchOptions,
): Promise<AgentDispatch> {
const req = new CreateAgentDispatchRequest({
room: roomName,
agentName,
metadata: options?.metadata,
}).toJson();
const data = await this.rpc.request(
svc,
'CreateDispatch',
req,
await this.authHeader({ roomAdmin: true, room: roomName }),
);
return AgentDispatch.fromJson(data, { ignoreUnknownFields: true });
}

/**
* Delete an explicit dispatch for an agent in a room.
* @param dispatchId - id of the dispatch to delete
* @param roomName - name of the room the dispatch is for
*/
async deleteDispatch(dispatchId: string, roomName: string): Promise<void> {
const req = new DeleteAgentDispatchRequest({
dispatchId,
room: roomName,
}).toJson();
await this.rpc.request(
svc,
'DeleteDispatch',
req,
await this.authHeader({ roomAdmin: true, room: roomName }),
);
}

/**
* Get an Agent dispatch by ID
* @param dispatchId - id of the dispatch to get
* @param roomName - name of the room the dispatch is for
* @returns the dispatch that was found, or undefined if not found
*/
async getDispatch(dispatchId: string, roomName: string): Promise<AgentDispatch | undefined> {
const req = new ListAgentDispatchRequest({
dispatchId,
room: roomName,
}).toJson();
const data = await this.rpc.request(
svc,
'ListDispatch',
req,
await this.authHeader({ roomAdmin: true, room: roomName }),
);
const res = ListAgentDispatchResponse.fromJson(data, { ignoreUnknownFields: true });
if (res.agentDispatches.length === 0) {
return undefined;
}
return res.agentDispatches[0];
}

/**
* List all agent dispatches for a room
* @param roomName - name of the room to list dispatches for
* @returns the list of dispatches
*/
async listDispatch(roomName: string): Promise<AgentDispatch[]> {
const req = new ListAgentDispatchRequest({
room: roomName,
}).toJson();
const data = await this.rpc.request(
svc,
'ListDispatch',
req,
await this.authHeader({ roomAdmin: true, room: roomName }),
);
const res = ListAgentDispatchResponse.fromJson(data, { ignoreUnknownFields: true });
return res.agentDispatches;
}
}
2 changes: 1 addition & 1 deletion packages/livekit-server-sdk/src/EgressClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export class EgressClient extends ServiceBase {
private readonly rpc: Rpc;

/**
* @param host - hostname including protocol. i.e. 'https://cluster.livekit.io'
* @param host - hostname including protocol. i.e. 'https://<project>.livekit.cloud'
* @param apiKey - API Key, can be set in env var LIVEKIT_API_KEY
* @param secret - API Secret, can be set in env var LIVEKIT_API_SECRET
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/livekit-server-sdk/src/IngressClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export class IngressClient extends ServiceBase {
private readonly rpc: Rpc;

/**
* @param host - hostname including protocol. i.e. 'https://cluster.livekit.io'
* @param host - hostname including protocol. i.e. 'https://<project>.livekit.cloud'
* @param apiKey - API Key, can be set in env var LIVEKIT_API_KEY
* @param secret - API Secret, can be set in env var LIVEKIT_API_SECRET
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/livekit-server-sdk/src/RoomServiceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class RoomServiceClient extends ServiceBase {

/**
*
* @param host - hostname including protocol. i.e. 'https://cluster.livekit.io'
* @param host - hostname including protocol. i.e. 'https://<project>.livekit.cloud'
* @param apiKey - API Key, can be set in env var LIVEKIT_API_KEY
* @param secret - API Secret, can be set in env var LIVEKIT_API_SECRET
*/
Expand Down
Loading

1 comment on commit e389115

@juninhodeluca
Copy link

@juninhodeluca juninhodeluca commented on e389115 Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is breaking changing something. I'was working and subtly started to got this error
Image

I tried to update all my packages and did updated livekit-server-sdk to 2.8.0, but nothing
Image

looks like @livekit/agents requires 2.6.1 version of node-sdk
Image

Please sign in to comment.