Skip to content

Commit 47ff168

Browse files
akkawelldavidliu
andauthored
feat: add support for AgentDispatchService (#113)
* feat: add support for AgentDispatchService * upgrade: upgrade CI.yml actions/cache@v2 to actions/cache@v4 * revert: revert CI.yml format * change: AgentDispatchServiceClient.kt#createDispatch allow metadata is null * change: AgentDispatchServiceClient.kt#createDispatch add @jvmoverloads * change: change format * change: add changeset * chore: spotless --------- Co-authored-by: davidliu <[email protected]>
1 parent d48f52e commit 47ff168

File tree

6 files changed

+296
-2
lines changed

6 files changed

+296
-2
lines changed

.changeset/good-carrots-do.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"server-sdk-kotlin": minor
3+
---
4+
5+
Implement AgentDispatchService

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
java-version: '12'
3636
distribution: 'adopt'
3737

38-
- uses: actions/cache@v2
38+
- uses: actions/cache@v4
3939
with:
4040
path: |
4141
~/.gradle/caches
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2025 LiveKit, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.livekit.server
18+
19+
import livekit.LivekitAgentDispatch
20+
import retrofit2.Call
21+
import retrofit2.http.Body
22+
import retrofit2.http.Header
23+
import retrofit2.http.Headers
24+
import retrofit2.http.POST
25+
26+
/**
27+
* Retrofit Interface for accessing the AgentDispatchService Apis.
28+
*/
29+
interface AgentDispatchService {
30+
@Headers("Content-Type: application/protobuf")
31+
@POST("/twirp/livekit.AgentDispatchService/CreateDispatch")
32+
fun createDispatch(
33+
@Body request: LivekitAgentDispatch.CreateAgentDispatchRequest,
34+
@Header("Authorization") authorization: String,
35+
): Call<LivekitAgentDispatch.AgentDispatch>
36+
37+
@Headers("Content-Type: application/protobuf")
38+
@POST("/twirp/livekit.AgentDispatchService/DeleteDispatch")
39+
fun deleteDispatch(
40+
@Body request: LivekitAgentDispatch.DeleteAgentDispatchRequest,
41+
@Header("Authorization") authorization: String,
42+
): Call<LivekitAgentDispatch.AgentDispatch>
43+
44+
@Headers("Content-Type: application/protobuf")
45+
@POST("/twirp/livekit.AgentDispatchService/ListDispatch")
46+
fun listDispatch(
47+
@Body request: LivekitAgentDispatch.ListAgentDispatchRequest,
48+
@Header("Authorization") authorization: String,
49+
): Call<LivekitAgentDispatch.ListAgentDispatchResponse>
50+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2025 LiveKit, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.livekit.server
18+
19+
import io.livekit.server.okhttp.OkHttpFactory
20+
import io.livekit.server.okhttp.OkHttpHolder
21+
import io.livekit.server.retrofit.TransformCall
22+
import livekit.LivekitAgentDispatch
23+
import okhttp3.OkHttpClient
24+
import retrofit2.Call
25+
import retrofit2.Retrofit
26+
import retrofit2.converter.protobuf.ProtoConverterFactory
27+
import java.util.function.Supplier
28+
29+
/**
30+
* A client for explicit agent dispatch.
31+
*
32+
* See: [Dispatching agents](https://docs.livekit.io/agents/build/dispatch/#explicit-agent-dispatch)
33+
*/
34+
class AgentDispatchServiceClient(
35+
private val service: AgentDispatchService,
36+
private val apiKey: String,
37+
private val secret: String,
38+
) {
39+
40+
/**
41+
* Creates an agent dispatch in a room.
42+
* @param room Name of the room to create dispatch in
43+
* @param agentName Name of the agent to dispatch
44+
* @param metadata Optional metadata to attach to the dispatch
45+
* @return Created agent dispatch
46+
*/
47+
@JvmOverloads
48+
fun createDispatch(
49+
room: String,
50+
agentName: String,
51+
metadata: String? = null,
52+
): Call<LivekitAgentDispatch.AgentDispatch> {
53+
val request = with(LivekitAgentDispatch.CreateAgentDispatchRequest.newBuilder()) {
54+
setRoom(room)
55+
setAgentName(agentName)
56+
if (metadata != null) {
57+
setMetadata(metadata)
58+
}
59+
build()
60+
}
61+
val credentials = authHeader(RoomAdmin(true), RoomName(room))
62+
return service.createDispatch(request, credentials)
63+
}
64+
65+
/**
66+
* Deletes an agent dispatch from a room.
67+
* @param room Name of the room to delete dispatch from
68+
* @param dispatchId ID of the dispatch to delete
69+
* @return Deleted agent dispatch
70+
*/
71+
fun deleteDispatch(room: String, dispatchId: String): Call<LivekitAgentDispatch.AgentDispatch> {
72+
val request = LivekitAgentDispatch.DeleteAgentDispatchRequest.newBuilder()
73+
.setRoom(room)
74+
.setDispatchId(dispatchId)
75+
.build()
76+
val credentials = authHeader(RoomAdmin(true), RoomName(room))
77+
return service.deleteDispatch(request, credentials)
78+
}
79+
80+
/**
81+
* List all agent dispatches in a room.
82+
* @param room Name of the room to list dispatches from
83+
* @return List of agent dispatches
84+
*/
85+
fun listDispatch(room: String): Call<List<LivekitAgentDispatch.AgentDispatch>> {
86+
val request = LivekitAgentDispatch.ListAgentDispatchRequest.newBuilder()
87+
.setRoom(room)
88+
.build()
89+
val credentials = authHeader(RoomAdmin(true), RoomName(room))
90+
return TransformCall(service.listDispatch(request, credentials)) {
91+
it.agentDispatchesList
92+
}
93+
}
94+
95+
private fun authHeader(vararg videoGrants: VideoGrant): String {
96+
val accessToken = AccessToken(apiKey, secret)
97+
accessToken.addGrants(*videoGrants)
98+
99+
val jwt = accessToken.toJwt()
100+
101+
return "Bearer $jwt"
102+
}
103+
104+
companion object {
105+
106+
/**
107+
* Create a new [AgentDispatchServiceClient] with the given host, api key, and secret.
108+
*
109+
* @param okHttpSupplier provide an [OkHttpFactory] if you wish to customize the http client
110+
* (e.g. proxy, timeout, certificate/auth settings), or supply your own OkHttpClient
111+
* altogether to pool resources with [OkHttpHolder].
112+
*
113+
* @see OkHttpHolder
114+
* @see OkHttpFactory
115+
*/
116+
@JvmStatic
117+
@JvmOverloads
118+
fun createClient(
119+
host: String,
120+
apiKey: String,
121+
secret: String,
122+
okHttpSupplier: Supplier<OkHttpClient> = OkHttpFactory(),
123+
): AgentDispatchServiceClient {
124+
val okhttp = okHttpSupplier.get()
125+
val service = Retrofit.Builder()
126+
.baseUrl(host)
127+
.addConverterFactory(ProtoConverterFactory.create())
128+
.client(okhttp)
129+
.build()
130+
.create(AgentDispatchService::class.java)
131+
132+
return AgentDispatchServiceClient(service, apiKey, secret)
133+
}
134+
}
135+
}

src/main/kotlin/io/livekit/server/okhttp/OkHttpSupplier.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 2025 LiveKit, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package io.livekit.server.okhttp
218

319
import okhttp3.OkHttpClient
@@ -42,4 +58,4 @@ constructor(
4258
}
4359

4460
override fun get() = okHttp
45-
}
61+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2025 LiveKit, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.livekit.server
18+
19+
import io.livekit.server.okhttp.OkHttpFactory
20+
import kotlin.test.BeforeTest
21+
import kotlin.test.Test
22+
import kotlin.test.assertNotNull
23+
import kotlin.test.assertTrue
24+
25+
class AgentDispatchServiceClientTest {
26+
27+
companion object {
28+
const val HOST = TestConstants.HOST
29+
const val KEY = TestConstants.KEY
30+
const val SECRET = TestConstants.SECRET
31+
32+
const val ROOM_NAME = "room_name"
33+
const val METADATA = "metadata"
34+
}
35+
36+
lateinit var client: AgentDispatchServiceClient
37+
lateinit var roomClient: RoomServiceClient
38+
39+
@BeforeTest
40+
fun setup() {
41+
client = AgentDispatchServiceClient.createClient(HOST, KEY, SECRET, OkHttpFactory(true, null))
42+
roomClient = RoomServiceClient.createClient(HOST, KEY, SECRET, OkHttpFactory(true, null))
43+
}
44+
45+
@Test
46+
fun createAgentDispatch() {
47+
roomClient.createRoom(name = ROOM_NAME).execute()
48+
client.createDispatch(
49+
room = ROOM_NAME,
50+
agentName = "agent",
51+
).execute()
52+
}
53+
54+
@Test
55+
fun listAgentDispatch() {
56+
roomClient.createRoom(name = ROOM_NAME).execute()
57+
val dispatchResp = client.createDispatch(
58+
room = ROOM_NAME,
59+
agentName = "agent",
60+
metadata = METADATA,
61+
).execute()
62+
val dispatch = dispatchResp.body()
63+
64+
assertNotNull(dispatch?.id)
65+
66+
val listResp = client.listDispatch(room = ROOM_NAME).execute()
67+
val allDispatches = listResp.body()
68+
assertTrue(listResp.isSuccessful)
69+
assertNotNull(allDispatches)
70+
assertTrue(allDispatches.any { item -> item.id == dispatch?.id })
71+
}
72+
73+
@Test
74+
fun deleteAgentDispatch() {
75+
roomClient.createRoom(name = ROOM_NAME).execute()
76+
val dispatchResp = client.createDispatch(
77+
room = ROOM_NAME,
78+
agentName = "agent",
79+
metadata = METADATA,
80+
).execute()
81+
val dispatch = dispatchResp.body()
82+
83+
assertNotNull(dispatch?.id)
84+
85+
val deleteResp = client.deleteDispatch(room = ROOM_NAME, dispatchId = dispatch?.id ?: "").execute()
86+
assertTrue(deleteResp.isSuccessful)
87+
}
88+
}

0 commit comments

Comments
 (0)