From 4d9eb5a617bf5ae3b4804bdfc6ce401c666c2e40 Mon Sep 17 00:00:00 2001 From: Tisfeng Date: Sat, 25 May 2024 20:29:03 +0800 Subject: [PATCH] refactor: add a base class LLMStreamService (#561) * refactor: add a base class LLMStreamService * refactor: change GeminiService to inherit from LLMStreamService * perf: remove #available macos-12 in Gemini * chore: update swift lint * perf: remove unused swiftlint:disable * perf: improve structure between LLMStreamService and BaseOpenAIService * perf: make subclass must override properties availableModels, apiKey and endpoint * perf: mark model as must be overridden --- .swiftlint.yml | 5 +- Easydict.xcodeproj/project.pbxproj | 4 + Easydict/Swift/Service/Ali/AliResponse.swift | 5 +- Easydict/Swift/Service/Ali/AliService.swift | 5 +- .../Swift/Service/Ali/AliTranslateType.swift | 4 - .../Service/BuiltInAI/BuiltInAIService.swift | 1 - .../Swift/Service/Caiyun/CaiyunResponse.swift | 3 - .../Swift/Service/Caiyun/CaiyunService.swift | 4 - .../Service/Caiyun/CaiyunTranslateType.swift | 4 - .../CustomOpenAI/CustomOpenAIService.swift | 3 - .../Swift/Service/Gemini/GeminiService.swift | 126 ++++++-------- .../Service/OpenAI/BaseOpenAIService.swift | 158 ++++++------------ .../Service/OpenAI/LLMStreamService.swift | 133 +++++++++++++++ .../Swift/Service/OpenAI/OpenAIService.swift | 4 - Easydict/Swift/Service/OpenAI/Prompt.swift | 62 +------ .../Service/Tencent/TencentService.swift | 3 - .../Service/Tencent/TencentSigning.swift | 5 +- .../Tencent/TencentTranslateType.swift | 1 - .../Extensions/String/String+Extension.swift | 2 +- .../SettingView/Tabs/TabView/GeneralTab.swift | 1 - EasydictSwiftTests/EasydictSwiftTests.swift | 4 - 21 files changed, 251 insertions(+), 286 deletions(-) create mode 100644 Easydict/Swift/Service/OpenAI/LLMStreamService.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index e245f0ba2..2259dc9d1 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -24,6 +24,7 @@ disabled_rules: - force_try - large_tuple - todo + - no_fallthrough_only opt_in_rules: - convenience_type @@ -33,6 +34,7 @@ opt_in_rules: line_length: warning: 120 ignores_comments: true + ignores_interpolated_strings: true function_body_length: warning: 120 error: 400 @@ -48,7 +50,7 @@ type_name: warning: 50 error: 50 identifier_name: - min_length: 3 + min_length: 2 excluded: # excluded via string array - id - URL @@ -58,6 +60,7 @@ identifier_name: - i - j - Defaults # Make use of `SwiftyUserDefaults` + - to reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji) trailing_comma: severity: warning diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 424012ffb..57581ea8d 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -86,6 +86,7 @@ 0383914F292FBE120009828C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03839145292FBE120009828C /* Main.storyboard */; }; 03839150292FBE120009828C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 03839147292FBE120009828C /* main.m */; }; 03839151292FBE120009828C /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 03839148292FBE120009828C /* AppDelegate.m */; }; + 0387FB7A2BFBA990000A7A82 /* LLMStreamService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0387FB792BFBA990000A7A82 /* LLMStreamService.swift */; }; 03882F8D29D95044005B5A52 /* CTView.m in Sources */ = {isa = PBXBuildFile; fileRef = 03882F8429D95044005B5A52 /* CTView.m */; }; 03882F8E29D95044005B5A52 /* ToastWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 03882F8629D95044005B5A52 /* ToastWindowController.m */; }; 03882F8F29D95044005B5A52 /* CTScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 03882F8729D95044005B5A52 /* CTScreen.m */; }; @@ -492,6 +493,7 @@ 03839149292FBE120009828C /* EasydictHelper.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = EasydictHelper.entitlements; sourceTree = ""; }; 0383914A292FBE120009828C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0383914B292FBE120009828C /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 0387FB792BFBA990000A7A82 /* LLMStreamService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMStreamService.swift; sourceTree = ""; }; 03882F8229D95044005B5A52 /* CTScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTScreen.h; sourceTree = ""; }; 03882F8329D95044005B5A52 /* ToastWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ToastWindowController.h; sourceTree = ""; }; 03882F8429D95044005B5A52 /* CTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTView.m; sourceTree = ""; }; @@ -1270,6 +1272,7 @@ 03779F0D2BB256A7008D3C42 /* OpenAI */ = { isa = PBXGroup; children = ( + 0387FB792BFBA990000A7A82 /* LLMStreamService.swift */, 0396DE542BB5844A009FD2A5 /* BaseOpenAIService.swift */, 03779F0B2BB256A7008D3C42 /* OpenAIService.swift */, 03779F0C2BB256A7008D3C42 /* Prompt.swift */, @@ -3192,6 +3195,7 @@ 0309E1ED292B439A00AFB76A /* EZTextView.m in Sources */, 03B0232B29231FA6001C7E63 /* NSMutableAttributedString+MM.m in Sources */, 03B022E829231FA6001C7E63 /* entry.m in Sources */, + 0387FB7A2BFBA990000A7A82 /* LLMStreamService.swift in Sources */, 039F5504294B6E29004AB940 /* EZPreferencesWindowController.m in Sources */, 03008B3F29444B0A0062B821 /* NSView+EZAnimatedHidden.m in Sources */, 03B022FD29231FA6001C7E63 /* EZFixedQueryWindow.m in Sources */, diff --git a/Easydict/Swift/Service/Ali/AliResponse.swift b/Easydict/Swift/Service/Ali/AliResponse.swift index 2683b649d..fe4d22941 100644 --- a/Easydict/Swift/Service/Ali/AliResponse.swift +++ b/Easydict/Swift/Service/Ali/AliResponse.swift @@ -140,9 +140,8 @@ enum AnyCodable: Codable { switch self { case let .int(i): String(i) - // swiftlint:disable:next identifier_name - case let .string(s): - s + case let .string(str): + str } } diff --git a/Easydict/Swift/Service/Ali/AliService.swift b/Easydict/Swift/Service/Ali/AliService.swift index 8cba69b3f..f2d59af25 100644 --- a/Easydict/Swift/Service/Ali/AliService.swift +++ b/Easydict/Swift/Service/Ali/AliService.swift @@ -6,8 +6,6 @@ // Copyright © 2023 izual. All rights reserved. // -// swiftlint:disable all - import Alamofire import CryptoKit import Defaults @@ -140,6 +138,7 @@ class AliService: QueryService { } } + // swiftlint:disable:next function_parameter_count private func requestByAPI( id: String, secret: String, @@ -346,5 +345,3 @@ class AliService: QueryService { }, serviceType: serviceType().rawValue) } } - -// swiftlint:enable all diff --git a/Easydict/Swift/Service/Ali/AliTranslateType.swift b/Easydict/Swift/Service/Ali/AliTranslateType.swift index 045d0a7fe..dc9c8255d 100644 --- a/Easydict/Swift/Service/Ali/AliTranslateType.swift +++ b/Easydict/Swift/Service/Ali/AliTranslateType.swift @@ -6,8 +6,6 @@ // Copyright © 2023 izual. All rights reserved. // -// swiftlint:disable all - import Foundation struct AliTranslateType: Equatable { @@ -103,5 +101,3 @@ struct AliTranslateType: Equatable { return AliTranslateType(sourceLanguage: fromLanguage, targetLanguage: toLanguage) } } - -// swiftlint:enable all diff --git a/Easydict/Swift/Service/BuiltInAI/BuiltInAIService.swift b/Easydict/Swift/Service/BuiltInAI/BuiltInAIService.swift index 11a761205..dbbb58cb6 100644 --- a/Easydict/Swift/Service/BuiltInAI/BuiltInAIService.swift +++ b/Easydict/Swift/Service/BuiltInAI/BuiltInAIService.swift @@ -39,7 +39,6 @@ class BuiltInAIService: BaseOpenAIService { } return model } - set { Defaults[.builtInAIModel] = newValue } diff --git a/Easydict/Swift/Service/Caiyun/CaiyunResponse.swift b/Easydict/Swift/Service/Caiyun/CaiyunResponse.swift index 9e5ef6d42..9225955b4 100644 --- a/Easydict/Swift/Service/Caiyun/CaiyunResponse.swift +++ b/Easydict/Swift/Service/Caiyun/CaiyunResponse.swift @@ -1,4 +1,3 @@ -// swiftlint:disable all // // CaiyunResponse.swift // Easydict @@ -14,5 +13,3 @@ struct CaiyunResponse: Codable { var rc: Int var target: [String] } - -// swiftlint:enable all diff --git a/Easydict/Swift/Service/Caiyun/CaiyunService.swift b/Easydict/Swift/Service/Caiyun/CaiyunService.swift index b036b72da..ee4d20149 100644 --- a/Easydict/Swift/Service/Caiyun/CaiyunService.swift +++ b/Easydict/Swift/Service/Caiyun/CaiyunService.swift @@ -6,8 +6,6 @@ // Copyright © 2023 izual. All rights reserved. // -// swiftlint:disable all - import Alamofire import Defaults import Foundation @@ -130,5 +128,3 @@ public final class CaiyunService: QueryService { enum QueryServiceError: Error { case notSupported } - -// swiftlint:enable all diff --git a/Easydict/Swift/Service/Caiyun/CaiyunTranslateType.swift b/Easydict/Swift/Service/Caiyun/CaiyunTranslateType.swift index 76bddf1e3..05698b33d 100644 --- a/Easydict/Swift/Service/Caiyun/CaiyunTranslateType.swift +++ b/Easydict/Swift/Service/Caiyun/CaiyunTranslateType.swift @@ -6,8 +6,6 @@ // Copyright © 2023 izual. All rights reserved. // -// swiftlint:disable all - import Foundation struct CaiyunTranslateType: RawRepresentable { @@ -60,5 +58,3 @@ struct CaiyunTranslateType: RawRepresentable { return CaiyunTranslateType(rawValue: "\(from)2\(to)") } } - -// swiftlint:enable all diff --git a/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift b/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift index ed0c83a4d..df44e5bd8 100644 --- a/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift +++ b/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift @@ -6,8 +6,6 @@ // Copyright © 2024 izual. All rights reserved. // -import Alamofire -import CryptoKit import Defaults import Foundation @@ -41,7 +39,6 @@ class CustomOpenAIService: BaseOpenAIService { get { Defaults[.customOpenAIModel] } - set { Defaults[.customOpenAIModel] = newValue } diff --git a/Easydict/Swift/Service/Gemini/GeminiService.swift b/Easydict/Swift/Service/Gemini/GeminiService.swift index 26b19e032..85c07e3f8 100644 --- a/Easydict/Swift/Service/Gemini/GeminiService.swift +++ b/Easydict/Swift/Service/Gemini/GeminiService.swift @@ -6,15 +6,13 @@ // Copyright © 2024 izual. All rights reserved. // -// swiftlint:disable all - import Defaults import Foundation import GoogleGenerativeAI // TODO: add a LLM stream service base class, make both OpenAI and Gemini inherit from it. @objc(EZGeminiService) -public final class GeminiService: QueryService { +public final class GeminiService: LLMStreamService { // MARK: Public override public func serviceType() -> ServiceType { @@ -29,21 +27,8 @@ public final class GeminiService: QueryService { NSLocalizedString("gemini_translate", comment: "The name of Gemini Translate") } - override public func supportLanguagesDictionary() -> MMOrderedDictionary { - // TODO: Replace MMOrderedDictionary. - let orderedDict = MMOrderedDictionary() - for language in EZLanguageManager.shared().allLanguages { - let value = language.rawValue - if !GeminiService.unsupportedLanguages.contains(language) { - orderedDict.setObject(value as NSString, forKey: language.rawValue as NSString) - } - } - - return orderedDict - } - - public override func isStream() -> Bool { - true + override public func queryTextType() -> EZQueryTextType { + [.translation] } override public func translate( @@ -55,54 +40,45 @@ public final class GeminiService: QueryService { Task { do { let translationPrompt = translationPrompt(text: text, from: from, to: to) - let prompt = QueryService.translationSystemPrompt + + let prompt = LLMStreamService.translationSystemPrompt + "\n" + translationPrompt -// logInfo("gemini prompt: \(prompt)") let model = GenerativeModel( name: "gemini-pro", apiKey: apiKey, safetySettings: [ - GeminiService.harassmentSafety, - GeminiService.hateSpeechSafety, - GeminiService.sexuallyExplicitSafety, - GeminiService.dangerousContentSafety, + harassmentBlockNone, + hateSpeechBlockNone, + sexuallyExplicitBlockNone, + dangerousContentBlockNone, ] ) + result.isStreamFinished = false + + var resultString = "" + // Gemini Docs: https://github.com/google/generative-ai-swift - if #available(macOS 12.0, *) { - result.isStreamFinished = false - var resultString = "" - let outputContentStream = model.generateContentStream(prompt) + let outputContentStream = model.generateContentStream(prompt) + for try await outputContent in outputContentStream { + guard let line = outputContent.text else { + return + } + if !result.isStreamFinished { + resultString += line - for try await outputContent in outputContentStream { - guard let line = outputContent.text else { - return - } - if !result.isStreamFinished { - resultString += line - result.translatedResults = [resultString] - await MainActor.run { - throttler.throttle { [unowned self] in - completion(result, nil) - } + result.translatedResults = [resultString] + await MainActor.run { + throttler.throttle { [unowned self] in + completion(result, nil) } } } - result.isStreamFinished = true - completion(result, nil) - } else { - // Gemini does not support stream in macOS 12.0- - let outputContent = try await model.generateContent(prompt) - guard let resultString = outputContent.text else { - return - } - result.translatedResults = [resultString] - await MainActor.run { - completion(result, nil) - } } + + result.isStreamFinished = true + result.translatedResults = [getFinalResultText(text: resultString)] + completion(result, nil) } catch { /** https://github.com/google/generative-ai-swift/issues/89 @@ -126,34 +102,32 @@ public final class GeminiService: QueryService { // MARK: Internal - let throttler = Throttler() - - // MARK: Private - // https://ai.google.dev/available_regions - private static let unsupportedLanguages: [Language] = [ - .persian, - .filipino, - .khmer, - .lao, - .malay, - .mongolian, - .burmese, - .telugu, - .tamil, - .urdu, - ] - - // Set Gemini safety level to BLOCK_NONE - private static let harassmentSafety = SafetySetting(harmCategory: .harassment, threshold: .blockNone) - private static let hateSpeechSafety = SafetySetting(harmCategory: .hateSpeech, threshold: .blockNone) - private static let sexuallyExplicitSafety = SafetySetting(harmCategory: .sexuallyExplicit, threshold: .blockNone) - private static let dangerousContentSafety = SafetySetting(harmCategory: .dangerousContent, threshold: .blockNone) + override var unsupportedLanguages: [Language] { + [ + .persian, + .filipino, + .khmer, + .lao, + .malay, + .mongolian, + .burmese, + .telugu, + .tamil, + .urdu, + ] + } // easydict://writeKeyValue?EZGeminiAPIKey=xxx - private var apiKey: String { + override var apiKey: String { Defaults[.geminiAPIKey] ?? "" } -} -// swiftlint:enable all + // MARK: Private + + // Set Gemini safety level to BLOCK_NONE + private let harassmentBlockNone = SafetySetting(harmCategory: .harassment, threshold: .blockNone) + private let hateSpeechBlockNone = SafetySetting(harmCategory: .hateSpeech, threshold: .blockNone) + private let sexuallyExplicitBlockNone = SafetySetting(harmCategory: .sexuallyExplicit, threshold: .blockNone) + private let dangerousContentBlockNone = SafetySetting(harmCategory: .dangerousContent, threshold: .blockNone) +} diff --git a/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift b/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift index f73c11914..25dfc2dff 100644 --- a/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift +++ b/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift @@ -6,7 +6,6 @@ // Copyright © 2024 izual. All rights reserved. // -import Defaults import Foundation import OpenAI @@ -16,57 +15,9 @@ import OpenAI @objcMembers @objc(EZBaseOpenAIService) -public class BaseOpenAIService: QueryService { +public class BaseOpenAIService: LLMStreamService { // MARK: Public - override public func isStream() -> Bool { - true - } - - override public func intelligentQueryTextType() -> EZQueryTextType { - Configuration.shared.intelligentQueryTextTypeForServiceType(serviceType()) - } - - override public func supportLanguagesDictionary() -> MMOrderedDictionary { - let allLangauges = EZLanguageManager.shared().allLanguages - let supportedLanguages = allLangauges.filter { language in - !unsupportedLanguages.contains(language) - } - - let orderedDict = MMOrderedDictionary() - for language in supportedLanguages { - orderedDict.setObject(language.rawValue as NSString, forKey: language.rawValue as NSString) - } - return orderedDict - } - - override public func queryTextType() -> EZQueryTextType { - var typeOptions: EZQueryTextType = [] - - let isTranslationEnabled = UserDefaults.bool(forKey: EZTranslationKey, serviceType: serviceType()) - let isSentenceEnabled = UserDefaults.bool(forKey: EZSentenceKey, serviceType: serviceType()) - let isDictionaryEnabled = UserDefaults.bool(forKey: EZDictionaryKey, serviceType: serviceType()) - - if isTranslationEnabled { - typeOptions.insert(.translation) - } - if isSentenceEnabled { - typeOptions.insert(.sentence) - } - if isDictionaryEnabled { - typeOptions.insert(.dictionary) - } - - return typeOptions - } - - override public func serviceUsageStatus() -> EZServiceUsageStatus { - let usageStatus = UserDefaults.string(forKey: EZServiceUsageStatusKey, serviceType: serviceType()) ?? "" - guard let value = UInt(usageStatus) else { return .default } - return EZServiceUsageStatus(rawValue: value) ?? .default - } - - // swiftlint:disable identifier_name override public func translate( _ text: String, from: Language, @@ -133,7 +84,6 @@ public class BaseOpenAIService: QueryService { // If already has error, we do not need to update it. if result.error == nil { resultText = getFinalResultText(text: resultText) - // log("\(name())-(\(model)): \(resultText)") handleResult(queryType: queryType, resultText: resultText, error: nil, completion: completion) result.isStreamFinished = true @@ -143,57 +93,12 @@ public class BaseOpenAIService: QueryService { } } - // swiftlint:enable identifier_name - // MARK: Internal - let throttler = Throttler() var updateCompletion: ((EZQueryResult, Error?) -> ())? - var model = "" - - var unsupportedLanguages: [Language] = [] - - var availableModels: [String] { - [""] - } - - var apiKey: String { - "" - } - - var endpoint: String { - "" - } - // MARK: Private - /// Get query type by text and from && to langauge. - private func queryType(text: String, from: Language, to _: Language) -> EZQueryTextType { - let enableDictionary = queryTextType().contains(.dictionary) - var isQueryDictionary = false - if enableDictionary { - isQueryDictionary = (text as NSString).shouldQueryDictionary(withLanguage: from, maxWordCount: 2) - if isQueryDictionary { - return .dictionary - } - } - - let enableSentence = queryTextType().contains(.sentence) - var isQueryEnglishSentence = false - if !isQueryDictionary, enableSentence { - let isEnglishText = from == .english - if isEnglishText { - isQueryEnglishSentence = (text as NSString).shouldQuerySentence(withLanguage: from) - if isQueryEnglishSentence { - return .sentence - } - } - } - - return .translation - } - private func handleResult( queryType: EZQueryTextType, resultText: String?, @@ -235,20 +140,61 @@ public class BaseOpenAIService: QueryService { updateCompletion() } } +} - private func getFinalResultText(text: String) -> String { - var resultText = text.trim() +// MARK: OpenAI chat messages - // Remove last , fix Groq model mixtral-8x7b-32768 - let stopFlag = "" - if !queryModel.queryText.hasSuffix(stopFlag), resultText.hasSuffix(stopFlag) { - resultText = String(resultText.dropLast(stopFlag.count)).trim() +extension BaseOpenAIService { + typealias ChatCompletionMessageParam = ChatQuery.ChatCompletionMessageParam + + func chatMessages(text: String, from: Language, to: Language) -> [ChatCompletionMessageParam] { + typealias Role = ChatCompletionMessageParam.Role + + var chats: [ChatCompletionMessageParam] = [] + let messages = translatioMessages(text: text, from: from, to: to) + for message in messages { + if let roleRawValue = message["role"], + let role = Role(rawValue: roleRawValue), + let content = message["content"] { + guard let chat = ChatCompletionMessageParam(role: role, content: content) else { return [] } + chats.append(chat) + } } - // Since it is more difficult to accurately remove redundant quotes in streaming, we wait until the end of the request to remove the quotes - let nsText = resultText as NSString - resultText = nsText.tryToRemoveQuotes().trim() + return chats + } + + func chatMessages( + queryType: EZQueryTextType, + text: String, + from: Language, + to: Language + ) -> [ChatCompletionMessageParam] { + typealias Role = ChatCompletionMessageParam.Role + + var messages = [[String: String]]() + + switch queryType { + case .sentence: + messages = sentenceMessages(sentence: text, from: from, to: to) + case .dictionary: + messages = dictMessages(word: text, sourceLanguage: from, targetLanguage: to) + case .translation: + fallthrough + default: + messages = translatioMessages(text: text, from: from, to: to) + } + + var chats: [ChatCompletionMessageParam] = [] + for message in messages { + if let roleRawValue = message["role"], + let role = Role(rawValue: roleRawValue), + let content = message["content"] { + guard let chat = ChatCompletionMessageParam(role: role, content: content) else { return [] } + chats.append(chat) + } + } - return resultText + return chats } } diff --git a/Easydict/Swift/Service/OpenAI/LLMStreamService.swift b/Easydict/Swift/Service/OpenAI/LLMStreamService.swift new file mode 100644 index 000000000..c85f49038 --- /dev/null +++ b/Easydict/Swift/Service/OpenAI/LLMStreamService.swift @@ -0,0 +1,133 @@ +// +// LLMStreamService.swift +// Easydict +// +// Created by tisfeng on 2024/5/20. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +// MARK: - LLMStreamService + +@objcMembers +@objc(EZLLMStreamService) +public class LLMStreamService: QueryService { + // MARK: Public + + override public func isStream() -> Bool { + true + } + + override public func intelligentQueryTextType() -> EZQueryTextType { + Configuration.shared.intelligentQueryTextTypeForServiceType(serviceType()) + } + + override public func supportLanguagesDictionary() -> MMOrderedDictionary { + let allLangauges = EZLanguageManager.shared().allLanguages + let supportedLanguages = allLangauges.filter { language in + !unsupportedLanguages.contains(language) + } + + let orderedDict = MMOrderedDictionary() + for language in supportedLanguages { + orderedDict.setObject(language.rawValue as NSString, forKey: language.rawValue as NSString) + } + return orderedDict + } + + override public func queryTextType() -> EZQueryTextType { + var typeOptions: EZQueryTextType = [] + + let isTranslationEnabled = UserDefaults.bool(forKey: EZTranslationKey, serviceType: serviceType()) + let isSentenceEnabled = UserDefaults.bool(forKey: EZSentenceKey, serviceType: serviceType()) + let isDictionaryEnabled = UserDefaults.bool(forKey: EZDictionaryKey, serviceType: serviceType()) + + if isTranslationEnabled { + typeOptions.insert(.translation) + } + if isSentenceEnabled { + typeOptions.insert(.sentence) + } + if isDictionaryEnabled { + typeOptions.insert(.dictionary) + } + + return typeOptions + } + + override public func serviceUsageStatus() -> EZServiceUsageStatus { + let usageStatus = UserDefaults.string(forKey: EZServiceUsageStatusKey, serviceType: serviceType()) ?? "" + guard let value = UInt(usageStatus) else { return .default } + return EZServiceUsageStatus(rawValue: value) ?? .default + } + + // MARK: Internal + + let throttler = Throttler() + + let mustOverride = "This property must be overridden by a subclass" + + var unsupportedLanguages: [Language] { + [] + } + + var model: String { + get { fatalError(mustOverride) } + set { _ = newValue; fatalError(mustOverride) } + } + + var availableModels: [String] { + fatalError(mustOverride) + } + + var apiKey: String { + fatalError(mustOverride) + } + + var endpoint: String { + fatalError(mustOverride) + } + + func getFinalResultText(text: String) -> String { + var resultText = text.trim() + + // Remove last , fix Groq model mixtral-8x7b-32768 + let stopFlag = "" + if !queryModel.queryText.hasSuffix(stopFlag), resultText.hasSuffix(stopFlag) { + resultText = String(resultText.dropLast(stopFlag.count)).trim() + } + + // Since it is more difficult to accurately remove redundant quotes in streaming, we wait until the end of the request to remove the quotes + let nsText = resultText as NSString + resultText = nsText.tryToRemoveQuotes().trim() + + return resultText + } + + /// Get query type by text and from && to langauge. + func queryType(text: String, from: Language, to _: Language) -> EZQueryTextType { + let enableDictionary = queryTextType().contains(.dictionary) + var isQueryDictionary = false + if enableDictionary { + isQueryDictionary = (text as NSString).shouldQueryDictionary(withLanguage: from, maxWordCount: 2) + if isQueryDictionary { + return .dictionary + } + } + + let enableSentence = queryTextType().contains(.sentence) + var isQueryEnglishSentence = false + if !isQueryDictionary, enableSentence { + let isEnglishText = from == .english + if isEnglishText { + isQueryEnglishSentence = (text as NSString).shouldQuerySentence(withLanguage: from) + if isQueryEnglishSentence { + return .sentence + } + } + } + + return .translation + } +} diff --git a/Easydict/Swift/Service/OpenAI/OpenAIService.swift b/Easydict/Swift/Service/OpenAI/OpenAIService.swift index 858b74d40..94a20532d 100644 --- a/Easydict/Swift/Service/OpenAI/OpenAIService.swift +++ b/Easydict/Swift/Service/OpenAI/OpenAIService.swift @@ -37,23 +37,19 @@ class OpenAIService: BaseOpenAIService { get { Defaults[.openAIModel] } - set { // easydict://writeKeyValue?EZOpenAIModelKey=gpt-3.5-turbo - Defaults[.openAIModel] = newValue } } override var apiKey: String { // easydict://writeKeyValue?EZOpenAIAPIKey= - Defaults[.openAIAPIKey] ?? "" } override var endpoint: String { // easydict://writeKeyValue?EZOpenAIEndPointKey= - var endPoint = Defaults[.openAIEndPoint] ?? "" if endPoint.isEmpty { endPoint = "https://api.openai.com/v1/chat/completions" diff --git a/Easydict/Swift/Service/OpenAI/Prompt.swift b/Easydict/Swift/Service/OpenAI/Prompt.swift index 15dd67867..268f960bc 100644 --- a/Easydict/Swift/Service/OpenAI/Prompt.swift +++ b/Easydict/Swift/Service/OpenAI/Prompt.swift @@ -7,11 +7,10 @@ // import Foundation -import OpenAI // swiftlint:disable all -extension QueryService { +extension LLMStreamService { static let translationSystemPrompt = """ You are a translation expert proficient in various languages that can only translate text and cannot interpret it. You are able to accurately understand the meaning of proper nouns, idioms, metaphors, allusions or other obscure words in sentences and translate them into appropriate words by combining the context and language environment. The result of the translation should be natural and fluent, you can only return the translated text, do not show redundant quotes and additional notes in translation. """ @@ -249,7 +248,7 @@ extension QueryService { let systemMessages = [ [ "role": "system", - "content": QueryService.translationSystemPrompt, + "content": LLMStreamService.translationSystemPrompt, ], ] @@ -466,7 +465,7 @@ extension QueryService { let systemMessages = [ [ "role": "system", - "content": QueryService.translationSystemPrompt, + "content": LLMStreamService.translationSystemPrompt, ], ] @@ -858,61 +857,6 @@ extension QueryService { } } -extension QueryService { - typealias ChatCompletionMessageParam = ChatQuery.ChatCompletionMessageParam - - func chatMessages(text: String, from: Language, to: Language) -> [ChatCompletionMessageParam] { - typealias Role = ChatCompletionMessageParam.Role - - var chats: [ChatCompletionMessageParam] = [] - let messages = translatioMessages(text: text, from: from, to: to) - for message in messages { - if let roleRawValue = message["role"], - let role = Role(rawValue: roleRawValue), - let content = message["content"] { - guard let chat = ChatCompletionMessageParam(role: role, content: content) else { return [] } - chats.append(chat) - } - } - - return chats - } - - func chatMessages( - queryType: EZQueryTextType, - text: String, - from: Language, - to: Language - ) -> [ChatCompletionMessageParam] { - typealias Role = ChatCompletionMessageParam.Role - - var messages = [[String: String]]() - - switch queryType { - case .sentence: - messages = sentenceMessages(sentence: text, from: from, to: to) - case .dictionary: - messages = dictMessages(word: text, sourceLanguage: from, targetLanguage: to) - case .translation: - fallthrough - default: - messages = translatioMessages(text: text, from: from, to: to) - } - - var chats: [ChatCompletionMessageParam] = [] - for message in messages { - if let roleRawValue = message["role"], - let role = Role(rawValue: roleRawValue), - let content = message["content"] { - guard let chat = ChatCompletionMessageParam(role: role, content: content) else { return [] } - chats.append(chat) - } - } - - return chats - } -} - extension Language { var queryLangaugeName: String { let languageName = switch self { diff --git a/Easydict/Swift/Service/Tencent/TencentService.swift b/Easydict/Swift/Service/Tencent/TencentService.swift index 0c5bf928c..743a5cef1 100644 --- a/Easydict/Swift/Service/Tencent/TencentService.swift +++ b/Easydict/Swift/Service/Tencent/TencentService.swift @@ -54,7 +54,6 @@ public final class TencentService: QueryService { 500 * 10000 } - // swiftlint:disable identifier_name override public func translate( _ text: String, from: Language, @@ -131,8 +130,6 @@ public final class TencentService: QueryService { }, serviceType: serviceType().rawValue) } - // swiftlint:enable identifier_name - // MARK: Private // easydict://writeKeyValue?EZTencentSecretId=xxx diff --git a/Easydict/Swift/Service/Tencent/TencentSigning.swift b/Easydict/Swift/Service/Tencent/TencentSigning.swift index 05f8f50e7..10fb95b1e 100644 --- a/Easydict/Swift/Service/Tencent/TencentSigning.swift +++ b/Easydict/Swift/Service/Tencent/TencentSigning.swift @@ -6,13 +6,12 @@ // Copyright © 2023 izual. All rights reserved. // -// swiftlint:disable all - import Alamofire import CryptoKit import Foundation // Tencent sigh header, Ref: https://github.com/TencentCloud/signature-process-demo/blob/main/signature-v3/swift/signv3.swift +// swiftlint:disable:next function_parameter_count func tencentSignHeader( service: String, action: String, @@ -103,5 +102,3 @@ extension String { return Data(hmac) } } - -// swiftlint:enable all diff --git a/Easydict/Swift/Service/Tencent/TencentTranslateType.swift b/Easydict/Swift/Service/Tencent/TencentTranslateType.swift index 904239e76..bc4c7af7d 100644 --- a/Easydict/Swift/Service/Tencent/TencentTranslateType.swift +++ b/Easydict/Swift/Service/Tencent/TencentTranslateType.swift @@ -176,7 +176,6 @@ struct TencentTranslateType: Equatable { var sourceLanguage: String var targetLanguage: String - // swiftlint:disable:next identifier_name static func transType(from: Language, to: Language) -> TencentTranslateType { /** 1. zh <--> zh-TW diff --git a/Easydict/Swift/Utility/Extensions/String/String+Extension.swift b/Easydict/Swift/Utility/Extensions/String/String+Extension.swift index 5bc7d27a4..4672f8d02 100644 --- a/Easydict/Swift/Utility/Extensions/String/String+Extension.swift +++ b/Easydict/Swift/Utility/Extensions/String/String+Extension.swift @@ -18,7 +18,7 @@ extension String { func trim() -> String { trimmingCharacters(in: .whitespacesAndNewlines) } - + /// Replace all newlines with whitespaces. /// For line breaks, currently macOS is `\n`, previously used `\r`, Windows is `\r\n`. func replacingNewlinesWithWhitespace() -> String { diff --git a/Easydict/Swift/View/SettingView/Tabs/TabView/GeneralTab.swift b/Easydict/Swift/View/SettingView/Tabs/TabView/GeneralTab.swift index 3513440ee..2244ce74b 100644 --- a/Easydict/Swift/View/SettingView/Tabs/TabView/GeneralTab.swift +++ b/Easydict/Swift/View/SettingView/Tabs/TabView/GeneralTab.swift @@ -381,7 +381,6 @@ private struct FirstAndSecondLanguageSettingView: View { // First language should not be same as second language. (\(duplicatedLanguage)) // \(setField) is replaced with \(setLanguage). String( - // swiftlint:disable:next line_length localized: "setting.general.language.duplicated_alert \(duplicatedLanguage.localizedName)\(String(localized: setField.localizedStringResource))\(setLanguage.localizedName)" ) } diff --git a/EasydictSwiftTests/EasydictSwiftTests.swift b/EasydictSwiftTests/EasydictSwiftTests.swift index 2354a1f6c..6aee847c8 100644 --- a/EasydictSwiftTests/EasydictSwiftTests.swift +++ b/EasydictSwiftTests/EasydictSwiftTests.swift @@ -6,8 +6,6 @@ // Copyright © 2023 izual. All rights reserved. // -// swiftlint:disable all - import XCTest final class EasydictSwiftTests: XCTestCase { @@ -41,5 +39,3 @@ final class EasydictSwiftTests: XCTestCase { } } } - -// swiftlint:enable all