diff --git a/Package.resolved b/Package.resolved index c06eb6f..63c9dd5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/daily-co/daily-client-ios", "state" : { - "revision" : "1b84803a17766240007f11c553ca7debbfcef33b", - "version" : "0.22.0" + "revision" : "431938db25e5807120e89e2dc5bab1c076729f59", + "version" : "0.31.0" } } ], diff --git a/Sources/Models/AnyCodable.swift b/Sources/Models/AnyCodable.swift new file mode 100644 index 0000000..6cf0b15 --- /dev/null +++ b/Sources/Models/AnyCodable.swift @@ -0,0 +1,80 @@ +// +// AnyCodable.swift +// +// +// Created by Anton Begehr on 2025-07-10. +// + +import Foundation + +public enum AnyCodable: Codable { + case string(String) + case int(Int) + case double(Double) + case bool(Bool) + case array([AnyCodable]) + case dictionary([String: AnyCodable]) + case null + + public init(_ value: Any?) { + switch value { + case let string as String: + self = .string(string) + case let int as Int: + self = .int(int) + case let double as Double: + self = .double(double) + case let bool as Bool: + self = .bool(bool) + case let array as [Any]: + self = .array(array.map { AnyCodable($0) }) + case let dict as [String: Any]: + self = .dictionary(dict.mapValues { AnyCodable($0) }) + case nil: + self = .null + default: + self = .null // Fallback for unsupported types + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let string = try? container.decode(String.self) { + self = .string(string) + } else if let int = try? container.decode(Int.self) { + self = .int(int) + } else if let double = try? container.decode(Double.self) { + self = .double(double) + } else if let bool = try? container.decode(Bool.self) { + self = .bool(bool) + } else if let array = try? container.decode([AnyCodable].self) { + self = .array(array) + } else if let dict = try? container.decode([String: AnyCodable].self) { + self = .dictionary(dict) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON type") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let value): + try container.encode(value) + case .int(let value): + try container.encode(value) + case .double(let value): + try container.encode(value) + case .bool(let value): + try container.encode(value) + case .array(let value): + try container.encode(value) + case .dictionary(let value): + try container.encode(value) + case .null: + try container.encodeNil() + } + } +} diff --git a/Sources/Models/AppMessage.swift b/Sources/Models/AppMessage.swift index 0f9c545..04dffb2 100644 --- a/Sources/Models/AppMessage.swift +++ b/Sources/Models/AppMessage.swift @@ -11,6 +11,7 @@ struct AppMessage: Codable { enum MessageType: String, Codable { case hang case functionCall = "function-call" + case toolCalls = "tool-calls" case transcript case speechUpdate = "speech-update" case metadata diff --git a/Sources/Models/ConversationUpdate.swift b/Sources/Models/ConversationUpdate.swift index 917a3e8..7c95dc8 100644 --- a/Sources/Models/ConversationUpdate.swift +++ b/Sources/Models/ConversationUpdate.swift @@ -5,10 +5,21 @@ public struct Message: Codable { case user = "user" case assistant = "assistant" case system = "system" + case tool = "tool" } - + + enum CodingKeys: String, CodingKey { + case role + case content + case toolCalls = "tool_calls" + case toolCallId = "tool_call_id" + } + public let role: Role - public let content: String + public let content: String? // For role=tool, has the response of the tool call. For role=assistant with tool_calls is nil. + public let toolCalls: [ToolCall]? // Only for role=assistant with tool calls. + public let toolCallId: String? // Only for role=tool, with tool response. + } public struct ConversationUpdate: Codable { diff --git a/Sources/Models/ToolCalls.swift b/Sources/Models/ToolCalls.swift new file mode 100644 index 0000000..778d565 --- /dev/null +++ b/Sources/Models/ToolCalls.swift @@ -0,0 +1,36 @@ +// +// ToolCalls.swift +// +// +// Created by Anton Begehr on 2025-07-10. +// + +import Foundation + +public struct ToolCalls: Codable { + public let toolCalls: [ToolCall] +} + +public struct ToolCall: Codable { + enum CodingKeys: CodingKey { + case id + case type + case function + } + + public let id: String + public let type: String + public let function: Function +} + +public extension ToolCall { + struct Function: Codable { + enum CodingKeys: CodingKey { + case name + case arguments + } + + public let name: String + public let arguments: AnyCodable // In `conversation-update`, this will be an encoded string. In `tool-calls`, this will be a dictionary. + } +} diff --git a/Sources/Models/VapiError.swift b/Sources/Models/VapiError.swift index 9502956..f4414b4 100644 --- a/Sources/Models/VapiError.swift +++ b/Sources/Models/VapiError.swift @@ -7,7 +7,7 @@ import Foundation -public enum VapiError: Swift.Error { +public enum VapiError: LocalizedError { case invalidURL case customError(String) case existingCallInProgress @@ -15,3 +15,22 @@ public enum VapiError: Swift.Error { case decodingError(message: String, response: String? = nil) case invalidJsonData } + +extension VapiError { + public var errorDescription: String? { + switch self { + case .invalidURL: + return "URL is invalid" + case .customError(let message): + return message + case .existingCallInProgress: + return "An existing call is in progress" + case .noCallInProgress: + return "No call in progress" + case .decodingError(let message, let response): + return "\(message), \(response ?? "no response")" + case .invalidJsonData: + return "Invalid JSON data" + } + } +} diff --git a/Sources/Vapi.swift b/Sources/Vapi.swift index 69bdaa3..11fe88f 100644 --- a/Sources/Vapi.swift +++ b/Sources/Vapi.swift @@ -42,6 +42,7 @@ public final class Vapi: CallClientDelegate { case callDidEnd case transcript(Transcript) case functionCall(FunctionCall) + case toolCalls([ToolCall]) case speechUpdate(SpeechUpdate) case metadata(Metadata) case conversationUpdate(ConversationUpdate) @@ -465,6 +466,9 @@ public final class Vapi: CallClientDelegate { let functionCall = FunctionCall(name: name, parameters: parameters) event = Event.functionCall(functionCall) + case .toolCalls: + let toolCalls = try decoder.decode(ToolCalls.self, from: unescapedData) + event = Event.toolCalls(toolCalls.toolCalls) case .hang: event = Event.hang case .transcript: