Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 2 additions & 20 deletions Examples/SwiftUICallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,10 @@ class CallManager: ObservableObject {
self?.callState = .started
case .callDidEnd:
self?.callState = .ended
case .speechUpdate:
print(event)
case .conversationUpdate:
print(event)
case .functionCall:
print(event)
case .hang:
print(event)
case .metadata:
print(event)
case .transcript:
print(event)
case .statusUpdate:
print(event)
case .modelOutput:
print(event)
case .userInterrupted:
print(event)
case .voiceInput:
print(event)
case .error(let error):
print("Error: \(error)")
default:
print(event)
}
}
.store(in: &cancellables)
Expand Down
52 changes: 52 additions & 0 deletions Sources/Models/AnyCodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation

public struct AnyCodable: Codable {
public let value: Any

public init(_ value: Any) {
self.value = value
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
value = NSNull()
} else if let bool = try? container.decode(Bool.self) {
value = bool
} else if let int = try? container.decode(Int.self) {
value = int
} else if let double = try? container.decode(Double.self) {
value = double
} else if let string = try? container.decode(String.self) {
value = string
} else if let array = try? container.decode([AnyCodable].self) {
value = array.map { $0.value }
} else if let dict = try? container.decode([String: AnyCodable].self) {
value = dict.mapValues { $0.value }
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value")
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch value {
case is NSNull:
try container.encodeNil()
case let bool as Bool:
try container.encode(bool)
case let int as Int:
try container.encode(int)
case let double as Double:
try container.encode(double)
case let string as String:
try container.encode(string)
case let array as [Any]:
try container.encode(array.map { AnyCodable($0) })
case let dict as [String: Any]:
try container.encode(dict.mapValues { AnyCodable($0) })
default:
throw EncodingError.invalidValue(value, .init(codingPath: encoder.codingPath, debugDescription: "Unsupported value"))
}
}
}
75 changes: 67 additions & 8 deletions Sources/Models/AppMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,76 @@
import Foundation

struct AppMessage: Codable {
enum MessageType: String, Codable {
enum MessageType: Codable, Equatable {
case hang
case functionCall = "function-call"
case functionCall
case transcript
case speechUpdate = "speech-update"
case speechUpdate
case metadata
case conversationUpdate = "conversation-update"
case modelOutput = "model-output"
case statusUpdate = "status-update"
case voiceInput = "voice-input"
case userInterrupted = "user-interrupted"
case conversationUpdate
case modelOutput
case statusUpdate
case voiceInput
case userInterrupted
case assistantStarted
case workflowNodeStarted
case toolCalls
case toolCallsResult
case transferUpdate
case languageChangeDetected
case chatCreated
case chatDeleted
case sessionCreated
case sessionUpdated
case sessionDeleted
case callDeleted
case callDeleteFailed
case unknown(String)

private static let rawValueMapping: [(String, MessageType)] = [
("hang", .hang),
("function-call", .functionCall),
("transcript", .transcript),
("transcript[transcriptType=\"final\"]", .transcript),
("speech-update", .speechUpdate),
("metadata", .metadata),
("conversation-update", .conversationUpdate),
("model-output", .modelOutput),
("status-update", .statusUpdate),
("voice-input", .voiceInput),
("user-interrupted", .userInterrupted),
("assistant.started", .assistantStarted),
("workflow.node.started", .workflowNodeStarted),
("tool-calls", .toolCalls),
("tool-calls-result", .toolCallsResult),
("transfer-update", .transferUpdate),
("language-change-detected", .languageChangeDetected),
("chat.created", .chatCreated),
("chat.deleted", .chatDeleted),
("session.created", .sessionCreated),
("session.updated", .sessionUpdated),
("session.deleted", .sessionDeleted),
("call.deleted", .callDeleted),
("call.delete.failed", .callDeleteFailed),
]

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(String.self)
self = Self.rawValueMapping.first(where: { $0.0 == rawValue })?.1 ?? .unknown(rawValue)
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .unknown(let rawValue):
try container.encode(rawValue)
default:
if let pair = Self.rawValueMapping.first(where: { $0.1 == self }) {
try container.encode(pair.0)
}
}
}
}

let type: MessageType
Expand Down
5 changes: 5 additions & 0 deletions Sources/Models/LanguageChangeDetected.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

public struct LanguageChangeDetected: Codable {
public let language: String
}
16 changes: 16 additions & 0 deletions Sources/Models/ToolCall.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation

public struct ToolCallFunction: Codable {
public let name: String
public let arguments: String
}

public struct ToolCall: Codable {
public let id: String
public let type: String
public let function: ToolCallFunction
}

public struct ToolCallList: Codable {
public let toolCallList: [ToolCall]
}
14 changes: 14 additions & 0 deletions Sources/Models/ToolCallResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

public struct ToolCallResult: Codable {
public let toolCallResult: [String: AnyCodable]

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
toolCallResult = (try? container.decode([String: AnyCodable].self, forKey: .toolCallResult)) ?? [:]
}

