diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index 6fa46146..9ce32958 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -36,7 +36,7 @@ jobs: PicoLLMAppTestUITests/BaseTest.swift - name: Inject Resource URL - run: sed -i '.bak' 's?{TESTING_MODEL_URL_HERE}?http://${{secrets.PV_CICD_RES_SERVER_AUTHORITY}}/github/picollm/res/phi2-290.pllm/latest/phi2-290.pllm?' + run: sed -i '.bak' 's?{TESTING_MODEL_URL_HERE}?http://${{secrets.PV_CICD_RES_SERVER_AUTHORITY}}/github/picollm/res/phi2-290.pllm/03-280e68c/phi2-290.pllm?' PicoLLMAppTestUITests/BaseTest.swift - name: XCode Build diff --git a/binding/ios/PicoLLM.swift b/binding/ios/PicoLLM.swift index 96e5c3b6..32995c78 100644 --- a/binding/ios/PicoLLM.swift +++ b/binding/ios/PicoLLM.swift @@ -9,7 +9,7 @@ import PvPicoLLM -// Usage information. +/// Usage information. public struct PicoLLMUsage: Codable { public let promptTokens: Int @@ -23,11 +23,12 @@ public struct PicoLLMUsage: Codable { } } -// Reasons for ending the generation process. +/// Reasons for ending the generation process. public enum PicoLLMEndpoint: Codable { case endOfSentence case completionTokenLimitReached case stopPhraseEncountered + case interrupted public static func fromC(cEndpoint: pv_picollm_endpoint_t) -> PicoLLMEndpoint? { switch cEndpoint { @@ -37,13 +38,15 @@ public enum PicoLLMEndpoint: Codable { return PicoLLMEndpoint.completionTokenLimitReached case PV_PICOLLM_ENDPOINT_STOP_PHRASE_ENCOUNTERED: return PicoLLMEndpoint.stopPhraseEncountered + case PV_PICOLLM_ENDPOINT_INTERRUPTED: + return PicoLLMEndpoint.interrupted default: return nil } } } -// Generated token and its log probability. +/// Generated token and its log probability. public struct PicoLLMToken: Codable { public let token: String @@ -57,7 +60,7 @@ public struct PicoLLMToken: Codable { } } -// Generated token within completion and top alternative tokens. +/// Generated token within completion and top alternative tokens. public struct PicoLLMCompletionToken: Codable { public let token: PicoLLMToken @@ -71,7 +74,7 @@ public struct PicoLLMCompletionToken: Codable { } } -// Result object containing stats and generated tokens. +/// Result object containing stats and generated tokens. public struct PicoLLMCompletion: Codable { public let usage: PicoLLMUsage @@ -93,7 +96,7 @@ public struct PicoLLMCompletion: Codable { } } -// Private callback for hoisting C callback into Swift callback. +/// Private callback for hoisting C callback into Swift callback. func cStreamCallback (completion: UnsafePointer?, context: UnsafeMutableRawPointer?) { let object = Unmanaged.fromOpaque(context!).takeUnretainedValue() @@ -312,6 +315,20 @@ public class PicoLLM { completion: completion) } + /// Interrupts `pv_picollm_generate()` if generation is in progress. Otherwise, it has no effect. + /// - Throws: PicoLLMError + public func interrupt() throws { + if handle == nil { + throw PicoLLMInvalidStateError("PicoLLM must be initialized before calling interrupt") + } + + let status = pv_picollm_interrupt(self.handle) + if status != PV_STATUS_SUCCESS { + let messageStack = try PicoLLM.getMessageStack() + throw PicoLLM.pvStatusToPicoLLMError(status, "PicoLLM interrupt failed", messageStack) + } + } + /// Tokenizes a given text using the model's tokenizer. /// This is a low-level function meant for benchmarking and advanced usage. /// `.generate()` should be used when possible. @@ -496,7 +513,8 @@ public class PicoLLM { "llama-3-70b-chat": Llama3ChatDialog.self, "mistral-7b-instruct-v0.1": MistralChatDialog.self, "mistral-7b-instruct-v0.2": MistralChatDialog.self, - "mixtral-8x7b-instruct-v0.1": MixtralChatDialog.self + "mixtral-8x7b-instruct-v0.1": MixtralChatDialog.self, + "phi3": Phi3ChatDialog.self ] private static let phi2Dialogs: [String: Phi2Dialog.Type] = [ diff --git a/binding/ios/PicoLLMAppTest/PicoLLMAppTestUITests/PicoLLMAppTestUITests.swift b/binding/ios/PicoLLMAppTest/PicoLLMAppTestUITests/PicoLLMAppTestUITests.swift index 1b584cb3..4efa8d0c 100644 --- a/binding/ios/PicoLLMAppTest/PicoLLMAppTestUITests/PicoLLMAppTestUITests.swift +++ b/binding/ios/PicoLLMAppTest/PicoLLMAppTestUITests/PicoLLMAppTestUITests.swift @@ -345,6 +345,27 @@ class PicoLLMAppTestUITests: BaseTest { XCTAssertEqual(pieces, expected) } + func testInterrupt() throws { + let testCase = PicollmTestCase(name: "default", data: self.picollmTestData!) + + let group = DispatchGroup() + + var res: PicoLLMCompletion? + + group.enter() + DispatchQueue.global(qos: .background).async { + do { + res = try self.handle!.generate(prompt: testCase.prompt) + } catch { } + group.leave() + } + sleep(1) + try handle!.interrupt() + + group.wait() + XCTAssertEqual(res!.endpoint, .interrupted) + } + func testTokenize() throws { let tokenizeData = self.picollmTestData!["tokenize"] as! [String: Any] let text = tokenizeData["text"] as! String @@ -401,10 +422,10 @@ class PicoLLMAppTestUITests: BaseTest { "llama-3-chat-dialog": Llama3ChatDialog.self, "mistral-chat-dialog": MistralChatDialog.self, "phi2-chat-dialog": Phi2ChatDialog.self, - "phi2-qa-dialog": Phi2QADialog.self + "phi2-qa-dialog": Phi2QADialog.self, + "phi3-chat-dialog": Phi3ChatDialog.self ] let dialogPrompts = self.dialogTestData![testName] as! [String: String] - let conversation = self.dialogTestData!["conversation"] as! [[String]] for (dialogClassName, dialogClassType) in dialogClasses { @@ -415,6 +436,7 @@ class PicoLLMAppTestUITests: BaseTest { try dialog.addHumanRequest(content: conversation[i][0]) try dialog.addLLMResponse(content: conversation[i][1]) } + try dialog.addHumanRequest(content: conversation.last![0]) XCTAssertEqual(try dialog.prompt(), prompt) } diff --git a/binding/ios/PicoLLMAppTest/Podfile b/binding/ios/PicoLLMAppTest/Podfile index 81b177d0..e603e7ef 100644 --- a/binding/ios/PicoLLMAppTest/Podfile +++ b/binding/ios/PicoLLMAppTest/Podfile @@ -2,9 +2,9 @@ source 'https://cdn.cocoapods.org/' platform :ios, '16.0' target 'PicoLLMAppTest' do - pod 'picoLLM-iOS', '~> 1.0.0' + pod 'picoLLM-iOS', :podspec => 'https://raw.githubusercontent.com/Picovoice/picollm/v1.1-ios/binding/ios/picoLLM-iOS.podspec' end target 'PicoLLMAppTestUITests' do - pod 'picoLLM-iOS', '~> 1.0.0' + pod 'picoLLM-iOS', :podspec => 'https://raw.githubusercontent.com/Picovoice/picollm/v1.1-ios/binding/ios/picoLLM-iOS.podspec' end diff --git a/binding/ios/PicoLLMAppTest/Podfile.lock b/binding/ios/PicoLLMAppTest/Podfile.lock index 3a9d0583..188da1e9 100644 --- a/binding/ios/PicoLLMAppTest/Podfile.lock +++ b/binding/ios/PicoLLMAppTest/Podfile.lock @@ -1,16 +1,16 @@ PODS: - - picoLLM-iOS (1.0.0) + - picoLLM-iOS (1.1.0) DEPENDENCIES: - - picoLLM-iOS (~> 1.0.0) + - picoLLM-iOS (from `https://raw.githubusercontent.com/Picovoice/picollm/v1.1-ios/binding/ios/picoLLM-iOS.podspec`) -SPEC REPOS: - trunk: - - picoLLM-iOS +EXTERNAL SOURCES: + picoLLM-iOS: + :podspec: https://raw.githubusercontent.com/Picovoice/picollm/v1.1-ios/binding/ios/picoLLM-iOS.podspec SPEC CHECKSUMS: - picoLLM-iOS: 02cdb501b4beb74a9c1dea29d5cf461d65ea4a6c + picoLLM-iOS: dc03cd7e992c702ff34c667f9a35dd9a8084c061 -PODFILE CHECKSUM: 5223638a26efe0676b29af87115eb47f41185dc4 +PODFILE CHECKSUM: 8557d5c1b8e4eb632f8926bbc8eeb29802224b3d -COCOAPODS: 1.15.2 +COCOAPODS: 1.11.3 diff --git a/binding/ios/PicoLLMDialog.swift b/binding/ios/PicoLLMDialog.swift index 7e19fc94..109b696b 100644 --- a/binding/ios/PicoLLMDialog.swift +++ b/binding/ios/PicoLLMDialog.swift @@ -160,6 +160,47 @@ public class Phi2ChatDialog: Phi2Dialog { } } +/// Dialog helper for `phi3`. +public class Phi3ChatDialog: BasePicoLLMDialog { + public override func prompt() throws -> String { + if self.humanRequests.count == self.llmResponses.count { + throw PicoLLMInvalidStateError("Only subclasses of PicoLLMDialog can return create prompts.") + } + + let humanRequests = (self.history == nil) ? + self.humanRequests[...] : + self.humanRequests[(self.humanRequests.count - Int(self.history!) - 1)...] + let llmResponses = (self.history == nil) ? + self.llmResponses[...] : + self.llmResponses[(self.llmResponses.count - Int(self.history!))...] + + var res = "" + if system != nil { + res += String(format: "<|system|>\n%@<|end|>\n", system!) + } + for i in 0..\n%@<|end|>\n", + humanRequests[i].trimmingCharacters(in: .whitespacesAndNewlines) + ) + res += String( + format: "<|assistant|>\n%@<|end|>\n", + llmResponses[i].trimmingCharacters(in: .whitespacesAndNewlines) + ) + } + + res += String( + format: "<|user|>\n%@<|end|>\n", + humanRequests.last!.trimmingCharacters(in: .whitespacesAndNewlines) + ) + res += String( + format: "<|assistant|>\n" + ) + + return res + } +} + /// Dialog helper for `mistral-7b-instruct-v0.1` and `mistral-7b-instruct-v0.2`. public class MistralChatDialog: BasePicoLLMDialog { public override func prompt() throws -> String { diff --git a/binding/ios/picoLLM-iOS.podspec b/binding/ios/picoLLM-iOS.podspec index bee6392e..f17d0a87 100644 --- a/binding/ios/picoLLM-iOS.podspec +++ b/binding/ios/picoLLM-iOS.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'picoLLM-iOS' s.module_name = 'PicoLLM' - s.version = '1.0.0' + s.version = '1.1.0' s.license = {:type => 'Apache 2.0'} s.summary = 'picoLLM Inference Engine' s.description = @@ -10,7 +10,7 @@ Pod::Spec.new do |s| DESC s.homepage = 'https://github.com/Picovoice/picollm/tree/master/binding/ios' s.author = { 'Picovoice' => 'hello@picovoice.ai' } - s.source = { :git => "https://github.com/Picovoice/picollm.git", :tag => "picoLLM-iOS-v1.0.0" } + s.source = { :git => "https://github.com/Picovoice/picollm.git", :tag => "picoLLM-iOS-v1.1.0" } s.ios.deployment_target = '16.0' s.swift_version = '5.0' s.vendored_frameworks = 'lib/ios/PvPicoLLM.xcframework' diff --git a/demo/ios/Chat/PicoLLMChatDemo/ChatView.swift b/demo/ios/Chat/PicoLLMChatDemo/ChatView.swift index 36c9644b..b5673914 100644 --- a/demo/ios/Chat/PicoLLMChatDemo/ChatView.swift +++ b/demo/ios/Chat/PicoLLMChatDemo/ChatView.swift @@ -26,7 +26,7 @@ struct ChatView: View { Spacer() HStack(alignment: .center) { - if !viewModel.enableGenerateButton { + if viewModel.isGenerating { ProgressView(value: 0).progressViewStyle(CircularProgressViewStyle()) Text("Generating...") .padding(.horizontal, 12) @@ -53,20 +53,20 @@ struct ChatView: View { .onSubmit { viewModel.generate() } - .disabled(isError || !viewModel.enableGenerateButton) + .disabled(isError || viewModel.isGenerating) HStack(alignment: .center) { Spacer() - Button(action: viewModel.generate) { - Image(systemName: "arrow.up") + Button(action: viewModel.isGenerating ? viewModel.interrupt : viewModel.generate) { + Image(systemName: viewModel.isGenerating ? "stop.fill" : "arrow.up") .imageScale(.medium) - .background(Constants.btnColor(viewModel.enableGenerateButton && !isError)) + .background(Constants.btnColor(!isError)) .foregroundColor(.white) .padding(6) }.background( - Capsule().fill(Constants.btnColor(viewModel.enableGenerateButton && !isError)) + Capsule().fill(Constants.btnColor(!isError)) ) .padding(.horizontal, 4) - .disabled(isError || !viewModel.enableGenerateButton) + .disabled(isError) } } .padding(.vertical, 12) @@ -111,7 +111,7 @@ struct ChatView: View { .imageScale(.large) } .padding(.horizontal, 12) - .disabled(!viewModel.enableGenerateButton) + .disabled(viewModel.isGenerating) Spacer() Button(action: viewModel.clearText) { Image(systemName: "arrow.counterclockwise") @@ -120,7 +120,7 @@ struct ChatView: View { .padding(.horizontal, 12) .disabled( viewModel.errorMessage.count > 0 || - !viewModel.enableGenerateButton || + viewModel.isGenerating || viewModel.chatText.isEmpty) } .padding(.bottom, 12) diff --git a/demo/ios/Chat/PicoLLMChatDemo/ViewModel.swift b/demo/ios/Chat/PicoLLMChatDemo/ViewModel.swift index 15261995..91463943 100644 --- a/demo/ios/Chat/PicoLLMChatDemo/ViewModel.swift +++ b/demo/ios/Chat/PicoLLMChatDemo/ViewModel.swift @@ -34,7 +34,7 @@ You can download directly to your device or airdrop from a Mac. @Published var picoLLMLoaded = false @Published var promptText = "" - @Published var enableGenerateButton = true + @Published var isGenerating = false @Published var chatText: [Message] = [] @@ -116,7 +116,7 @@ You can download directly to your device or airdrop from a Mac. errorMessage = "" - enableGenerateButton = false + isGenerating = true numTokens = 0 DispatchQueue.global(qos: .userInitiated).async { [self] in @@ -143,7 +143,18 @@ You can download directly to your device or airdrop from a Mac. DispatchQueue.main.async { [self] in promptText = "" - enableGenerateButton = true + isGenerating = false + } + } + } + + public func interrupt() { + do { + try picollm?.interrupt() + } catch { + DispatchQueue.main.async { [self] in + errorMessage = "\(error.localizedDescription)" + enableLoadModelButton = true } } } diff --git a/demo/ios/Chat/Podfile b/demo/ios/Chat/Podfile index c56bc89a..07bd75e0 100644 --- a/demo/ios/Chat/Podfile +++ b/demo/ios/Chat/Podfile @@ -2,5 +2,5 @@ source 'https://cdn.cocoapods.org/' platform :ios, '16.0' target 'PicoLLMChatDemo' do - pod 'picoLLM-iOS', '~> 1.0.0' + pod 'picoLLM-iOS', :podspec => 'https://raw.githubusercontent.com/Picovoice/picollm/v1.1-ios/binding/ios/picoLLM-iOS.podspec' end diff --git a/demo/ios/Chat/Podfile.lock b/demo/ios/Chat/Podfile.lock index 0b22387e..83aec644 100644 --- a/demo/ios/Chat/Podfile.lock +++ b/demo/ios/Chat/Podfile.lock @@ -1,16 +1,16 @@ PODS: - - picoLLM-iOS (1.0.0) + - picoLLM-iOS (1.1.0) DEPENDENCIES: - - picoLLM-iOS (~> 1.0.0) + - picoLLM-iOS (from `https://raw.githubusercontent.com/Picovoice/picollm/v1.1-ios/binding/ios/picoLLM-iOS.podspec`) -SPEC REPOS: - trunk: - - picoLLM-iOS +EXTERNAL SOURCES: + picoLLM-iOS: + :podspec: https://raw.githubusercontent.com/Picovoice/picollm/v1.1-ios/binding/ios/picoLLM-iOS.podspec SPEC CHECKSUMS: - picoLLM-iOS: 02cdb501b4beb74a9c1dea29d5cf461d65ea4a6c + picoLLM-iOS: dc03cd7e992c702ff34c667f9a35dd9a8084c061 -PODFILE CHECKSUM: 5ceffe351e8a95d803e0000324674ffa525cbb43 +PODFILE CHECKSUM: 51304d5689a6025865f0d87106b6efef0b049eb8 -COCOAPODS: 1.15.2 +COCOAPODS: 1.11.3 diff --git a/demo/ios/Completion/PicoLLMCompletionDemo/CompletionView.swift b/demo/ios/Completion/PicoLLMCompletionDemo/CompletionView.swift index 904dc4c6..16485303 100644 --- a/demo/ios/Completion/PicoLLMCompletionDemo/CompletionView.swift +++ b/demo/ios/Completion/PicoLLMCompletionDemo/CompletionView.swift @@ -28,7 +28,7 @@ struct CompletionView: View { .padding(.horizontal, 12) } ) - .disabled(!viewModel.enableGenerateButton) + .disabled(viewModel.isGenerating) Spacer() Text("picoLLM Completion Demo") Spacer() @@ -41,7 +41,7 @@ struct CompletionView: View { Spacer() HStack(alignment: .center) { - if !viewModel.enableGenerateButton { + if viewModel.isGenerating { ProgressView(value: 0).progressViewStyle(CircularProgressViewStyle()) Text("Generating...") .padding(.horizontal, 12) @@ -72,20 +72,19 @@ struct CompletionView: View { .onSubmit { viewModel.generate() } - .disabled(!viewModel.enableGenerateButton) + .disabled(viewModel.isGenerating) HStack(alignment: .center) { Spacer() - Button(action: viewModel.generate) { - Image(systemName: "arrow.up") + Button(action: viewModel.isGenerating ? viewModel.interrupt : viewModel.generate) { + Image(systemName: viewModel.isGenerating ? "stop.fill" : "arrow.up") .imageScale(.medium) - .background(Constants.btnColor(viewModel.enableGenerateButton)) + .background(Constants.activeBlue) .foregroundColor(.white) .padding(6) }.background( - Capsule().fill(Constants.btnColor(viewModel.enableGenerateButton)) + Capsule().fill(Constants.activeBlue) ) .padding(.horizontal, 4) - .disabled(!viewModel.enableGenerateButton) } } .padding(.vertical, 12) @@ -102,7 +101,7 @@ struct CompletionView: View { VStack { ScrollViewReader { proxy in ScrollView { - if showStats && viewModel.enableGenerateButton { + if showStats && !viewModel.isGenerating { Text(viewModel.statsText) .padding(12) } else { @@ -128,14 +127,14 @@ struct CompletionView: View { .imageScale(.large) } .padding(.horizontal, 12) - .disabled(!viewModel.enableGenerateButton) + .disabled(viewModel.isGenerating) Spacer() Button( action: {() in showStats = !showStats}, label: { Image(systemName: "chevron.left") .imageScale(.small) - if showStats && viewModel.enableGenerateButton { + if showStats && !viewModel.isGenerating { Text("/") } else { Text(" ") @@ -147,7 +146,7 @@ struct CompletionView: View { .padding(.horizontal, 12) .disabled( viewModel.errorMessage.count > 0 || - !viewModel.enableGenerateButton || + viewModel.isGenerating || viewModel.completionText.isEmpty) } .padding(.bottom, 12) diff --git a/demo/ios/Completion/PicoLLMCompletionDemo/ViewModel.swift b/demo/ios/Completion/PicoLLMCompletionDemo/ViewModel.swift index dd10a701..2261bada 100644 --- a/demo/ios/Completion/PicoLLMCompletionDemo/ViewModel.swift +++ b/demo/ios/Completion/PicoLLMCompletionDemo/ViewModel.swift @@ -40,7 +40,7 @@ You can download directly to your device or airdrop from a Mac. @Published var generatePresencePenalty = 0.0 @Published var generateFrequencyPenalty = 0.0 @Published var generateNumTopChoices = 0.0 - @Published var enableGenerateButton = true + @Published var isGenerating = false @Published var completionPromptText = "" @Published var completionText = "" @@ -140,7 +140,7 @@ You can download directly to your device or airdrop from a Mac. return } - enableGenerateButton = false + isGenerating = true completionPromptText = promptText completionText = "" tpsText = "" @@ -171,7 +171,18 @@ You can download directly to your device or airdrop from a Mac. DispatchQueue.main.async { [self] in promptText = "" - enableGenerateButton = true + isGenerating = false + } + } + } + + public func interrupt() { + do { + try picollm?.interrupt() + } catch { + DispatchQueue.main.async { [self] in + errorMessage = "\(error.localizedDescription)" + enableLoadModelButton = true } } } diff --git a/demo/ios/Completion/Podfile b/demo/ios/Completion/Podfile index c233ec56..5c71c4f5 100644 --- a/demo/ios/Completion/Podfile +++ b/demo/ios/Completion/Podfile @@ -2,5 +2,5 @@ source 'https://cdn.cocoapods.org/' platform :ios, '16.0' target 'PicoLLMCompletionDemo' do - pod 'picoLLM-iOS', '~> 1.0.0' + pod 'picoLLM-iOS', :podspec => 'https://raw.githubusercontent.com/Picovoice/picollm/v1.1-ios/binding/ios/picoLLM-iOS.podspec' end diff --git a/demo/ios/Completion/Podfile.lock b/demo/ios/Completion/Podfile.lock index 91961390..62c1e7d2 100644 --- a/demo/ios/Completion/Podfile.lock +++ b/demo/ios/Completion/Podfile.lock @@ -1,16 +1,16 @@ PODS: - - picoLLM-iOS (1.0.0) + - picoLLM-iOS (1.1.0) DEPENDENCIES: - - picoLLM-iOS (~> 1.0.0) + - picoLLM-iOS (from `https://raw.githubusercontent.com/Picovoice/picollm/v1.1-ios/binding/ios/picoLLM-iOS.podspec`) -SPEC REPOS: - trunk: - - picoLLM-iOS +EXTERNAL SOURCES: + picoLLM-iOS: + :podspec: https://raw.githubusercontent.com/Picovoice/picollm/v1.1-ios/binding/ios/picoLLM-iOS.podspec SPEC CHECKSUMS: - picoLLM-iOS: 02cdb501b4beb74a9c1dea29d5cf461d65ea4a6c + picoLLM-iOS: dc03cd7e992c702ff34c667f9a35dd9a8084c061 -PODFILE CHECKSUM: 0f0318dbd90bfa46ae09b74d92d7e21912583c46 +PODFILE CHECKSUM: d5764505a13214074d2e9db975f4c26fd5c720be -COCOAPODS: 1.15.2 +COCOAPODS: 1.11.3 diff --git a/resources/.lint/spell-check/dict.txt b/resources/.lint/spell-check/dict.txt index be899255..c0d435ad 100644 --- a/resources/.lint/spell-check/dict.txt +++ b/resources/.lint/spell-check/dict.txt @@ -35,6 +35,7 @@ picollmoutofmemoryerror picovoice pllm pluginutils +podspec readables rtld salut