Skip to content

Commit 8de0f65

Browse files
authored
Consume generated attributes (#743)
- Adds generated attributes https://github.com/livekit/attribute-definitions - Consumes agent attributes via `Participant.State` (cache) - Transcription attributes will come soon with moving the logic from the VoiceAgent → SDK A basic test case: - See if the agent (visualizer) state is handled properly - Avatars are also fine - You may need livekit/components-swift#27
1 parent d59536a commit 8de0f65

File tree

7 files changed

+135
-63
lines changed

7 files changed

+135
-63
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2025 LiveKit
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+
extension AgentState: CustomStringConvertible {
18+
public var description: String {
19+
rawValue.capitalized
20+
}
21+
}

Sources/LiveKit/Agent/AgentState.swift

Lines changed: 0 additions & 54 deletions
This file was deleted.

Sources/LiveKit/Agent/Participant+Agent.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,20 @@ public extension Participant {
2525
}
2626
}
2727

28-
@objc
2928
var agentState: AgentState {
30-
guard isAgent else { return .unknown }
31-
guard let attrString = attributes[agentStateAttributeKey] else { return .unknown }
32-
guard let state = AgentState.fromString(attrString) else { return .unknown }
33-
return state
29+
_state.agentAttributes?.lkAgentState ?? .idle
30+
}
31+
32+
@objc
33+
var agentStateString: String {
34+
agentState.rawValue
3435
}
3536
}
3637

3738
public extension Participant {
3839
private var publishingOnBehalf: [Participant.Identity: Participant] {
3940
guard let _room else { return [:] }
40-
return _room.allParticipants.filter { $0.value.attributes[publishOnBehalfAttributeKey] == identity?.stringValue }
41+
return _room.allParticipants.filter { $0.value._state.agentAttributes?.lkPublishOnBehalf == identity?.stringValue }
4142
}
4243

4344
/// The avatar worker participant associated with the agent.

Sources/LiveKit/Agent/Room+Agent.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,14 @@
1616

1717
import Foundation
1818

19-
let publishOnBehalfAttributeKey = "lk.publish_on_behalf"
20-
2119
public extension Room {
2220
/// A dictionary containing all agent participants.
2321
///
2422
/// - Note: This will not include participants that are publishing on behalf of another participant
2523
/// e.g. avatar workers. To access them directly use ``Participant/avatarWorker`` property of `agentParticipant`.
2624
@objc
2725
var agentParticipants: [Participant.Identity: Participant] {
28-
allParticipants.filter { $0.value.isAgent && $0.value.attributes[publishOnBehalfAttributeKey] == nil }
26+
allParticipants.filter { $0.value.isAgent && $0.value._state.agentAttributes?.lkPublishOnBehalf == nil }
2927
}
3028

3129
/// The first agent participant.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2025 LiveKit
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+
import Foundation
18+
19+
extension Encodable {
20+
func mapped<T: Decodable>(to type: T.Type,
21+
encoder: JSONEncoder = JSONEncoder(),
22+
decoder: JSONDecoder = JSONDecoder()) -> T?
23+
{
24+
guard let encoded = try? encoder.encode(self),
25+
let decoded = try? decoder.decode(type, from: encoded) else { return nil }
26+
27+
return decoded
28+
}
29+
}

Sources/LiveKit/Participant/Participant.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public class Participant: NSObject, @unchecked Sendable, ObservableObject, Logga
9595
var permissions = ParticipantPermissions()
9696
var trackPublications = [Track.Sid: TrackPublication]()
9797
var attributes = [String: String]()
98+
var agentAttributes: AgentAttributes?
9899
}
99100

100101
struct InternalState: Equatable, Hashable {
@@ -241,6 +242,7 @@ public class Participant: NSObject, @unchecked Sendable, ObservableObject, Logga
241242
$0.metadata = info.metadata
242243
$0.kind = info.kind.toLKType()
243244
$0.attributes = info.attributes
245+
$0.agentAttributes = info.attributes.mapped(to: AgentAttributes.self)
244246
$0.state = info.state.toLKType()
245247

246248
// Attempt to get millisecond precision.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2025 LiveKit
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+
import Foundation
18+
19+
// Quicktype cannot generate both at the same time
20+
extension AgentAttributes: Hashable {}
21+
extension AgentAttributes: Equatable {}
22+
23+
// MARK: - AgentAttributes
24+
25+
struct AgentAttributes: Codable, Sendable {
26+
let lkAgentInputs: [AgentInput]?
27+
let lkAgentOutputs: [AgentOutput]?
28+
let lkAgentState: AgentState?
29+
let lkPublishOnBehalf: String?
30+
31+
enum CodingKeys: String, CodingKey {
32+
case lkAgentInputs = "lk.agent.inputs"
33+
case lkAgentOutputs = "lk.agent.outputs"
34+
case lkAgentState = "lk.agent.state"
35+
case lkPublishOnBehalf = "lk.publish_on_behalf"
36+
}
37+
}
38+
39+
enum AgentInput: String, Codable, Sendable {
40+
case audio
41+
case text
42+
case video
43+
}
44+
45+
enum AgentOutput: String, Codable, Sendable {
46+
case audio
47+
case transcription
48+
}
49+
50+
public enum AgentState: String, Codable, Sendable {
51+
case idle
52+
case initializing
53+
case listening
54+
case speaking
55+
case thinking
56+
}
57+
58+
/// Schema for transcription-related attributes
59+
60+
// MARK: - TranscriptionAttributes
61+
62+
struct TranscriptionAttributes: Codable, Sendable {
63+
/// The segment id of the transcription
64+
let lkSegmentID: String?
65+
/// The associated track id of the transcription
66+
let lkTranscribedTrackID: String?
67+
/// Whether the transcription is final
68+
let lkTranscriptionFinal: Bool?
69+
70+
enum CodingKeys: String, CodingKey {
71+
case lkSegmentID = "lk.segment_id"
72+
case lkTranscribedTrackID = "lk.transcribed_track_id"
73+
case lkTranscriptionFinal = "lk.transcription_final"
74+
}
75+
}

0 commit comments

Comments
 (0)