Skip to content

Commit c91c476

Browse files
authored
Fixes for AgentTypes (#847)
* Fix exception when parsing AgentAttribute inputs and outputs * Don't throw on invalid values for Agent enum types * Use unknown enum value instead of null * spotless
1 parent 5acc3d8 commit c91c476

File tree

9 files changed

+280
-33
lines changed

9 files changed

+280
-33
lines changed

.changeset/calm-pears-refuse.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"client-sdk-android": patch
3+
---
4+
5+
Fix exception when parsing AgentAttribute inputs and outputs

.changeset/gold-eggs-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"client-sdk-android": patch
3+
---
4+
5+
Don't throw on invalid values for Agent enum types

livekit-android-sdk/src/main/java/io/livekit/android/room/participant/Participant.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2025 LiveKit, Inc.
2+
* Copyright 2023-2026 LiveKit, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@ import io.livekit.android.room.track.RemoteTrackPublication
2727
import io.livekit.android.room.track.Track
2828
import io.livekit.android.room.track.TrackPublication
2929
import io.livekit.android.room.types.AgentAttributes
30-
import io.livekit.android.room.types.fromMap
30+
import io.livekit.android.room.types.fromStringMap
3131
import io.livekit.android.util.FlowObservable
3232
import io.livekit.android.util.diffMapChange
3333
import io.livekit.android.util.flow
@@ -436,7 +436,7 @@ open class Participant(
436436
permissions = ParticipantPermission.fromProto(info.permission)
437437
}
438438
attributes = info.attributesMap
439-
agentAttributes = AgentAttributes.fromMap(info.attributesMap)
439+
agentAttributes = AgentAttributes.fromStringMap(info.attributesMap)
440440
state = State.fromProto(info.state)
441441
}
442442

livekit-android-sdk/src/main/java/io/livekit/android/room/types/AgentTypes.kt

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2025 LiveKit, Inc.
2+
* Copyright 2025-2026 LiveKit, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,22 +22,23 @@ import com.beust.klaxon.Converter
2222
import com.beust.klaxon.Json
2323
import com.beust.klaxon.JsonValue
2424
import com.beust.klaxon.Klaxon
25+
import io.livekit.android.util.LKLog
2526
import kotlinx.serialization.Serializable
2627

2728
private fun <T> Klaxon.convert(k: kotlin.reflect.KClass<*>, fromJson: (JsonValue) -> T, toJson: (T) -> String, isUnion: Boolean = false) =
2829
this.converter(
2930
object : Converter {
3031
@Suppress("UNCHECKED_CAST")
3132
override fun toJson(value: Any) = toJson(value as T)
32-
override fun fromJson(jv: JsonValue) = fromJson(jv) as Any
33+
override fun fromJson(jv: JsonValue) = fromJson(jv) as Any?
3334
override fun canConvert(cls: Class<*>) = cls == k.java || (isUnion && cls.superclass == k.java)
3435
},
3536
)
3637

3738
internal val klaxon = Klaxon()
38-
.convert(AgentInput::class, { AgentInput.fromValue(it.string!!) }, { "\"${it.value}\"" })
39-
.convert(AgentOutput::class, { AgentOutput.fromValue(it.string!!) }, { "\"${it.value}\"" })
40-
.convert(AgentSdkState::class, { AgentSdkState.fromValue(it.string!!) }, { "\"${it.value}\"" })
39+
.convert(AgentInput::class, { it.string?.let { AgentInput.fromValue(it) } }, { "\"${it?.value}\"" })
40+
.convert(AgentOutput::class, { it.string?.let { AgentOutput.fromValue(it) } }, { "\"${it?.value}\"" })
41+
.convert(AgentSdkState::class, { it.string?.let { AgentSdkState.fromValue(it) } }, { "\"${it?.value}\"" })
4142