private enum CodingKeys: String, CodingKey {
case toolCallResult
}
}
15 changes: 15 additions & 0 deletions Sources/Models/TransferUpdate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

public struct TransferDestination: Codable {
public let type: String?
public let message: String?
public let description: String?
}

public struct TransferUpdate: Codable {
public let destination: TransferDestination?
public let toAssistant: [String: AnyCodable]?
public let fromAssistant: [String: AnyCodable]?
public let toStepRecord: [String: AnyCodable]?
public let fromStepRecord: [String: AnyCodable]?
}
14 changes: 14 additions & 0 deletions Sources/Models/WorkflowNodeStarted.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

public struct WorkflowNodeStarted: Codable {
public let node: [String: AnyCodable]

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
node = (try? container.decode([String: AnyCodable].self, forKey: .node)) ?? [:]
}

private enum CodingKeys: String, CodingKey {
case node
}
}
63 changes: 60 additions & 3 deletions Sources/Vapi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ public final class Vapi: CallClientDelegate {
case voiceInput(VoiceInput)
case hang
case error(Swift.Error)
case assistantStarted([String: Any])
case workflowNodeStarted(WorkflowNodeStarted)
case toolCalls(ToolCallList)
case toolCallsResult(ToolCallResult)
case transferUpdate(TransferUpdate)
case languageChangeDetected(LanguageChangeDetected)
case chatCreated([String: Any])
case chatDeleted([String: Any])
case sessionCreated([String: Any])
case sessionUpdated([String: Any])
case sessionDeleted([String: Any])
case callDeleted
case callDeleteFailed
}

// MARK: - Properties
Expand Down Expand Up @@ -439,10 +452,9 @@ public final class Vapi: CallClientDelegate {
return
}

// Parse the JSON data generically to determine the type of event
let decoder = JSONDecoder()
let appMessage = try decoder.decode(AppMessage.self, from: unescapedData)
// Parse the JSON data again, this time using the specific type

let event: Event
switch appMessage.type {
case .functionCall:
Expand All @@ -462,7 +474,6 @@ public final class Vapi: CallClientDelegate {
throw VapiError.decodingError(message: "App message missing parameters")
}


let functionCall = FunctionCall(name: name, parameters: parameters)
event = Event.functionCall(functionCall)
case .hang:
Expand Down Expand Up @@ -491,6 +502,52 @@ public final class Vapi: CallClientDelegate {
case .voiceInput:
let voiceInput = try decoder.decode(VoiceInput.self, from: unescapedData)
event = Event.voiceInput(voiceInput)
case .assistantStarted:
guard let messageDictionary = try JSONSerialization.jsonObject(with: unescapedData, options: []) as? [String: Any] else {
throw VapiError.decodingError(message: "App message isn't a valid JSON object")
}
event = Event.assistantStarted(messageDictionary)
case .workflowNodeStarted:
let workflowNode = try decoder.decode(WorkflowNodeStarted.self, from: unescapedData)
event = Event.workflowNodeStarted(workflowNode)
case .toolCalls:
let toolCallList = try decoder.decode(ToolCallList.self, from: unescapedData)
event = Event.toolCalls(toolCallList)
case .toolCallsResult:
let toolCallResult = try decoder.decode(ToolCallResult.self, from: unescapedData)
event = Event.toolCallsResult(toolCallResult)
case .transferUpdate:
let transferUpdate = try decoder.decode(TransferUpdate.self, from: unescapedData)
event = Event.transferUpdate(transferUpdate)
case .languageChangeDetected:
let langChange = try decoder.decode(LanguageChangeDetected.self, from: unescapedData)
event = Event.languageChangeDetected(langChange)
case .chatCreated, .chatDeleted:
guard let messageDictionary = try JSONSerialization.jsonObject(with: unescapedData, options: []) as? [String: Any] else {
throw VapiError.decodingError(message: "App message isn't a valid JSON object")
}
event = appMessage.type == .chatCreated
? Event.chatCreated(messageDictionary)
: Event.chatDeleted(messageDictionary)
case .sessionCreated, .sessionUpdated, .sessionDeleted:
guard let messageDictionary = try JSONSerialization.jsonObject(with: unescapedData, options: []) as? [String: Any] else {
throw VapiError.decodingError(message: "App message isn't a valid JSON object")
}
switch appMessage.type {
case .sessionCreated:
event = Event.sessionCreated(messageDictionary)
case .sessionUpdated:
event = Event.sessionUpdated(messageDictionary)
default:
event = Event.sessionDeleted(messageDictionary)
}
case .callDeleted:
event = Event.callDeleted
case .callDeleteFailed:
event = Event.callDeleteFailed
case .unknown(let rawType):
print("Received unhandled message type: \(rawType)")
return
}
eventSubject.send(event)
} catch {
Expand Down