@@ -102,6 +102,39 @@ public class ElevenLabsSDK {
102
102
Data ( base64Encoded: base64)
103
103
}
104
104
105
+ // MARK: - Client Tools
106
+
107
+ public typealias ClientToolHandler = @Sendable ( Parameters) async throws -> String ?
108
+
109
+ public typealias Parameters = [ String : Any ]
110
+
111
+ public struct ClientTools : Sendable {
112
+ private var tools : [ String : ClientToolHandler ] = [ : ]
113
+ private let lock = NSLock ( ) // Ensure thread safety
114
+
115
+ public init ( ) { }
116
+
117
+ public mutating func register( _ name: String , handler: @escaping @Sendable ClientToolHandler ) {
118
+ lock. withLock {
119
+ tools [ name] = handler
120
+ }
121
+ }
122
+
123
+ public func handle( _ name: String , parameters: Parameters ) async throws -> String ? {
124
+ let handler : ClientToolHandler ? = lock. withLock { tools [ name] }
125
+ guard let handler = handler else {
126
+ throw ClientToolError . handlerNotFound ( name)
127
+ }
128
+ return try await handler ( parameters)
129
+ }
130
+ }
131
+
132
+ public enum ClientToolError : Error {
133
+ case handlerNotFound( String )
134
+ case invalidParameters
135
+ case executionFailed( String )
136
+ }
137
+
105
138
// MARK: - Audio Processing
106
139
107
140
public class AudioConcatProcessor {
@@ -190,14 +223,14 @@ public class ElevenLabsSDK {
190
223
public let overrides : ConversationConfigOverride ?
191
224
public let customLlmExtraBody : [ String : LlmExtraBodyValue ] ?
192
225
193
- public init ( signedUrl: String , overrides: ConversationConfigOverride ? = nil , customLlmExtraBody: [ String : LlmExtraBodyValue ] ? = nil ) {
226
+ public init ( signedUrl: String , overrides: ConversationConfigOverride ? = nil , customLlmExtraBody: [ String : LlmExtraBodyValue ] ? = nil , clientTools _ : ClientTools = ClientTools ( ) ) {
194
227
self . signedUrl = signedUrl
195
228
agentId = nil
196
229
self . overrides = overrides
197
230
self . customLlmExtraBody = customLlmExtraBody
198
231
}
199
232
200
- public init ( agentId: String , overrides: ConversationConfigOverride ? = nil , customLlmExtraBody: [ String : LlmExtraBodyValue ] ? = nil ) {
233
+ public init ( agentId: String , overrides: ConversationConfigOverride ? = nil , customLlmExtraBody: [ String : LlmExtraBodyValue ] ? = nil , clientTools _ : ClientTools = ClientTools ( ) ) {
201
234
self . agentId = agentId
202
235
signedUrl = nil
203
236
self . overrides = overrides
@@ -559,6 +592,7 @@ public class ElevenLabsSDK {
559
592
private let input : Input
560
593
private let output : Output
561
594
private let callbacks : Callbacks
595
+ private let clientTools : ClientTools ?
562
596
563
597
private let modeLock = NSLock ( )
564
598
private let statusLock = NSLock ( )
@@ -649,11 +683,12 @@ public class ElevenLabsSDK {
649
683
}
650
684
}
651
685
652
- private init ( connection: Connection , input: Input , output: Output , callbacks: Callbacks ) {
686
+ private init ( connection: Connection , input: Input , output: Output , callbacks: Callbacks , clientTools : ClientTools ? ) {
653
687
self . connection = connection
654
688
self . input = input
655
689
self . output = output
656
690
self . callbacks = callbacks
691
+ self . clientTools = clientTools
657
692
658
693
// Set the onProcess callback
659
694
audioConcatProcessor. onProcess = { [ weak self] finished in
@@ -672,8 +707,9 @@ public class ElevenLabsSDK {
672
707
/// - Parameters:
673
708
/// - config: Session configuration
674
709
/// - callbacks: Callbacks for conversation events
710
+ /// - clientTools: Client tools callbacks (optional)
675
711
/// - Returns: A started `Conversation` instance
676
- public static func startSession( config: SessionConfig , callbacks: Callbacks = Callbacks ( ) ) async throws -> Conversation {
712
+ public static func startSession( config: SessionConfig , callbacks: Callbacks = Callbacks ( ) , clientTools : ClientTools ? = nil ) async throws -> Conversation {
677
713
// Step 1: Configure the audio session
678
714
try ElevenLabsSDK . configureAudioSession ( )
679
715
@@ -687,7 +723,7 @@ public class ElevenLabsSDK {
687
723
let output = try await Output . create ( sampleRate: Double ( connection. sampleRate) )
688
724
689
725
// Step 5: Initialize the Conversation
690
- let conversation = Conversation ( connection: connection, input: input, output: output, callbacks: callbacks)
726
+ let conversation = Conversation ( connection: connection, input: input, output: output, callbacks: callbacks, clientTools : clientTools )
691
727
692
728
// Step 6: Start the AVAudioEngine
693
729
try output. engine. start ( )
@@ -740,6 +776,9 @@ public class ElevenLabsSDK {
740
776
}
741
777
742
778
switch type {
779
+ case " client_tool_call " :
780
+ handleClientToolCall ( json)
781
+
743
782
case " interruption " :
744
783
handleInterruptionEvent ( json)
745
784
@@ -776,6 +815,52 @@ public class ElevenLabsSDK {
776
815
}
777
816
}
778
817
818
+ private func handleClientToolCall( _ json: [ String : Any ] ) {
819
+ guard let toolCall = json [ " client_tool_call " ] as? [ String : Any ] ,
820
+ let toolName = toolCall [ " tool_name " ] as? String ,
821
+ let toolCallId = toolCall [ " tool_call_id " ] as? String ,
822
+ let parameters = toolCall [ " parameters " ] as? [ String : Any ]
823
+ else {
824
+ callbacks. onError ( " Invalid client tool call format " , json)
825
+ return
826
+ }
827
+
828
+ // Serialize parameters to JSON Data for thread-safety
829
+ let serializedParameters : Data
830
+ do {
831
+ serializedParameters = try JSONSerialization . data ( withJSONObject: parameters, options: [ ] )
832
+ } catch {
833
+ callbacks. onError ( " Failed to serialize parameters " , error)
834
+ return
835
+ }
836
+
837
+ // Execute in a Task (now safe because of serializedParameters)
838
+ Task { [ toolName, toolCallId, serializedParameters] in
839
+ do {
840
+ // Deserialize within the Task to pass into clientTools.handle
841
+ let deserializedParameters = try JSONSerialization . jsonObject ( with: serializedParameters) as? [ String : Any ] ?? [ : ]
842
+
843
+ let result = try await clientTools? . handle ( toolName, parameters: deserializedParameters)
844
+
845
+ let response : [ String : Any ] = [
846
+ " type " : " client_tool_result " ,
847
+ " tool_call_id " : toolCallId,
848
+ " result " : result ?? " " ,
849
+ " is_error " : false ,
850
+ ]
851
+ sendWebSocketMessage ( response)
852
+ } catch {
853
+ let response : [ String : Any ] = [
854
+ " type " : " client_tool_result " ,
855
+ " tool_call_id " : toolCallId,
856
+ " result " : error. localizedDescription,
857
+ " is_error " : true ,
858
+ ]
859
+ sendWebSocketMessage ( response)
860
+ }
861
+ }
862
+ }
863
+
779
864
private func handleInterruptionEvent( _ json: [ String : Any ] ) {
780
865
guard let event = json [ " interruption_event " ] as? [ String : Any ] ,
781
866
let eventId = event [ " event_id " ] as? Int else { return }
0 commit comments