4243
@Keep
4344
data class AgentAttributes(
@@ -64,28 +65,38 @@ data class AgentAttributes(
6465
enum class AgentInput(val value: String) {
6566
Audio("audio"),
6667
Text("text"),
67-
Video("video");
68+
Video("video"),
69+
Unknown("unknown"),
70+
;
6871

6972
companion object {
70-
fun fromValue(value: String): AgentInput = when (value) {
73+
fun fromValue(value: String): AgentInput? = when (value) {
7174
"audio" -> Audio
7275
"text" -> Text
7376
"video" -> Video
74-
else -> throw IllegalArgumentException()
77+
else -> {
78+
LKLog.e { "Unknown agent input value: $value" }
79+
Unknown
80+
}
7581
}
7682
}
7783
}
7884

7985
@Keep
8086
enum class AgentOutput(val value: String) {
8187
Audio("audio"),
82-
Transcription("transcription");
88+
Transcription("transcription"),
89+
Unknown("unknown"),
90+
;
8391

8492
companion object {
85-
fun fromValue(value: String): AgentOutput = when (value) {
93+
fun fromValue(value: String): AgentOutput? = when (value) {
8694
"audio" -> Audio
8795
"transcription" -> Transcription
88-
else -> throw IllegalArgumentException()
96+
else -> {
97+
LKLog.e { "Unknown agent output value: $value" }
98+
Unknown
99+
}
89100
}
90101
}
91102
}
@@ -97,16 +108,21 @@ enum class AgentSdkState(val value: String) {
97108
Initializing("initializing"),
98109
Listening("listening"),
99110
Speaking("speaking"),
100-
Thinking("thinking");
111+
Thinking("thinking"),
112+
Unknown("unknown"),
113+
;
101114

102115
companion object {
103-
fun fromValue(value: String): AgentSdkState = when (value) {
116+
fun fromValue(value: String): AgentSdkState? = when (value) {
104117
"idle" -> Idle
105118
"initializing" -> Initializing
106119
"listening" -> Listening
107120
"speaking" -> Speaking
108121
"thinking" -> Thinking
109-
else -> throw IllegalArgumentException()
122+
else -> {
123+
LKLog.e { "Unknown agent sdk state value: $value" }
124+
Unknown
125+
}
110126
}
111127
}
112128
}

livekit-android-sdk/src/main/java/io/livekit/android/room/types/AgentTypesExt.kt

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2025 LiveKit, Inc.
2+
* Copyright 2025-2026 LiveKit, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,25 +21,66 @@ import com.beust.klaxon.JsonObject
2121
// AgentTypes.kt is a generated file and should not be edited.
2222
// Add any required functions through extensions here.
2323

24-
fun AgentAttributes.Companion.fromJsonObject(jsonObject: JsonObject) =
24+
internal fun AgentAttributes.Companion.fromJsonObject(jsonObject: JsonObject) =
2525
klaxon.parseFromJsonObject<AgentAttributes>(jsonObject)
2626

27+
/**
28+
* @suppress
29+
*/
2730
fun AgentAttributes.Companion.fromMap(map: Map<String, *>): AgentAttributes {
28-
val jsonObject = JsonObject(map)
29-
return fromJsonObject(jsonObject)!!
31+
return fromJsonObject(JsonObject(map)) ?: AgentAttributes()
32+
}
33+
34+
/**
35+
* @suppress
36+
*/
37+
fun AgentAttributes.Companion.fromStringMap(map: Map<String, String>): AgentAttributes {
38+
val parseMap = mutableMapOf<String, Any?>()
39+
for ((key, converter) in AGENT_ATTRIBUTES_CONVERSION) {
40+
parseMap[key] = converter(map[key])
41+
}
42+
43+
return fromMap(parseMap)
3044
}
3145

32-
fun TranscriptionAttributes.Companion.fromJsonObject(jsonObject: JsonObject) =
46+
/**
47+
* Protobuf attribute maps are [String, String], so need to parse arrays/maps manually.
48+
* @suppress
49+
*/
50+
val AGENT_ATTRIBUTES_CONVERSION = mapOf<String, (String?) -> Any?>(
51+
"lk.agent.inputs" to { json -> json?.let { klaxon.parseArray<List<String>>(json) } },
52+
"lk.agent.outputs" to { json -> json?.let { klaxon.parseArray<List<String>>(json) } },
53+
"lk.agent.state" to { json -> json },
54+
"lk.publish_on_behalf" to { json -> json },
55+
)
56+
57+
internal fun TranscriptionAttributes.Companion.fromJsonObject(jsonObject: JsonObject) =
3358
klaxon.parseFromJsonObject<TranscriptionAttributes>(jsonObject)
3459

60+
/**
61+
* @suppress
62+
*/
3563
fun TranscriptionAttributes.Companion.fromMap(map: Map<String, *>): TranscriptionAttributes {
36-
var map = map
37-
val transcriptionFinal = map["lk.transcription_final"]
38-
if (transcriptionFinal !is Boolean) {
39-
map = map.toMutableMap()
40-
map["lk.transcription_final"] = transcriptionFinal?.toString()?.toBooleanStrictOrNull()
41-
}
42-
val jsonObject = JsonObject(map)
64+
return fromJsonObject(JsonObject(map)) ?: TranscriptionAttributes()
65+
}
4366

44-
return fromJsonObject(jsonObject)!!
67+
/**
68+
* @suppress
69+
*/
70+
fun TranscriptionAttributes.Companion.fromStringMap(map: Map<String, String>): TranscriptionAttributes {
71+
val parseMap = mutableMapOf<String, Any?>()
72+
for ((key, converter) in TRANSCRIPTION_ATTRIBUTES_CONVERSION) {
73+
parseMap[key] = converter(map[key])
74+
}
75+
return fromMap(parseMap)
4576
}
77+
78+
/**
79+
* Protobuf attribute maps are [String, String], so need to parse arrays/maps manually.
80+
* @suppress
81+
*/
82+
val TRANSCRIPTION_ATTRIBUTES_CONVERSION = mapOf<String, (String?) -> Any?>(
83+
"lk.segment_id" to { json -> json },
84+
"lk.transcribed_track_id" to { json -> json },
85+
"lk.transcription_final" to { json -> json?.let { klaxon.parse(json) } },
86+
)

livekit-android-test/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ dependencies {
9797
testImplementation libs.junit
9898
testImplementation libs.robolectric
9999
testImplementation libs.okhttp.mockwebserver
100+
testImplementation libs.klaxon
100101
kaptTest libs.dagger.compiler
101102

102103
androidTestImplementation libs.androidx.test.junit

livekit-android-test/src/test/java/io/livekit/android/room/participant/ParticipantTest.kt

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2025 LiveKit, Inc.
2+
* Copyright 2023-2026 LiveKit, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,10 +18,17 @@ package io.livekit.android.room.participant
1818

1919
import io.livekit.android.events.ParticipantEvent
2020
import io.livekit.android.room.track.TrackPublication
21+
import io.livekit.android.room.types.AgentInput
22+
import io.livekit.android.room.types.AgentOutput
23+
import io.livekit.android.room.types.AgentSdkState
2124
import io.livekit.android.test.coroutines.TestCoroutineRule
2225
import io.livekit.android.test.events.EventCollector
2326
import kotlinx.coroutines.ExperimentalCoroutinesApi
2427
import kotlinx.coroutines.test.runTest
28+
import kotlinx.serialization.encodeToString
29+
import kotlinx.serialization.json.Json
30+
import kotlinx.serialization.json.add
31+
import kotlinx.serialization.json.buildJsonArray
2532
import livekit.LivekitModels
2633
import org.junit.Assert
2734
import org.junit.Assert.assertEquals
@@ -62,6 +69,74 @@ class ParticipantTest {
6269
assertEquals(INFO, participant.participantInfo)
6370
}
6471

72+
@Test
73+
fun agentAttributes() = runTest {
74+
val json = Json
75+
val inputs = json.encodeToString(
76+
buildJsonArray {
77+
add("audio")
78+
add("text")
79+
add("video")
80+
},
81+
)
82+
83+
val outputs = json.encodeToString(
84+
buildJsonArray {
85+
add("audio")
86+
add("transcription")
87+
},
88+
)
89+
90+
println(inputs)
91+
println(outputs)
92+
93+
val agentInfo = with(INFO.toBuilder()) {
94+
putAttributes("lk.agent.inputs", inputs)
95+
putAttributes("lk.agent.outputs", outputs)
96+
putAttributes("lk.agent.state", "idle")
97+
putAttributes("lk.publish_on_behalf", "other_participant_id")
98+
build()
99+
}
100+
participant.updateFromInfo(agentInfo)
101+
102+
val agentAttributes = participant.agentAttributes
103+
assertTrue(agentAttributes.lkAgentInputs?.contains(AgentInput.Audio) ?: false)
104+
assertTrue(agentAttributes.lkAgentInputs?.contains(AgentInput.Text) ?: false)
105+
assertTrue(agentAttributes.lkAgentInputs?.contains(AgentInput.Video) ?: false)
106+
assertTrue(agentAttributes.lkAgentOutputs?.contains(AgentOutput.Audio) ?: false)
107+
assertTrue(agentAttributes.lkAgentOutputs?.contains(AgentOutput.Transcription) ?: false)
108+
109+
// WARNING: Do not change AgentSdkState to AgentState.
110+
// The attribute definitions are renamed, so this test is used
111+
// to throw a compile error if it was renamed back.
112+
assertEquals(AgentSdkState.Idle, agentAttributes.lkAgentState)
113+
assertEquals("other_participant_id", agentAttributes.lkPublishOnBehalf)
114+
}
115+
116+
@Test
117+
fun invalidAgentAttributeDoesNotThrow() = runTest {
118+
val json = Json
119+
val inputs = json.encodeToString(
120+
buildJsonArray {
121+
add("audio")
122+
add("lorem")
123+
add("video")
124+
},
125+
)
126+
127+
val agentInfo = with(INFO.toBuilder()) {
128+
putAttributes("lk.agent.inputs", inputs)
129+
build()
130+
}
131+
participant.updateFromInfo(agentInfo)
132+
133+
val agentAttributes = participant.agentAttributes
134+
assertTrue(agentAttributes.lkAgentInputs?.contains(AgentInput.Audio) ?: false)
135+
assertTrue(agentAttributes.lkAgentInputs?.contains(AgentInput.Video) ?: false)
136+
assertTrue(agentAttributes.lkAgentInputs?.contains(AgentInput.Unknown) ?: false)
137+
assertTrue(agentAttributes.lkAgentInputs?.size == 3)
138+
}
139+
65140
@Test
66141
fun setMetadataCallsListeners() = runTest {
67142
class MetadataListener : ParticipantListener {

0 commit comments

Comments
 (0)