diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 0000000..106af85 --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,20 @@ +# This workflow will build a Swift project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift + +name: Build & Test + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Build + run: swift build -v + - name: Test + run: swift test -v diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyGPT.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyGPT.xcscheme new file mode 100644 index 0000000..00140a4 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyGPT.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Documentation/CHAT.md b/Documentation/CHAT.md deleted file mode 100644 index b8add31..0000000 --- a/Documentation/CHAT.md +++ /dev/null @@ -1,104 +0,0 @@ -# Chat - -Chat is the first and main feature of SwiftyGPT, it consists of creating a conversation with **ChatGPT**. -The goal is to provide all the features of this AI Chat Assistant through a very simple interface, therefore SwiftyGPT provides multiple methods for achieving the same purpose. We will therefore make a difference between low-level methods and high-level methods. - -## Low Level - -It allow you maximum control over request creation. The main element of a request is a **SwiftyGPTChatMessage**. - -```swift -let message = SwiftyGPTChatMessage(role: .user, content: "Hi, how are you?") -``` - -You can use **role** to instruct the model precisely as explained by the ChatGPT documentation and get the control you want. - -```swift -swiftyGPT.chat(message: message) { result in - switch result { - case .success(let response): - print(response) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } -} -``` -Alternatively if you need to send multiple messages in one request you can use the multiple input method, that accept an array as input. - -```swift -swiftyGPT.chat(messages: messages) { result in - switch result { - case .success(let response): - print(response) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } -} -``` -In both method you can specify some optional parameters like temperature, maxTokens and others established by OpenAI. Of course, you can also select the model to use. - -```swift -swiftyGPT.chat(message: SwiftyGPTChatMessage(role: .user, content: "Hi, how are you?"), temperature: 5, user: "Test") { result in - switch result { - case .success(let response): - print(response) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } -} -``` - -In case of success methods return a **SwiftyGPTChatResponse** object which is the entire transcript of ChatGPT HTTP response. -To access the received message or messages you have to check the content of the 'choices' attribute. By default choices array size is one, so you can get the message in this way and read its content or other attributes. - -```swift -let message = response.choices.first?.message -``` - -However, if you have requested a different number of choices, the array will have a larger size and you will have to manage the response in a custom way. - - -## High Level - -If you don't need a lot of control on your requests these methods works with simple Strings. Obviously this brings some limitations : - -- **Role:** all messages are sent using the 'user' role, you can't send messages as 'system'. -- **Parameters:** these methods allow you to specify only the model to use and, if necessary, a user. -- **Single Choice:** each request corresponds to a single response message. - -```swift -swiftyGPT.chat(message: "Hi how are you ?") { response in - switch result { - case .success(let response): - print(response) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } -} -``` - -In this case the method directly returns the message of the single choice in string format. - -## Async/Await - -All methods of the chat feature are also available in Async/Await version. - -```swift -let result: Result = await swiftyGPT.chat(message: "Hi how are you ?") -``` diff --git a/Documentation/COMPLETION.md b/Documentation/COMPLETION.md deleted file mode 100644 index 14c8544..0000000 --- a/Documentation/COMPLETION.md +++ /dev/null @@ -1,36 +0,0 @@ -# Completion - -SwiftyGPT also provides methods for creating completions using models like Davinci or Babbage. Given a prompt, the model will return one or more predicted completions based on the 'choices' parameters which you have already seen before. -
-
-Also in this case it is obviously possible to set some parameters in such a way as to best condition our response. - -```swift -swiftyGPT.completion(prompt: "Say \"Hello\" in italian", model: .text_davinci_003) { result in - switch result { - case .success(let response): - print(response) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } -} -``` - -In case of success methods return a SwiftyGPTCompletionResponse object which is the entire transcript of HTTP response. -To get the concrete response text you have to check the content of the 'choices' attribute. - -```swift -let text = response.choices.first?.text -``` - -## Async/Await - -All methods of the completion feature are also available in Async/Await version. - -```swift -let result: Result = await swiftyGPT.completion(prompt: "Say \"Hello\" in italian") -``` diff --git a/Documentation/CORRECTION.md b/Documentation/CORRECTION.md deleted file mode 100644 index e6a23d3..0000000 --- a/Documentation/CORRECTION.md +++ /dev/null @@ -1,32 +0,0 @@ -# Grammar Correction - -This feature allows you to make grammar corrections in a text written in one of the supported languages. -
-It can be thought of as a simple wrapper around the 'Completion' feature which performs a pre-set task, using optimized parameters. -
-Specifying the language used in the text is not mandatory, in fact the model is able to understand it independently. However, in some cases specifying it speeds up response times and increases the quality of the output. - -```swift -swiftyGPT.correction(text: "She no went to the market", language: .english) { result in - switch result { - case .success(let response): - print(response) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } -} -``` - -If successful, the method returns the correct text directly in string format. - -## Async/Await - -The feature is also available in Async/Await version. - -```swift -let result: Result = await swiftyGPT.correction(text: "She no went to the market", language: .english) -``` diff --git a/Documentation/IMAGE.md b/Documentation/IMAGE.md deleted file mode 100644 index 1226971..0000000 --- a/Documentation/IMAGE.md +++ /dev/null @@ -1,50 +0,0 @@ -# Image Generation - -SwiftyGPT uses DALL-E to generate images from textual descriptions. You can describe an object or a scene in words, and SwiftyGPT can create a corresponding image of it. - -## Single Generation - -The easiest way to generate an image is to use the following method, that accept a prompt and a size. It has the limitation of generating only square images of the following sizes: 256x256, 512x512 and 1024x1024. Also in this case if necessary you can specify a user for each call. - -```swift -swiftyGPT.image(prompt: "Draw an unicorn", size: .x256) { result in - switch result { - case .success(let image): - print(image) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } -} -``` -If successful, the method returns an UIImage that you can use directly in UIKit or wrap with an Image if you use SwiftUI. - -## Multiple Generation - -In case you want to generate several different images starting from the same description, you can specify the choices parameter. In this case the method will return an array of Data. - -```swift -swiftyGPT.image(prompt: "Draw an unicorn", choices: 2, size: .x256) { result in - switch result { - case .success(let images): - print(images) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } -} -``` - -## Async/Await - -All methods of the image feature are also available in Async/Await version. - -```swift -let result: Result = await swiftyGPT.image(prompt: "Draw an unicorn", size: .x256) -``` diff --git a/Documentation/SENTIMENT.md b/Documentation/SENTIMENT.md deleted file mode 100644 index 909d6f0..0000000 --- a/Documentation/SENTIMENT.md +++ /dev/null @@ -1,33 +0,0 @@ -# Sentyment Analysis - -It allows you to understand the sentiment related to a text, and therefore to understand if it is positive, negative or neutral. -
-It can be thought of as a simple wrapper around the 'Completion' feature which performs a pre-set task, using optimized parameters. -
-Specifying the language used in the input text is not mandatory, in fact the model is able to understand it independently. However, in some cases specifying it speeds up response times and increases the quality of the output. - -```swift -swiftyGPT.sentiment(text: "I loved the new Batman movie!", language: .english) { result in - switch result { - case .success(let response): - print(response.rawValue) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } -} -``` - -If successful, the method returns a **SwiftyGPTSentiment** object. That is an enum that exposes all possible cases. - -## Async/Await - -The feature is also available in Async/Await version. - -```swift -let result: Result = await swiftyGPT.sentiment(text: "I loved the new Batman movie!", language: .english) -``` - diff --git a/Documentation/SUMMARY.md b/Documentation/SUMMARY.md deleted file mode 100644 index de1b73d..0000000 --- a/Documentation/SUMMARY.md +++ /dev/null @@ -1,37 +0,0 @@ -# Language Translation - -This feature allows you to summarise a text, choosing from all languages those supported by SwiftyGPT.. -
-It can be thought of as a simple wrapper around the 'Completion' feature which performs a pre-set task, using optimized parameters. -
-Specifying the language used in the input text is not mandatory, in fact the model is able to understand it independently. However, in some cases specifying it speeds up response times and increases the quality of the output. - -```swift -let text = """ - La mia istruttrice di nuoto Elke W. aveva un cane barbone bianco, piuttosto grande per la sua razza, di nome Martino. Lei stessa ammetteva che era un cane stupido e fifone, pieno di difetti che lei amava elencare anche a interlocutori occasionali o a perfetti sconosciuti che incontrava per strada. Martino aveva paura, tra le altre cose, degli oggetti appuntiti, delle oche (un cane barbone che aveva paura della oche!) e di entrare nei furgoni dalla parte posteriore. In compenso appena poteva si buttava nell’acqua a nuotare e non si faceva scoraggiare dalla stagione avversa e dalla veemenza dei flutti, talché un giorno di marzo fu proprio la padrona a dover entrare nell’acqua gelata fino alla vita e trascinarlo fuori dal mare per il collare, dopo che il povero Martino era stato sul punto di soccombere. Lo coperse di contumelie, un po’ in italiano e un po’ in dialetto, mentre lui uggiolava sotto un massaggio vigoroso a colpi di plaid. - """ - -swiftyGPT.summary(text: text, language: .italian) { result in - switch result { - case .success(let response): - print(response) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } - expectation.fulfill() -} -``` - -If successful, the method returns the summarized text directly in string format. - -## Async/Await - -The feature is also available in Async/Await version. - -```swift -let result: Result = await swiftyGPT.summary(text: text, language: .italian) -``` diff --git a/Documentation/TRANSLATION.md b/Documentation/TRANSLATION.md deleted file mode 100644 index e0b45af..0000000 --- a/Documentation/TRANSLATION.md +++ /dev/null @@ -1,33 +0,0 @@ -# Language Translation - -This feature allows you to translate a text in another language, choosing from all those supported by SwiftyGPT.. -
-It can be thought of as a simple wrapper around the 'Completion' feature which performs a pre-set task, using optimized parameters. -
-Specifying the language used in the input text is not mandatory, in fact the model is able to understand it independently. However, in some cases specifying it speeds up response times and increases the quality of the output. - -```swift -swiftyGPT.translation(text: "Hi, how are you ?", from: .english, to: .italian) { result in - switch result { - case .success(let response): - print(response) - case .failure(let error): - if let error = error as? SwiftyGPTError { - print(error.message) - } else { - print(error.localizedDescription) - } - } - expectation.fulfill() -} -``` - -If successful, the method returns the correct text directly in string format. - -## Async/Await - -The feature is also available in Async/Await version. - -```swift -let result: Result = await swiftyGPT.translation(text: "Hello", from: .english, to: .italian) -``` diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 13e3017..0000000 --- a/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "pins" : [ - { - "identity" : "swiftyhttp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/antonio-war/SwiftyHTTP", - "state" : { - "revision" : "a342df5ff99456b38543ad5147c539ed3198cb44", - "version" : "0.1.5" - } - }, - { - "identity" : "swiftyranged", - "kind" : "remoteSourceControl", - "location" : "https://github.com/antonio-war/SwiftyRanged", - "state" : { - "revision" : "8ee2f1b071fe5b06334aeab7555df560e08ae555", - "version" : "1.0.0" - } - } - ], - "version" : 2 -} diff --git a/Package.swift b/Package.swift index ca191ac..6f30ac2 100644 --- a/Package.swift +++ b/Package.swift @@ -5,25 +5,43 @@ import PackageDescription let package = Package( name: "SwiftyGPT", - platforms: [.macOS(.v13), .iOS(.v13)], + platforms: [ + .macOS(.v13), + .iOS(.v13) + ], products: [ .library( name: "SwiftyGPT", - targets: ["SwiftyGPT"]), + targets: [ + "SwiftyGPTChat", + ] + ), ], dependencies: [ - .package(url: "https://github.com/antonio-war/SwiftyHTTP", from: "0.1.5"), - .package(url: "https://github.com/antonio-war/SwiftyRanged", from: "1.0.0") ], targets: [ .target( - name: "SwiftyGPT", - dependencies: ["SwiftyHTTP", "SwiftyRanged"]), + name: "SwiftyGPTChat", + dependencies: [ + "SwiftyGPTNetworking" + ] + ), + .testTarget( + name: "SwiftyGPTChatTests", + dependencies: [ + "SwiftyGPTChat" + ] + ), + .target( + name: "SwiftyGPTNetworking", + dependencies: [ + ] + ), .testTarget( - name: "SwiftyGPTTests", - dependencies: ["SwiftyGPT", "SwiftyHTTP"], - resources: [ - .copy("Utils/OpenAI-Info.plist") - ]) + name: "SwiftyGPTNetworkingTests", + dependencies: [ + "SwiftyGPTNetworking" + ] + ) ] ) diff --git a/Sources/SwiftyGPT/Chat/SwiftyGPT+Chat.swift b/Sources/SwiftyGPT/Chat/SwiftyGPT+Chat.swift deleted file mode 100644 index 4dcb3d5..0000000 --- a/Sources/SwiftyGPT/Chat/SwiftyGPT+Chat.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// SwiftyGPT+Chat.swift -// -// -// Created by Antonio Guerra on 30/03/23. -// - -import Foundation -import SwiftyHTTP -import SwiftyRanged - -// MARK: - Chat -extension SwiftyGPT { - - public func chat(messages: [SwiftyGPTChatMessage], model: SwiftyGPTChatModel = .stable, @SwiftyOptionalRanged(0...2) temperature: Float? = nil, choices: Int? = nil, maxTokens: Int? = nil, @SwiftyOptionalRanged(-2...2) presencePenalty: Float? = nil, @SwiftyOptionalRanged(-2...2) frequencyPenalty: Float? = nil, user: String? = nil, completion: @escaping (Result) -> ()) { - - let request = SwiftyGPTChatRequest(messages: messages, model: model, temperature: temperature, choices: choices, stream: false, maxTokens: maxTokens, presencePenalty: presencePenalty, frequencyPenalty: frequencyPenalty, user: user) - SwiftyHTTP.request(SwiftyGPTRouter.chat(apiKey, request)) { result in - switch result { - case .success(let response): - if response.statusCode == 200 { - guard let body = try? JSONDecoder().decode(SwiftyGPTChatResponse.self, from: response.body) else { - completion(.failure(URLError(.badServerResponse))) - return - } - completion(.success(body)) - } else { - guard let error = try? JSONDecoder().decode(SwiftyGPTError.self, from: response.body) else { - completion(.failure(URLError(.badServerResponse))) - return - } - completion(.failure(error)) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func chat(messages: [SwiftyGPTChatMessage], model: SwiftyGPTChatModel = .stable, @SwiftyOptionalRanged(0...2) temperature: Float? = nil, choices: Int? = nil, maxTokens: Int? = nil, @SwiftyOptionalRanged(-2...2) presencePenalty: Float? = nil, @SwiftyOptionalRanged(-2...2) frequencyPenalty: Float? = nil, user: String? = nil) async -> Result { - - return await withCheckedContinuation { continuation in - chat(messages: messages, model: model, temperature: temperature, choices: choices, maxTokens: maxTokens, presencePenalty: presencePenalty, frequencyPenalty: frequencyPenalty, user: user) { result in - continuation.resume(returning: result) - } - } - } - - public func chat(message: SwiftyGPTChatMessage, model: SwiftyGPTChatModel = .stable, @SwiftyOptionalRanged(0...2) temperature: Float? = nil, choices: Int? = nil, maxTokens: Int? = nil, @SwiftyOptionalRanged(-2...2) presencePenalty: Float? = nil, @SwiftyOptionalRanged(-2...2) frequencyPenalty: Float? = nil, user: String? = nil, completion: @escaping (Result) -> ()) { - chat(messages: [message], model: model, temperature: temperature, choices: choices, maxTokens: maxTokens, presencePenalty: presencePenalty, frequencyPenalty: frequencyPenalty, user: user, completion: completion) - } - - public func chat(message: SwiftyGPTChatMessage, model: SwiftyGPTChatModel = .stable, @SwiftyOptionalRanged(0...2) temperature: Float? = nil, choices: Int? = nil, maxTokens: Int? = nil, @SwiftyOptionalRanged(-2...2) presencePenalty: Float? = nil, @SwiftyOptionalRanged(-2...2) frequencyPenalty: Float? = nil, user: String? = nil) async -> Result { - - await chat(messages: [message], model: model, temperature: temperature, choices: choices, maxTokens: maxTokens, presencePenalty: presencePenalty, frequencyPenalty: frequencyPenalty, user: user) - } - - public func chat(messages: [String], model: SwiftyGPTChatModel = .stable, user: String? = nil, completion: @escaping (Result) -> ()) { - chat(messages: messages.map({SwiftyGPTChatMessage(content: $0)}), model: model, user: user) { result in - switch result { - case .success(let response): - guard let message = response.choices.first?.message else { - completion(.failure(URLError(.badServerResponse))) - return - } - completion(.success(message.content)) - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func chat(messages: [String], model: SwiftyGPTChatModel = .stable, user: String? = nil) async -> Result { - return await withCheckedContinuation { continuation in - chat(messages: messages, model: model, user: user) { result in - continuation.resume(returning: result) - } - } - } - - public func chat(message: String, model: SwiftyGPTChatModel = .stable, user: String? = nil, completion: @escaping (Result) -> ()) { - chat(messages: [message], model: model, user: user, completion: completion) - } - - public func chat(message: String, model: SwiftyGPTChatModel = .stable, user: String? = nil) async -> Result { - await chat(messages: [message], model: model, user: user) - } -} diff --git a/Sources/SwiftyGPT/Chat/SwiftyGPTChatMessage.swift b/Sources/SwiftyGPT/Chat/SwiftyGPTChatMessage.swift deleted file mode 100644 index ce03e47..0000000 --- a/Sources/SwiftyGPT/Chat/SwiftyGPTChatMessage.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// SwiftyGPTChatMessage.swift -// -// -// Created by Antonio Guerra on 27/03/23. -// - -import Foundation - -public struct SwiftyGPTChatMessage: Codable, Identifiable { - public let id: UUID = UUID() - public let date: Date = Date() - public let role: SwiftyGPTChatRole - public let content: String - - enum CodingKeys: String, CodingKey { - case role - case content - } - - public init(role: SwiftyGPTChatRole, content: String) { - self.role = role - self.content = content - } - - public init(content: String) { - self.role = .user - self.content = content - } -} diff --git a/Sources/SwiftyGPT/Chat/SwiftyGPTChatModel.swift b/Sources/SwiftyGPT/Chat/SwiftyGPTChatModel.swift deleted file mode 100644 index cf4b9eb..0000000 --- a/Sources/SwiftyGPT/Chat/SwiftyGPTChatModel.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// SwiftyGPTChatModel.swift -// -// -// Created by Antonio Guerra on 27/03/23. -// - -import Foundation - -public enum SwiftyGPTChatModel: String, SwiftyGPTModel { - case gpt4 = "gpt-4" - case gpt4_0314 = "gpt-4-0314" - case gpt4_32k = "gpt-4-32k" - case gpt4_32k_0314 = "gpt-4-32k-0314" - case gpt3_5_turbo = "gpt-3.5-turbo" - case gpt3_5_turbo_0301 = "gpt-3.5-turbo-0301" - - public static var stable: SwiftyGPTChatModel { - gpt3_5_turbo - } - - var maxTokens: Int { - switch self { - case .gpt4: - return 8_192 - case .gpt4_0314: - return 8_192 - case .gpt4_32k: - return 8_192 - case .gpt4_32k_0314: - return 8_192 - case .gpt3_5_turbo: - return 4_096 - case .gpt3_5_turbo_0301: - return 4_096 - } - } -} diff --git a/Sources/SwiftyGPT/Chat/SwiftyGPTChatRequest.swift b/Sources/SwiftyGPT/Chat/SwiftyGPTChatRequest.swift deleted file mode 100644 index 2de2c8b..0000000 --- a/Sources/SwiftyGPT/Chat/SwiftyGPTChatRequest.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// SwiftyGPTChatRequest.swift -// -// -// Created by Antonio Guerra on 27/03/23. -// - -import Foundation -import SwiftyHTTP - -public struct SwiftyGPTChatRequest: SwiftyGPTRequest { - public let messages: [SwiftyGPTChatMessage] - public let model: SwiftyGPTChatModel - public let temperature: Float? - public let choices: Int? - public let stream: Bool? - public let maxTokens: Int? - public let presencePenalty: Float? - public let frequencyPenalty: Float? - public let user: String? - - enum CodingKeys: String, CodingKey { - case model - case messages - case temperature - case choices = "n" - case stream - case maxTokens = "max_tokens" - case presencePenalty = "presence_penalty" - case frequencyPenalty = "frequency_penalty" - case user - } -} diff --git a/Sources/SwiftyGPT/Chat/SwiftyGPTChatResponse.swift b/Sources/SwiftyGPT/Chat/SwiftyGPTChatResponse.swift deleted file mode 100644 index c2dd251..0000000 --- a/Sources/SwiftyGPT/Chat/SwiftyGPTChatResponse.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// SwiftyGPTChatResponse.swift -// -// -// Created by Antonio Guerra on 27/03/23. -// - -import Foundation -import SwiftyHTTP - -public struct SwiftyGPTChatResponse: SwiftyGPTResponse { - public let id, object: String - public let created: TimeInterval - public let model: SwiftyGPTChatModel - public let usage: SwiftyGPTUsage - public let choices: [SwiftyGPTChatChoice] -} - -// MARK: - Choice -public struct SwiftyGPTChatChoice: SwiftyGPTChoice { - public let finishReason: SwiftyGPTFinishReason - public let index: Int - public let message: SwiftyGPTChatMessage - - enum CodingKeys: String, CodingKey { - case message - case finishReason = "finish_reason" - case index - } -} diff --git a/Sources/SwiftyGPT/Chat/SwiftyGPTChatRole.swift b/Sources/SwiftyGPT/Chat/SwiftyGPTChatRole.swift deleted file mode 100644 index aa869ec..0000000 --- a/Sources/SwiftyGPT/Chat/SwiftyGPTChatRole.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// SwiftyGPTChatRole.swift -// -// -// Created by Antonio Guerra on 11/04/23. -// - -import Foundation - -public enum SwiftyGPTChatRole: String, Codable { - case system - case user - case assistant -} diff --git a/Sources/SwiftyGPT/Completion/SwiftyGPT+Completion.swift b/Sources/SwiftyGPT/Completion/SwiftyGPT+Completion.swift deleted file mode 100644 index 40ab522..0000000 --- a/Sources/SwiftyGPT/Completion/SwiftyGPT+Completion.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SwiftyGPT+Completion.swift -// -// -// Created by Antonio Guerra on 11/04/23. -// - -import Foundation -import SwiftyHTTP -import SwiftyRanged - -// MARK: - Completion -extension SwiftyGPT { - - public func completion(prompt: String, model: SwiftyGPTCompletionModel = .stable, suffix: String? = nil, maxTokens: Int? = nil, @SwiftyOptionalRanged(0...2) temperature: Float? = nil, choices: Int? = nil, @SwiftyOptionalRanged(1...5) logprobs: Int? = nil, echo: Bool? = nil, @SwiftyOptionalRanged(-2...2) presencePenalty: Float? = nil, @SwiftyOptionalRanged(-2...2) frequencyPenalty: Float? = nil, user: String? = nil, completion: @escaping (Result) -> ()) { - - let lowerBound = SwiftyGPTTokenizer().tokenize(prompt) - let upperBound = model.maxTokens-lowerBound - @SwiftyRanged(lowerBound...upperBound) var maxTokens = maxTokens ?? upperBound - - let request = SwiftyGPTCompletionRequest(prompt: prompt, model: model, suffix: suffix, maxTokens: maxTokens, temperature: temperature, choices: choices, stream: false, logprobs: logprobs, echo: echo, presencePenalty: presencePenalty, frequencyPenalty: frequencyPenalty, user: user) - SwiftyHTTP.request(SwiftyGPTRouter.completion(apiKey, request)) { result in - switch result { - case .success(let response): - if response.statusCode == 200 { - guard let body = try? JSONDecoder().decode(SwiftyGPTCompletionResponse.self, from: response.body) else { - completion(.failure(URLError(.badServerResponse))) - return - } - - let formattedBody = SwiftyGPTCompletionResponse(id: body.id, object: body.object, created: body.created, usage: body.usage, model: body.model, choices: body.choices.map { SwiftyGPTCompletionChoice(finishReason: $0.finishReason, index: $0.index, text: $0.text.trimmingCharacters(in: .whitespacesAndNewlines), logprobs: $0.logprobs)}) - - completion(.success(formattedBody)) - } else { - guard let error = try? JSONDecoder().decode(SwiftyGPTError.self, from: response.body) else { - completion(.failure(URLError(.badServerResponse))) - return - } - completion(.failure(error)) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func completion(prompt: String, model: SwiftyGPTCompletionModel = .stable, suffix: String? = nil, maxTokens: Int? = nil, @SwiftyOptionalRanged(0...2) temperature: Float? = nil, choices: Int? = nil, @SwiftyOptionalRanged(1...5) logprobs: Int? = nil, echo: Bool? = nil, @SwiftyOptionalRanged(-2...2) presencePenalty: Float? = nil, @SwiftyOptionalRanged(-2...2) frequencyPenalty: Float? = nil, user: String? = nil) async -> Result { - - return await withCheckedContinuation { continuation in - completion(prompt: prompt, model: model, suffix: suffix, maxTokens: maxTokens, temperature: temperature, choices: choices, logprobs: logprobs, echo: echo, presencePenalty: presencePenalty, frequencyPenalty: frequencyPenalty, user: user) { result in - continuation.resume(returning: result) - } - } - } -} diff --git a/Sources/SwiftyGPT/Completion/SwiftyGPTCompletionModel.swift b/Sources/SwiftyGPT/Completion/SwiftyGPTCompletionModel.swift deleted file mode 100644 index 7b126cd..0000000 --- a/Sources/SwiftyGPT/Completion/SwiftyGPTCompletionModel.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// SwiftyGPTCompletionModel.swift -// -// -// Created by Antonio Guerra on 11/04/23. -// - -import Foundation - -public enum SwiftyGPTCompletionModel: String, SwiftyGPTModel { - - case ada - case text_ada_001 = "text-ada-001" - - case babbage - case text_babbage_001 = "text-babbage-001" - - case curie - case text_curie_001 = "text-curie-001" - - case davinci - case text_davinci_002 = "text-davinci-002" - case text_davinci_003 = "text-davinci-003" - - public static var stable: SwiftyGPTCompletionModel { - .text_davinci_003 - } - - var maxTokens: Int { - switch self { - case .ada: - return 2_048 - case .text_ada_001: - return 2_048 - case .babbage: - return 2_048 - case .text_babbage_001: - return 2_048 - case .curie: - return 2_048 - case .text_curie_001: - return 2_048 - case .davinci: - return 2_048 - case .text_davinci_002: - return 4_096 - case .text_davinci_003: - return 4_094 - } - } -} diff --git a/Sources/SwiftyGPT/Completion/SwiftyGPTCompletionRequest.swift b/Sources/SwiftyGPT/Completion/SwiftyGPTCompletionRequest.swift deleted file mode 100644 index c763a90..0000000 --- a/Sources/SwiftyGPT/Completion/SwiftyGPTCompletionRequest.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// SwiftyGPTCompletionRequest.swift -// -// -// Created by Antonio Guerra on 11/04/23. -// - -import Foundation -import SwiftyHTTP - -public struct SwiftyGPTCompletionRequest: SwiftyGPTRequest { - public let prompt: String - public let model: SwiftyGPTCompletionModel - public let suffix: String? - public let maxTokens: Int? - public let temperature: Float? - public let choices: Int? - public let stream: Bool? - public let logprobs: Int? - public let echo: Bool? - public let presencePenalty: Float? - public let frequencyPenalty: Float? - public let user: String? - - enum CodingKeys: String, CodingKey { - case prompt - case model - case suffix - case maxTokens = "max_tokens" - case temperature - case choices = "n" - case stream - case logprobs - case echo - case presencePenalty = "presence_penalty" - case frequencyPenalty = "frequency_penalty" - case user - } -} diff --git a/Sources/SwiftyGPT/Completion/SwiftyGPTCompletionResponse.swift b/Sources/SwiftyGPT/Completion/SwiftyGPTCompletionResponse.swift deleted file mode 100644 index 31682f6..0000000 --- a/Sources/SwiftyGPT/Completion/SwiftyGPTCompletionResponse.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// SwiftyGPTCompletionResponse.swift -// -// -// Created by Antonio Guerra on 11/04/23. -// - -import Foundation - -public struct SwiftyGPTCompletionResponse: SwiftyGPTResponse { - public let id: String - public let object: String - public let created: TimeInterval - public let usage: SwiftyGPTUsage - public let model: SwiftyGPTCompletionModel - public let choices: [SwiftyGPTCompletionChoice] -} - -// MARK: - Choice -public struct SwiftyGPTCompletionChoice: SwiftyGPTChoice { - public let finishReason: SwiftyGPTFinishReason - public let index: Int - public let text: String - public let logprobs: Int? - - enum CodingKeys: String, CodingKey { - case text - case finishReason = "finish_reason" - case index - case logprobs - } -} diff --git a/Sources/SwiftyGPT/Correction/SwiftyGPT+Correction.swift b/Sources/SwiftyGPT/Correction/SwiftyGPT+Correction.swift deleted file mode 100644 index f33cc10..0000000 --- a/Sources/SwiftyGPT/Correction/SwiftyGPT+Correction.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// SwiftyGPT+Correction.swift -// -// -// Created by Antonio Guerra on 13/04/23. -// - -import Foundation - -extension SwiftyGPT { - - public func correction(text: String, language: SwiftyGPTLanguage? = nil, user: String? = nil, completion: @escaping (Result) -> ()) { - self.completion(prompt: prompt(text: text, language: language), model: .text_davinci_003, temperature: 0.0, presencePenalty: 0.0, frequencyPenalty: 0.0, user: user) { result in - switch result { - case .success(let response): - if let text = response.choices.first?.text { - completion(.success(text)) - } else { - completion(.failure(URLError(.badServerResponse))) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func correction(text: String, language: SwiftyGPTLanguage? = nil, user: String? = nil) async -> Result { - return await withCheckedContinuation { continuation in - correction(text: text, language: language, user: user) { result in - continuation.resume(returning: result) - } - } - } - - private func prompt(text: String, language: SwiftyGPTLanguage? = nil) -> String { - let text = text.trimmingCharacters(in: .whitespacesAndNewlines) - guard let language = language?.rawValue.capitalized else { - return "Correct this text: \n\(text)" - } - return "Correct this text to standard \(language): \n\(text)" - } -} diff --git a/Sources/SwiftyGPT/Image/SwiftyGPT+Image.swift b/Sources/SwiftyGPT/Image/SwiftyGPT+Image.swift deleted file mode 100644 index 6803c4e..0000000 --- a/Sources/SwiftyGPT/Image/SwiftyGPT+Image.swift +++ /dev/null @@ -1,145 +0,0 @@ -// -// SwiftyGPT+Image.swift -// -// -// Created by Antonio Guerra on 30/03/23. -// - -import Foundation -import SwiftyHTTP -import SwiftyRanged - -// MARK: - Image with UIKit - -#if canImport(UIKit) - -import UIKit - -extension SwiftyGPT { - - public func image(prompt: String, @SwiftyRanged(1...10) choices: Int, size: SwiftyGPTImageSize? = nil, user: String? = nil, completion: @escaping (Result<[UIImage], Error>) -> ()) { - - let request = SwiftyGPTImageRequest(prompt: prompt, choices: choices, size: size, responseFormat: .b64, user: user) - SwiftyHTTP.request(SwiftyGPTRouter.image(apiKey, request)) { result in - switch result { - case .success(let response): - if response.statusCode == 200 { - guard let body = try? JSONDecoder().decode(SwiftyGPTImageResponse.self, from: response.body) else { - completion(.failure(URLError(.badServerResponse))) - return - } - completion(.success(body.data.compactMap({ Data(base64Encoded: $0.b64) }).compactMap({ UIImage(data: $0)}))) - } else { - guard let error = try? JSONDecoder().decode(SwiftyGPTError.self, from: response.body) else { - completion(.failure(URLError(.badServerResponse))) - return - } - completion(.failure(error)) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func image(prompt: String, @SwiftyRanged(1...10) choices: Int, size: SwiftyGPTImageSize? = nil, user: String? = nil) async -> Result<[UIImage], Error> { - - return await withCheckedContinuation { continuation in - image(prompt: prompt, choices: choices, size: size, user: user) { result in - continuation.resume(returning: result) - } - } - } - - public func image(prompt: String, size: SwiftyGPTImageSize? = nil, user: String? = nil, completion: @escaping (Result) -> ()) { - image(prompt: prompt, choices: 1, size: size, user: user) { result in - switch result { - case .success(let images): - if let image = images.first { - completion(.success(image)) - } else { - completion(.failure(URLError(.badURL))) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func image(prompt: String, size: SwiftyGPTImageSize? = nil, user: String? = nil) async -> Result { - return await withCheckedContinuation { continuation in - image(prompt: prompt, size: size, user: user) { result in - continuation.resume(returning: result) - } - } - } - -} - -// MARK: - Image with AppKit - -#elseif canImport(AppKit) - -import AppKit - -extension SwiftyGPT { - - public func image(prompt: String, @SwiftyRanged(1...10) choices: Int, size: SwiftyGPTImageSize? = nil, user: String? = nil, completion: @escaping (Result<[NSImage], Error>) -> ()) { - - let request = SwiftyGPTImageRequest(prompt: prompt, choices: choices, size: size, responseFormat: .b64, user: user) - SwiftyHTTP.request(SwiftyGPTRouter.image(apiKey, request)) { result in - switch result { - case .success(let response): - if response.statusCode == 200 { - guard let body = try? JSONDecoder().decode(SwiftyGPTImageResponse.self, from: response.body) else { - completion(.failure(URLError(.badServerResponse))) - return - } - completion(.success(body.data.compactMap({ Data(base64Encoded: $0.b64) }).compactMap({ NSImage(data: $0) }))) - } else { - guard let error = try? JSONDecoder().decode(SwiftyGPTError.self, from: response.body) else { - completion(.failure(URLError(.badServerResponse))) - return - } - completion(.failure(error)) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func image(prompt: String, @SwiftyRanged(1...10) choices: Int, size: SwiftyGPTImageSize? = nil, user: String? = nil) async -> Result<[NSImage], Error> { - - return await withCheckedContinuation { continuation in - image(prompt: prompt, choices: choices, size: size, user: user) { result in - continuation.resume(returning: result) - } - } - } - - public func image(prompt: String, size: SwiftyGPTImageSize? = nil, user: String? = nil, completion: @escaping (Result) -> ()) { - image(prompt: prompt, choices: 1, size: size, user: user) { result in - switch result { - case .success(let images): - if let image = images.first { - completion(.success(image)) - } else { - completion(.failure(URLError(.badURL))) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func image(prompt: String, size: SwiftyGPTImageSize? = nil, user: String? = nil) async -> Result { - return await withCheckedContinuation { continuation in - image(prompt: prompt, size: size, user: user) { result in - continuation.resume(returning: result) - } - } - } -} - -#endif diff --git a/Sources/SwiftyGPT/Image/SwiftyGPTImageRequest.swift b/Sources/SwiftyGPT/Image/SwiftyGPTImageRequest.swift deleted file mode 100644 index 67b83a9..0000000 --- a/Sources/SwiftyGPT/Image/SwiftyGPTImageRequest.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// SwiftyGPTImageRequest.swift -// -// -// Created by Antonio Guerra on 30/03/23. -// - -import Foundation -import SwiftyHTTP - -public struct SwiftyGPTImageRequest: SwiftyHTTPRequestBody { - public let prompt: String - public let choices: Int? - public let size: SwiftyGPTImageSize? - public let responseFormat: SwiftyGPTImageResponseFormat? - public let user: String? - - enum CodingKeys: String, CodingKey { - case prompt - case choices = "n" - case size - case responseFormat = "response_format" - case user - } -} - -// MARK: - ImageSize -public enum SwiftyGPTImageSize: String, Encodable { - case x256 = "256x256" - case x512 = "512x512" - case x1024 = "1024x1024" -} - -// MARK: - ResponseFormat -public enum SwiftyGPTImageResponseFormat: String, Codable { - case url = "url" - case b64 = "b64_json" -} diff --git a/Sources/SwiftyGPT/Image/SwiftyGPTImageResponse.swift b/Sources/SwiftyGPT/Image/SwiftyGPTImageResponse.swift deleted file mode 100644 index 3bea4a6..0000000 --- a/Sources/SwiftyGPT/Image/SwiftyGPTImageResponse.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SwiftyGPTImageResponse.swift -// -// -// Created by Antonio Guerra on 30/03/23. -// - -import Foundation -import SwiftyHTTP - -struct SwiftyGPTImageResponse: SwiftyHTTPResponseBody { - let created: TimeInterval - let data: [ResponseData] -} - -protocol SwiftyGPTImageResponseData: Decodable {} - -struct SwiftyGTPImageURLResponseData: SwiftyGPTImageResponseData { - let url: String -} - -struct SwiftyGPTB64ResponseData: SwiftyGPTImageResponseData { - let b64: String - - enum CodingKeys: String, CodingKey { - case b64 = "b64_json" - } -} diff --git a/Sources/SwiftyGPT/Networking/SwiftyGPTRouter.swift b/Sources/SwiftyGPT/Networking/SwiftyGPTRouter.swift deleted file mode 100644 index 6f9b9ed..0000000 --- a/Sources/SwiftyGPT/Networking/SwiftyGPTRouter.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// SwiftyGPTRouter.swift -// -// -// Created by Antonio Guerra on 27/03/23. -// - -import Foundation -import SwiftyHTTP - -enum SwiftyGPTRouter: SwiftyHTTPRequest { - - case chat(String, SwiftyGPTChatRequest) - case image(String, SwiftyGPTImageRequest) - case completion(String, SwiftyGPTCompletionRequest) - - var baseURL: URL? { - return URL(string: "https://api.openai.com") - } - - var path: String { - switch self { - case .chat: - return "/v1/chat/completions" - case .image: - return "/v1/images/generations" - case .completion: - return "/v1/completions" - } - } - - var method: SwiftyHTTPMethod { - switch self { - case .chat: - return .post - case .image: - return .post - case .completion: - return .post - } - } - - var headers: [SwiftyHTTPHeader] { - switch self { - case .chat(let apiKey, _), .image(let apiKey, _), .completion(let apiKey, _): - return [ - SwiftyHTTPHeader.contentType(.application(.json)), - SwiftyHTTPHeader.authorization(.bearer(apiKey)) - ] - } - } - - var parameters: [SwiftyHTTPQueryParameter] { - [] - } - - var body: SwiftyHTTPRequestBody? { - switch self { - case .chat(_, let request): - return request - case .image(_, let request): - return request - case .completion(_, let request): - return request - } - } -} diff --git a/Sources/SwiftyGPT/Sentiment/SwiftyGPT+SentimentAnalysis.swift b/Sources/SwiftyGPT/Sentiment/SwiftyGPT+SentimentAnalysis.swift deleted file mode 100644 index cc54b43..0000000 --- a/Sources/SwiftyGPT/Sentiment/SwiftyGPT+SentimentAnalysis.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// SwiftyGPT+Sentiment.swift -// -// -// Created by Antonio Guerra on 13/04/23. -// - -import Foundation - -extension SwiftyGPT { - - public func sentiment(text: String, language: SwiftyGPTLanguage? = nil, user: String? = nil, completion: @escaping (Result) -> ()) { - self.completion(prompt: prompt(text: text, language: language), model: .text_davinci_003, temperature: 0.0, presencePenalty: 0.0, frequencyPenalty: 0.5, user: user) { result in - switch result { - case .success(let response): - if let text = response.choices.first?.text { - completion(.success(SwiftyGPTSentiment(rawValue: text.lowercased()) ?? .neutral)) - } else { - completion(.failure(URLError(.badServerResponse))) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func sentiment(text: String, language: SwiftyGPTLanguage? = nil, user: String? = nil) async -> Result { - return await withCheckedContinuation { continuation in - sentiment(text: text, language: language, user: user) { result in - continuation.resume(returning: result) - } - } - } - - private func prompt(text: String, language: SwiftyGPTLanguage? = nil) -> String { - let text = text.trimmingCharacters(in: .whitespacesAndNewlines) - guard let language = language?.rawValue.capitalized else { - return "Decide whether this text sentiment is positive, neutral, or negative: \n\(text)" - } - return "Decide whether this \(language) text sentiment is positive, neutral, or negative: \n\(text)" - } -} diff --git a/Sources/SwiftyGPT/Sentiment/SwiftyGPTSentiment.swift b/Sources/SwiftyGPT/Sentiment/SwiftyGPTSentiment.swift deleted file mode 100644 index 7289b53..0000000 --- a/Sources/SwiftyGPT/Sentiment/SwiftyGPTSentiment.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// SwiftyGPTSentiment.swift -// -// -// Created by Antonio Guerra on 13/04/23. -// - -import Foundation - -public enum SwiftyGPTSentiment: String, Codable { - case positive - case neutral - case negative - - var intValue: Int { - switch self { - case .positive: - return 1 - case .neutral: - return 0 - case .negative: - return -1 - } - } -} diff --git a/Sources/SwiftyGPT/Summary/SwiftyGPT+Summary.swift b/Sources/SwiftyGPT/Summary/SwiftyGPT+Summary.swift deleted file mode 100644 index 0fbf232..0000000 --- a/Sources/SwiftyGPT/Summary/SwiftyGPT+Summary.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// SwiftyGPT+Summary.swift -// -// -// Created by Antonio Guerra on 12/04/23. -// - -import Foundation - -extension SwiftyGPT { - - public func summary(text: String, language: SwiftyGPTLanguage? = nil, user: String? = nil, completion: @escaping (Result) -> ()) { - self.completion(prompt: prompt(text: text, language: language), model: .text_davinci_003, temperature: 0.3, presencePenalty: 0.0, frequencyPenalty: 0.0, user: user) { result in - switch result { - case .success(let response): - if let text = response.choices.first?.text { - completion(.success(text)) - } else { - completion(.failure(URLError(.badServerResponse))) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func summary(text: String, language: SwiftyGPTLanguage? = nil, user: String? = nil) async -> Result { - return await withCheckedContinuation { continuation in - summary(text: text, language: language, user: user) { result in - continuation.resume(returning: result) - } - } - } - - private func prompt(text: String, language: SwiftyGPTLanguage? = nil) -> String { - let text = text.trimmingCharacters(in: .whitespacesAndNewlines) - guard let language = language?.rawValue.capitalized else { - return "Summarize this text for a second-grade student: \n\(text)" - } - return "Summarize this \(language) text for a second-grade student: \n\(text)" - } -} diff --git a/Sources/SwiftyGPT/SwiftyGPT.swift b/Sources/SwiftyGPT/SwiftyGPT.swift deleted file mode 100644 index 54c87d0..0000000 --- a/Sources/SwiftyGPT/SwiftyGPT.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// SwiftyGPT.swift -// -// -// Created by Antonio Guerra on 27/03/23. -// - -import Foundation -import SwiftyHTTP -import SwiftyRanged - -public struct SwiftyGPT { - - let apiKey: String - - public init(apiKey: String) { - self.apiKey = apiKey - } -} diff --git a/Sources/SwiftyGPT/Translation/SwiftyGPT+Translation.swift b/Sources/SwiftyGPT/Translation/SwiftyGPT+Translation.swift deleted file mode 100644 index 03dffb9..0000000 --- a/Sources/SwiftyGPT/Translation/SwiftyGPT+Translation.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// SwiftyGPT+Translation.swift -// -// -// Created by Antonio Guerra on 12/04/23. -// - -import Foundation - -extension SwiftyGPT { - - public func translation(text: String, from: SwiftyGPTLanguage? = nil, to: SwiftyGPTLanguage, user: String? = nil, completion: @escaping (Result) -> ()) { - self.completion(prompt: prompt(text: text, from: from, to: to), model: .text_davinci_003, temperature: 0.3, presencePenalty: 0.0, frequencyPenalty: 0.0, user: user) { result in - switch result { - case .success(let response): - if let text = response.choices.first?.text { - completion(.success(text)) - } else { - completion(.failure(URLError(.badServerResponse))) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public func translation(text: String, from: SwiftyGPTLanguage? = nil, to: SwiftyGPTLanguage, user: String? = nil) async -> Result { - return await withCheckedContinuation { continuation in - translation(text: text, from: from, to: to, user: user) { result in - continuation.resume(returning: result) - } - } - } - - private func prompt(text: String, from: SwiftyGPTLanguage? = nil, to: SwiftyGPTLanguage) -> String { - let text = text.trimmingCharacters(in: .whitespacesAndNewlines) - let to = to.rawValue.capitalized - guard let from = from?.rawValue.capitalized else { - return "Translate this into \(to): \n\(text)" - } - return "Translate this from \(from) into \(to): \n\(text)" - } -} diff --git a/Sources/SwiftyGPT/Utils/SwiftyGPTError.swift b/Sources/SwiftyGPT/Utils/SwiftyGPTError.swift deleted file mode 100644 index b613b9a..0000000 --- a/Sources/SwiftyGPT/Utils/SwiftyGPTError.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// SwiftyGPTError.swift -// -// -// Created by Antonio Guerra on 29/03/23. -// - -import Foundation - -public struct SwiftyGPTError: Error, Decodable { - let message, type: String - let code: String? - - enum WrapperCodingKeys: CodingKey { - case error - } - - enum CodingKeys: CodingKey { - case message - case type - case code - } - - public init(from decoder: Decoder) throws { - let wrapper = try decoder.container(keyedBy: WrapperCodingKeys.self) - let container = try wrapper.nestedContainer(keyedBy: CodingKeys.self, forKey: .error) - self.message = try container.decode(String.self, forKey: .message) - self.type = try container.decode(String.self, forKey: .type) - self.code = try container.decode(String?.self, forKey: .code) - } -} diff --git a/Sources/SwiftyGPT/Utils/SwiftyGPTLanguage.swift b/Sources/SwiftyGPT/Utils/SwiftyGPTLanguage.swift deleted file mode 100644 index 5000e4a..0000000 --- a/Sources/SwiftyGPT/Utils/SwiftyGPTLanguage.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// SwiftyGPTLanguage.swift -// -// -// Created by Antonio Guerra on 12/04/23. -// - -import Foundation -import NaturalLanguage - -public enum SwiftyGPTLanguage: String { - case afrikaans - case arabic - case armenian - case azerbaijani - case belarusian - case bosnian - case bulgarian - case catalan - case chinese - case croatian - case czech - case danish - case dutch - case english - case estonian - case finnish - case french - case galician - case german - case greek - case hebrew - case hindi - case hungarian - case icelandic - case indonesian - case italian - case japanese - case kannada - case kazakh - case korean - case latvian - case spanish - case swahili - case swedish - case tagalog - case tamil - case thai - case turkish - case ukrainian - case urdu - case vietnamese - case welsh - - var naturalLanguage: NLLanguage? { - switch self { - case .afrikaans: - return nil - case .arabic: - return .arabic - case .armenian: - return .armenian - case .azerbaijani: - return nil - case .belarusian: - return nil - case .bosnian: - return nil - case .bulgarian: - return .bulgarian - case .catalan: - return .catalan - case .chinese: - return .simplifiedChinese - case .croatian: - return .croatian - case .czech: - return .czech - case .danish: - return .danish - case .dutch: - return .dutch - case .english: - return .english - case .estonian: - return nil - case .finnish: - return .finnish - case .french: - return .french - case .galician: - return nil - case .german: - return .german - case .greek: - return .greek - case .hebrew: - return .hebrew - case .hindi: - return .hindi - case .hungarian: - return .hungarian - case .icelandic: - return .icelandic - case .indonesian: - return .indonesian - case .italian: - return .italian - case .japanese: - return .japanese - case .kannada: - return .kannada - case .kazakh: - if #available(iOS 16.0, *) { - return .kazakh - } else { - return nil - } - case .korean: - return .korean - case .latvian: - return nil - case .spanish: - return .spanish - case .swahili: - return nil - case .swedish: - return .swedish - case .tagalog: - return nil - case .tamil: - return .tamil - case .thai: - return .thai - case .turkish: - return .turkish - case .ukrainian: - return .ukrainian - case .urdu: - return .urdu - case .vietnamese: - return .vietnamese - case .welsh: - return nil - } - } -} diff --git a/Sources/SwiftyGPT/Utils/SwiftyGPTModel.swift b/Sources/SwiftyGPT/Utils/SwiftyGPTModel.swift deleted file mode 100644 index 60e0731..0000000 --- a/Sources/SwiftyGPT/Utils/SwiftyGPTModel.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// SwiftyGPTModel.swift -// -// -// Created by Antonio Guerra on 11/04/23. -// - -import Foundation - -protocol SwiftyGPTModel: Codable { - static var stable: Self { get } - - var maxTokens: Int { get } -} diff --git a/Sources/SwiftyGPT/Utils/SwiftyGPTRequest.swift b/Sources/SwiftyGPT/Utils/SwiftyGPTRequest.swift deleted file mode 100644 index a58acba..0000000 --- a/Sources/SwiftyGPT/Utils/SwiftyGPTRequest.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SwiftyGPTRequest.swift -// -// -// Created by Antonio Guerra on 11/04/23. -// - -import Foundation -import SwiftyHTTP - -protocol SwiftyGPTRequest: SwiftyHTTPRequestBody { - associatedtype GPTModel: SwiftyGPTModel - - var model: GPTModel { get } - var user: String? { get } - var choices: Int? { get } - var maxTokens: Int? { get } - var temperature: Float? { get } - var stream: Bool? { get } - var presencePenalty: Float? { get } - var frequencyPenalty: Float? { get } -} diff --git a/Sources/SwiftyGPT/Utils/SwiftyGPTResponse.swift b/Sources/SwiftyGPT/Utils/SwiftyGPTResponse.swift deleted file mode 100644 index b7ef271..0000000 --- a/Sources/SwiftyGPT/Utils/SwiftyGPTResponse.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// SwiftyGPTResponse.swift -// -// -// Created by Antonio Guerra on 11/04/23. -// - -import Foundation -import SwiftyHTTP - -protocol SwiftyGPTResponse: SwiftyHTTPResponseBody { - associatedtype GPTModel: SwiftyGPTModel - associatedtype GPTChoice: SwiftyGPTChoice - - var id: String { get } - var object: String { get } - var created: TimeInterval { get } - var model: GPTModel { get } - var usage: SwiftyGPTUsage { get } - var choices: [GPTChoice] { get } -} - -// MARK: - Choice -protocol SwiftyGPTChoice: Codable { - var finishReason: SwiftyGPTFinishReason { get } - var index: Int { get } -} - -// MARK: - Usage -public struct SwiftyGPTUsage: Codable { - public let promptTokens, completionTokens, totalTokens: Int - - enum CodingKeys: String, CodingKey { - case promptTokens = "prompt_tokens" - case completionTokens = "completion_tokens" - case totalTokens = "total_tokens" - } -} - -// MARK: - FinishReason -public enum SwiftyGPTFinishReason: String, Codable { - case stop - case length - case contentFilter = "content_filter" - case null -} diff --git a/Sources/SwiftyGPT/Utils/SwiftyGPTTokenizer.swift b/Sources/SwiftyGPT/Utils/SwiftyGPTTokenizer.swift deleted file mode 100644 index b2515fe..0000000 --- a/Sources/SwiftyGPT/Utils/SwiftyGPTTokenizer.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// SwiftyGPTTokenizer.swift -// -// -// Created by Antonio Guerra on 13/04/23. -// - -import Foundation -import NaturalLanguage - -public struct SwiftyGPTTokenizer { - private let wrapped: NLTokenizer - - init() { - wrapped = NLTokenizer(unit: .word) - } - - func tokenize(_ text: String, language: SwiftyGPTLanguage? = nil) -> Int { - wrapped.string = text - - if let language = language?.naturalLanguage { - wrapped.setLanguage(language) - } - - return Int(Float(wrapped.tokens(for: text.startIndex.. SwiftyGPTChatResponse { + let requestBody = SwiftyGPTChatRequestBody(messages: messages, model: model, frequencyPenalty: frequencyPenalty, logitBias: logitBias, logprobs: logprobs, topLogprobs: topLogprobs, maxTokens: maxTokens, n: n, presencePenalty: presencePenalty, responseFormat: responseFormat, seed: seed, temperature: temperature, topP: topP, user: user) + let responseBody = try await service.request(body: requestBody) + return try SwiftyGPTChatResponse(body: responseBody) + } +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatMessage.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatMessage.swift new file mode 100644 index 0000000..4968661 --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatMessage.swift @@ -0,0 +1,175 @@ +// +// SwiftyGPTChatMessage.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +import Foundation + +protocol SwiftyGPTChatMessage: Equatable, Encodable, Decodable { + var role: SwiftyGPTChatRole { get } +} + +public struct SwiftyGPTChatSystemMessage: SwiftyGPTChatMessage { + public let role: SwiftyGPTChatRole = .system + public let content: String + public let name: String? + + public init(content: String, name: String? = nil) { + self.content = content + self.name = name + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.content = try container.decode(String.self, forKey: .content) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(role, forKey: .role) + try container.encode(content, forKey: .content) + try container.encodeIfPresent(name, forKey: .name) + } + + enum CodingKeys: CodingKey { + case role + case content + case name + } +} + +struct SwiftyGPTChatUserMessage: SwiftyGPTChatMessage { + let role: SwiftyGPTChatRole = .user + let content: String + let name: String? + + init(content: String, name: String? = nil) { + self.content = content + self.name = name + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.content = try container.decode(String.self, forKey: .content) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(role, forKey: .role) + try container.encode(content, forKey: .content) + try container.encodeIfPresent(name, forKey: .name) + } + + enum CodingKeys: CodingKey { + case role + case content + case name + } +} + +struct SwiftyGPTChatAssistantMessage: SwiftyGPTChatMessage { + let role: SwiftyGPTChatRole = .assistant + // TODO: content will be optional once tool_calls parameter will be supported + let content: String + let name: String? + // TODO: add tool_calls parameter support + + init(content: String, name: String? = nil) { + self.content = content + self.name = name + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.content = try container.decode(String.self, forKey: .content) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.role, forKey: .role) + try container.encode(self.content, forKey: .content) + try container.encodeIfPresent(self.name, forKey: .name) + } + + enum CodingKeys: CodingKey { + case role + case content + case name + } +} + +struct SwiftyGPTChatToolMessage: SwiftyGPTChatMessage { + let role: SwiftyGPTChatRole = .tool + let content: String + // TODO: add tool_call_id + + init(content: String) { + self.content = content + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.content = try container.decode(String.self, forKey: .content) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.role, forKey: .role) + try container.encode(self.content, forKey: .content) + } + + enum CodingKeys: CodingKey { + case role + case content + } +} + +enum SwiftyGPTChatCodableMessage: Equatable, Encodable, Decodable { + case system(SwiftyGPTChatSystemMessage) + case user(SwiftyGPTChatUserMessage) + case assistant(SwiftyGPTChatAssistantMessage) + case tool(SwiftyGPTChatToolMessage) + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let singleContainer = try decoder.singleValueContainer() + let role = try container.decode(SwiftyGPTChatRole.self, forKey: .role) + switch role { + case .system: + let message = try singleContainer.decode(SwiftyGPTChatSystemMessage.self) + self = .system(message) + case .user: + let message = try singleContainer.decode(SwiftyGPTChatUserMessage.self) + self = .user(message) + case .assistant: + let message = try singleContainer.decode(SwiftyGPTChatAssistantMessage.self) + self = .assistant(message) + case .tool: + let message = try singleContainer.decode(SwiftyGPTChatToolMessage.self) + self = .tool(message) + } + } + + func encode(to encoder: any Encoder) throws { + var singleContainer = encoder.singleValueContainer() + switch self { + case .system(let message): + try singleContainer.encode(message) + case .user(let message): + try singleContainer.encode(message) + case .assistant(let message): + try singleContainer.encode(message) + case .tool(let message): + try singleContainer.encode(message) + } + } + + enum CodingKeys: CodingKey { + case role + } +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatModel.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatModel.swift new file mode 100644 index 0000000..15e6a25 --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatModel.swift @@ -0,0 +1,26 @@ +// +// SwiftyGPTChatModel.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +import Foundation + +public enum SwiftyGPTChatModel: String, Codable { + case gpt4_turbo = "gpt-4-turbo" + case gpt4_turbo_preview = "gpt-4-turbo-preview" + case gpt4_0125_preview = "gpt-4-0125-preview" + case gpt4_1106_preview = "gpt-4-1106-preview" + case gpt4 = "gpt-4" + case gpt4_0613 = "gpt-4-0613" + case gpt4_32k = "gpt-4-32k" + case gpt4_32k_0613 = "gpt-4-32k-0613" + case gpt3_5_turbo_0125 = "gpt-3.5-turbo-0125" + case gpt3_5_turbo = "gpt-3.5-turbo" + case gpt3_5_turbo_0301 = "gpt-3.5-turbo-0301" + case gpt3_5_turbo_1106 = "gpt-3.5-turbo-1106" + case gpt3_5_turbo_16k = "gpt-3.5-turbo-16k" + case gpt3_5_turbo_0613 = "gpt-3.5-turbo-0613" + case gpt3_5_turbo_16k_0613 = "gpt-3.5-turbo-16k-0613" +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatRequest.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatRequest.swift new file mode 100644 index 0000000..7c78d85 --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatRequest.swift @@ -0,0 +1,55 @@ +// +// SwiftyGPTChatRequest.swift +// +// +// Created by Antonio Guerra on 14/04/24. +// + +import Foundation +import SwiftyGPTNetworking + +struct SwiftyGPTChatRequest: SwiftyGPTNetworkingRequest { + private let apiKey: String + private let organization: String? + internal let body: Data? + + init(apiKey: String, organization: String? = nil, body: Data) { + self.apiKey = apiKey + self.organization = organization + self.body = body + } + + var endpoint: URL? { + return URL(string: "https://api.openai.com/") + } + + var path: String { + return "v1/chat/completions" + } + + var method: SwiftyGPTNetworkingMethod { + return .post + } + + public var headers: [String : String] { + guard let organization else { + return [ + "Content-Type": "application/json", + "Authorization": "Bearer \(apiKey)" + ] + } + return [ + "Content-Type": "application/json", + "Authorization": "Bearer \(apiKey)", + "OpenAI-Organization": organization + ] + } + + public var cachePolicy: URLRequest.CachePolicy { + return .reloadIgnoringLocalCacheData + } + + public var timeout: TimeInterval { + return 60.0 + } +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatRequestBody.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatRequestBody.swift new file mode 100644 index 0000000..9959b5f --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatRequestBody.swift @@ -0,0 +1,98 @@ +// +// SwiftyGPTChatRequestBody.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +import Foundation + +struct SwiftyGPTChatRequestBody: Encodable { + let messages: [any SwiftyGPTChatMessage] + let model: SwiftyGPTChatModel + let frequencyPenalty: Double? + let logitBias: [Int: Int]? + let logprobs: Bool? + let topLogprobs: Int? + let maxTokens: Int? + let n: Int? + let presencePenalty: Double? + let responseFormat: SwiftyGPTChatResponseFormat? + let seed: Int? + // TODO: add stop parameter support + // TODO: add stream parameter support + let temperature: Double? + let topP: Double? + // TODO: add tools parameter support + // TODO: add tool_choice parameter support + let user: String? + + init(messages: [any SwiftyGPTChatMessage], model: SwiftyGPTChatModel, frequencyPenalty: Double? = nil, logitBias: [Int: Int]? = nil, logprobs: Bool? = nil, topLogprobs: Int? = nil, maxTokens: Int? = nil, n: Int? = nil, presencePenalty: Double? = nil, responseFormat: SwiftyGPTChatResponseFormat? = nil, seed: Int? = nil, temperature: Double? = nil, topP: Double? = nil, user: String? = nil) { + self.messages = messages + self.model = model + self.frequencyPenalty = frequencyPenalty + self.logitBias = logitBias + self.logprobs = logprobs + self.topLogprobs = topLogprobs + self.maxTokens = maxTokens + self.n = n + self.presencePenalty = presencePenalty + self.responseFormat = responseFormat + self.seed = seed + self.temperature = temperature + self.topP = topP + self.user = user + } + + var codableMessages: [SwiftyGPTChatCodableMessage] { + return messages.compactMap { + switch $0 { + case let message as SwiftyGPTChatSystemMessage: + return .system(message) + case let message as SwiftyGPTChatUserMessage: + return .user(message) + case let message as SwiftyGPTChatAssistantMessage: + return .assistant(message) + case let message as SwiftyGPTChatToolMessage: + return .tool(message) + default: + return .none + } + } + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(model, forKey: .model) + try container.encode(codableMessages, forKey: .messages) + try container.encodeIfPresent(frequencyPenalty, forKey: .frequencyPenalty) + try container.encodeIfPresent(logitBias, forKey: .logitBias) + try container.encodeIfPresent(logprobs, forKey: .logprobs) + try container.encodeIfPresent(topLogprobs, forKey: .topLogprobs) + try container.encodeIfPresent(maxTokens, forKey: .maxTokens) + try container.encodeIfPresent(n, forKey: .n) + try container.encodeIfPresent(presencePenalty, forKey: .presencePenalty) + try container.encodeIfPresent(responseFormat, forKey: .responseFormat) + try container.encodeIfPresent(seed, forKey: .seed) + try container.encodeIfPresent(temperature, forKey: .temperature) + try container.encodeIfPresent(topP, forKey: .topP) + try container.encodeIfPresent(user, forKey: .user) + } + + enum CodingKeys: String, CodingKey { + case messages + case model + case frequencyPenalty = "frequency_penalty" + case logitBias = "logit_bias" + case logprobs + case topLogprobs = "top_logprobs" + case maxTokens = "max_tokens" + case n = "n" + case presencePenalty = "presence_penalty" + case responseFormat = "response_format" + case seed + case temperature + case topP = "top_p" + case user + } +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponse.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponse.swift new file mode 100644 index 0000000..2b55db6 --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponse.swift @@ -0,0 +1,24 @@ +// +// SwiftyGPTChatResponse.swift +// +// +// Created by Antonio Guerra on 14/04/24. +// + +import Foundation + +enum SwiftyGPTChatResponse: Equatable { + case success(body: SwiftyGPTChatResponseSuccessBody) + case failure(body: SwiftyGPTChatResponseFailureBody) + + init(body: any SwiftyGPTChatResponseBody) throws { + switch body { + case let body as SwiftyGPTChatResponseSuccessBody: + self = .success(body: body) + case let body as SwiftyGPTChatResponseFailureBody: + self = .failure(body: body) + default: + throw URLError(.cannotParseResponse) + } + } +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseBody.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseBody.swift new file mode 100644 index 0000000..c3e72ec --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseBody.swift @@ -0,0 +1,33 @@ +// +// SwiftyGPTChatResponseBody.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +import Foundation + +public protocol SwiftyGPTChatResponseBody: Decodable, Equatable {} + +public struct SwiftyGPTChatResponseSuccessBody: SwiftyGPTChatResponseBody, Identifiable { + public let id: String + public let choices: [SwiftyGPTChatResponseChoice] + public let created: TimeInterval + public let model: SwiftyGPTChatModel + public let fingerprint: String + public let object: SwiftyGPTChatResponseObject + public let usage: SwiftyGPTChatResponseTokenUsage + + enum CodingKeys: String, CodingKey { + case id + case choices + case created + case model + case fingerprint = "system_fingerprint" + case object + case usage + } +} + +public struct SwiftyGPTChatResponseFailureBody: SwiftyGPTChatResponseBody, Decodable { +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseChoice.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseChoice.swift new file mode 100644 index 0000000..2fa97f6 --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseChoice.swift @@ -0,0 +1,27 @@ +// +// SwiftyGPTChatResponseChoice.swift +// +// +// Created by Antonio Guerra on 27/04/24. +// + +import Foundation + +public struct SwiftyGPTChatResponseChoice: Decodable, Equatable { + let index: Int + let codableMessage: SwiftyGPTChatCodableMessage + let finishReason: SwiftyGPTChatResponseFinishReason + + var message: any SwiftyGPTChatMessage { + switch codableMessage { + case .system(let message): + return message + case .user(let message): + return message + case .assistant(let message): + return message + case .tool(let message): + return message + } + } +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseFinishReason.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseFinishReason.swift new file mode 100644 index 0000000..a78df4c --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseFinishReason.swift @@ -0,0 +1,15 @@ +// +// SwiftyGPTChatResponseFinishReason.swift +// +// +// Created by Antonio Guerra on 27/04/24. +// + +import Foundation + +enum SwiftyGPTChatResponseFinishReason: String, Decodable { + case stop + case lenght + case contentFilter = "content_filter" + case toolCalls = "tool_calls" +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseFormat.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseFormat.swift new file mode 100644 index 0000000..3bd1a8d --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseFormat.swift @@ -0,0 +1,22 @@ +// +// SwiftyGPTChatResponseFormat.swift +// +// +// Created by Antonio Guerra on 27/04/24. +// + +import Foundation + +enum SwiftyGPTChatResponseFormat: String, Encodable { + case text = "text" + case json = "json_object" + + enum CodingKeys: CodingKey { + case type + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(rawValue, forKey: .type) + } +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseObject.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseObject.swift new file mode 100644 index 0000000..9ce5cd1 --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseObject.swift @@ -0,0 +1,12 @@ +// +// SwiftyGPTChatResponseObject.swift +// +// +// Created by Antonio Guerra on 27/04/24. +// + +import Foundation + +public enum SwiftyGPTChatResponseObject: String, Decodable { + case completion = "chat.completion" +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseTokenUsage.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseTokenUsage.swift new file mode 100644 index 0000000..09c0ed6 --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatResponseTokenUsage.swift @@ -0,0 +1,20 @@ +// +// SwiftyGPTChatResponseUsage.swift +// +// +// Created by Antonio Guerra on 27/04/24. +// + +import Foundation + +public struct SwiftyGPTChatResponseTokenUsage: Decodable, Equatable { + let completion: Int + let prompt: Int + let total: Int + + enum CodingKeys: String, CodingKey { + case completion = "completion_tokens" + case prompt = "prompt_tokens" + case total = "total_tokens" + } +} diff --git a/Sources/SwiftyGPTChat/Models/SwiftyGPTChatRole.swift b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatRole.swift new file mode 100644 index 0000000..756f275 --- /dev/null +++ b/Sources/SwiftyGPTChat/Models/SwiftyGPTChatRole.swift @@ -0,0 +1,15 @@ +// +// SwiftyGPTChatRole.swift +// +// +// Created by Antonio Guerra on 09/04/24. +// + +import Foundation + +public enum SwiftyGPTChatRole: String, Encodable, Decodable { + case system + case user + case assistant + case tool +} diff --git a/Sources/SwiftyGPTChat/Services/SwiftyGPTChatMockService.swift b/Sources/SwiftyGPTChat/Services/SwiftyGPTChatMockService.swift new file mode 100644 index 0000000..1a27c91 --- /dev/null +++ b/Sources/SwiftyGPTChat/Services/SwiftyGPTChatMockService.swift @@ -0,0 +1,29 @@ +// +// SwiftyGPTChatMockService.swift +// +// +// Created by Antonio Guerra on 13/04/24. +// + +import Foundation + +class SwiftyGPTChatMockService: SwiftyGPTChatService { + private let responseBody: any SwiftyGPTChatResponseBody + private let duration: TimeInterval + + private (set) var requestCallCount: Int = 0 + var requestCalled: Bool { + return requestCallCount > 0 + } + + init(responseBody: any SwiftyGPTChatResponseBody, duration: TimeInterval = 0.0) { + self.responseBody = responseBody + self.duration = duration + } + + func request(body: SwiftyGPTChatRequestBody) async throws -> any SwiftyGPTChatResponseBody { + try await Task.sleep(nanoseconds: UInt64(duration) * 1_000_000_000) + requestCallCount += 1 + return responseBody + } +} diff --git a/Sources/SwiftyGPTChat/Services/SwiftyGPTChatNetworkingService.swift b/Sources/SwiftyGPTChat/Services/SwiftyGPTChatNetworkingService.swift new file mode 100644 index 0000000..e3f57b8 --- /dev/null +++ b/Sources/SwiftyGPTChat/Services/SwiftyGPTChatNetworkingService.swift @@ -0,0 +1,37 @@ +// +// SwiftyGPTChatNetworkingService.swift +// +// +// Created by Antonio Guerra on 13/04/24. +// + +import Foundation +import SwiftyGPTNetworking + +struct SwiftyGPTChatNetworkingService: SwiftyGPTChatService { + let client: SwiftyGPTNetworkingClient + let encoder: JSONEncoder + let decoder: JSONDecoder + let apiKey: String + let organization: String? + + init(client: SwiftyGPTNetworkingClient = SwiftyGPTNetworkingClient(session: URLSession.shared), encoder: JSONEncoder = JSONEncoder(), decoder: JSONDecoder = JSONDecoder(), apiKey: String, organization: String? = nil) { + self.client = client + self.encoder = encoder + self.decoder = decoder + self.apiKey = apiKey + self.organization = organization + } + + func request(body: SwiftyGPTChatRequestBody) async throws -> any SwiftyGPTChatResponseBody { + let body = try JSONEncoder().encode(body) + let request = SwiftyGPTChatRequest(apiKey: apiKey, organization: organization, body: body) + let response = try await client.send(request: request) + switch response.statusCode { + case 200..<300: + return try decoder.decode(SwiftyGPTChatResponseSuccessBody.self, from: response.body) + default: + return try decoder.decode(SwiftyGPTChatResponseFailureBody.self, from: response.body) + } + } +} diff --git a/Sources/SwiftyGPTChat/Services/SwiftyGPTChatService.swift b/Sources/SwiftyGPTChat/Services/SwiftyGPTChatService.swift new file mode 100644 index 0000000..8989689 --- /dev/null +++ b/Sources/SwiftyGPTChat/Services/SwiftyGPTChatService.swift @@ -0,0 +1,12 @@ +// +// SwiftyGPTChatService.swift +// +// +// Created by Antonio Guerra on 13/04/24. +// + +import Foundation + +protocol SwiftyGPTChatService { + func request(body: SwiftyGPTChatRequestBody) async throws -> any SwiftyGPTChatResponseBody +} diff --git a/Sources/SwiftyGPTNetworking/Clients/SwiftyGPTNetworkingClient.swift b/Sources/SwiftyGPTNetworking/Clients/SwiftyGPTNetworkingClient.swift new file mode 100644 index 0000000..9b97395 --- /dev/null +++ b/Sources/SwiftyGPTNetworking/Clients/SwiftyGPTNetworkingClient.swift @@ -0,0 +1,24 @@ +// +// SwiftyGPTNetworkingClient.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +import Foundation + +public struct SwiftyGPTNetworkingClient: Equatable { + private let session: URLSession + + public init(session: URLSession = URLSession.shared) { + self.session = session + } + + public func send(request: SwiftyGPTNetworkingRequest) async throws -> SwiftyGPTNetworkingResponse { + let (body, underlyingResponse) = try await session.data(for: request.underlyingRequest) + guard let underlyingResponse = underlyingResponse as? HTTPURLResponse else { + throw URLError(.cannotParseResponse) + } + return SwiftyGPTNetworkingResponse(underlyingResponse: underlyingResponse, body: body) + } +} diff --git a/Sources/SwiftyGPTNetworking/Models/SwiftyGPTNetworkingMethod.swift b/Sources/SwiftyGPTNetworking/Models/SwiftyGPTNetworkingMethod.swift new file mode 100644 index 0000000..374fc2f --- /dev/null +++ b/Sources/SwiftyGPTNetworking/Models/SwiftyGPTNetworkingMethod.swift @@ -0,0 +1,15 @@ +// +// SwiftyGPTNetworkingMethod.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +import Foundation + +public enum SwiftyGPTNetworkingMethod: String { + case get = "GET" + case post = "POST" + case put = "PUT" + case delete = "DELETE" +} diff --git a/Sources/SwiftyGPTNetworking/Models/SwiftyGPTNetworkingRequest.swift b/Sources/SwiftyGPTNetworking/Models/SwiftyGPTNetworkingRequest.swift new file mode 100644 index 0000000..de1053c --- /dev/null +++ b/Sources/SwiftyGPTNetworking/Models/SwiftyGPTNetworkingRequest.swift @@ -0,0 +1,41 @@ +// +// SwiftyGPTNetworkingRequest.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +import Foundation + +public protocol SwiftyGPTNetworkingRequest { + var endpoint: URL? { get } + var path: String { get } + var url: URL { get throws } + var method: SwiftyGPTNetworkingMethod { get } + var headers: [String: String] { get } + var body: Data? { get } + var cachePolicy: URLRequest.CachePolicy { get } + var timeout: TimeInterval { get } + var underlyingRequest: URLRequest { get throws } +} + +public extension SwiftyGPTNetworkingRequest { + var url: URL { + get throws { + guard let endpoint, let scheme = endpoint.scheme, scheme.lowercased().hasPrefix("http"), let url = URL(string: path, relativeTo: endpoint) else { + throw URLError(.badURL) + } + return url + } + } + + var underlyingRequest: URLRequest { + get throws { + var request = try URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: timeout) + request.httpMethod = method.rawValue + request.httpBody = body + request.allHTTPHeaderFields = headers + return request + } + } +} diff --git a/Sources/SwiftyGPTNetworking/Models/SwiftyGPTNetworkingResponse.swift b/Sources/SwiftyGPTNetworking/Models/SwiftyGPTNetworkingResponse.swift new file mode 100644 index 0000000..e15a3ea --- /dev/null +++ b/Sources/SwiftyGPTNetworking/Models/SwiftyGPTNetworkingResponse.swift @@ -0,0 +1,17 @@ +// +// SwiftyGPTNetworkingResponse.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +import Foundation + +public struct SwiftyGPTNetworkingResponse { + public var underlyingResponse: HTTPURLResponse + public var body: Data + + public var statusCode: Int { + return underlyingResponse.statusCode + } +} diff --git a/Tests/SwiftyGPTChatTests/Managers/SwiftyGPTChatManagerTests.swift b/Tests/SwiftyGPTChatTests/Managers/SwiftyGPTChatManagerTests.swift new file mode 100644 index 0000000..f51da3b --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Managers/SwiftyGPTChatManagerTests.swift @@ -0,0 +1,20 @@ +// +// SwiftyGPTChatManagerTests.swift +// +// +// Created by Antonio Guerra on 14/04/24. +// + +@testable import SwiftyGPTChat +import XCTest + +final class SwiftyGPTChatManagerTests: XCTestCase { + + func testRequestWhenResponseIsSuccessful() async throws { + let mockedResponseBody = SwiftyGPTChatResponseSuccessBody(id: "Test", created: 0.0, model: .gpt3_5_turbo, fingerprint: "Test", object: "Test") + let service = SwiftyGPTChatMockService(responseBody: mockedResponseBody) + let manager = SwiftyGPTChatManager(service: service) + let response = try await manager.send(messages: [], model: .gpt3_5_turbo) + XCTAssertEqual(response, .success(body: mockedResponseBody)) + } +} diff --git a/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatMessageTests.swift b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatMessageTests.swift new file mode 100644 index 0000000..3b1028e --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatMessageTests.swift @@ -0,0 +1,160 @@ +// +// SwiftyGPTChatMessageTests.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +@testable import SwiftyGPTChat +import XCTest + +final class SwiftyGPTChatMessageTests: XCTestCase { + + var encoder: JSONEncoder! + + override func setUpWithError() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + self.encoder = encoder + } + + override func tearDownWithError() throws { + self.encoder = nil + } + + func testSystemMessageInitWhenNameIsNil() throws { + let message = SwiftyGPTChatSystemMessage(content: "Test") + XCTAssertEqual(message.role, .system) + XCTAssertEqual(message.content, "Test") + XCTAssertEqual(message.name, nil) + } + + func testSystemMessageInitWhenNameIsNotNil() throws { + let message = SwiftyGPTChatSystemMessage(content: "Test", name: "Test") + XCTAssertEqual(message.role, .system) + XCTAssertEqual(message.content, "Test") + XCTAssertEqual(message.name, "Test") + } + + func testSystemMessageEncodeWhenNameIsNil() throws { + let message = SwiftyGPTChatSystemMessage(content: "Test") + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"role\":\"system\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testSystemMessageEncodeWhenNameIsNotNil() throws { + let message = SwiftyGPTChatSystemMessage(content: "Test", name: "Test") + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"name\":\"Test\",\"role\":\"system\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testUserMessageInitWhenNameIsNil() throws { + let message = SwiftyGPTChatUserMessage(content: "Test") + XCTAssertEqual(message.role, .user) + XCTAssertEqual(message.content, "Test") + XCTAssertEqual(message.name, nil) + } + + func testUserMessageInitWhenNameIsNotNil() throws { + let message = SwiftyGPTChatUserMessage(content: "Test", name: "Test") + XCTAssertEqual(message.role, .user) + XCTAssertEqual(message.content, "Test") + XCTAssertEqual(message.name, "Test") + } + + func testUserMessageEncodeWhenNameIsNil() throws { + let message = SwiftyGPTChatUserMessage(content: "Test") + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"role\":\"user\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testUserMessageEncodeWhenNameIsNotNil() throws { + let message = SwiftyGPTChatUserMessage(content: "Test", name: "Test") + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"name\":\"Test\",\"role\":\"user\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testAssistantMessageInitWhenNameIsNil() throws { + let message = SwiftyGPTChatAssistantMessage(content: "Test") + XCTAssertEqual(message.role, .assistant) + XCTAssertEqual(message.content, "Test") + XCTAssertEqual(message.name, nil) + } + + func testAssistantMessageInitWhenNameIsNotNil() throws { + let message = SwiftyGPTChatAssistantMessage(content: "Test", name: "Test") + XCTAssertEqual(message.role, .assistant) + XCTAssertEqual(message.content, "Test") + XCTAssertEqual(message.name, "Test") + } + + func testAssistanteMessageEncodeWhenNameIsNil() throws { + let message = SwiftyGPTChatAssistantMessage(content: "Test") + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"role\":\"assistant\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testAssistantMessageEncodeWhenNameIsNotNil() throws { + let message = SwiftyGPTChatAssistantMessage(content: "Test", name: "Test") + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"name\":\"Test\",\"role\":\"assistant\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testToolMessageInit() throws { + let message = SwiftyGPTChatToolMessage(content: "Test") + XCTAssertEqual(message.role, .tool) + XCTAssertEqual(message.content, "Test") + } + + func testToolMessageEncode() throws { + let message = SwiftyGPTChatToolMessage(content: "Test") + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"role\":\"tool\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testCodableMessageEncodeWhenRoleIsSystem() throws { + let message = SwiftyGPTChatCodableMessage.system(SwiftyGPTChatSystemMessage(content: "Test")) + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"role\":\"system\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testCodableMessageEncodeWhenRoleIsUser() throws { + let message = SwiftyGPTChatCodableMessage.user(SwiftyGPTChatUserMessage(content: "Test")) + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"role\":\"user\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testCodableMessageEncodeWhenRoleIsAssistant() throws { + let message = SwiftyGPTChatCodableMessage.assistant(SwiftyGPTChatAssistantMessage(content: "Test")) + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"role\":\"assistant\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testCodableMessageEncodeWhenRoleIsTool() throws { + let message = SwiftyGPTChatCodableMessage.tool(SwiftyGPTChatToolMessage(content: "Test")) + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"content\":\"Test\",\"role\":\"tool\"}" + XCTAssertEqual(encodedString, expectedString) + } +} diff --git a/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatModelTests.swift b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatModelTests.swift new file mode 100644 index 0000000..9fb9d83 --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatModelTests.swift @@ -0,0 +1,88 @@ +// +// SwiftyGPTChatModelTests.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +@testable import SwiftyGPTChat +import XCTest + +final class SwiftyGPTChatModelTests: XCTestCase { + + func testGpt4_turboInitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-4-turbo")) + XCTAssertEqual(model, .gpt4_turbo) + } + + func testGpt4_turbo_previewInitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-4-turbo-preview")) + XCTAssertEqual(model, .gpt4_turbo_preview) + } + + func testGpt4_0125_previewInitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-4-0125-preview")) + XCTAssertEqual(model, .gpt4_0125_preview) + } + + func testGpt4_1106_previewInitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-4-1106-preview")) + XCTAssertEqual(model, .gpt4_1106_preview) + } + + func testGpt4InitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-4")) + XCTAssertEqual(model, .gpt4) + } + + func testGpt4_0613InitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-4-0613")) + XCTAssertEqual(model, .gpt4_0613) + } + + func testGpt4_32kInitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-4-32k")) + XCTAssertEqual(model, .gpt4_32k) + } + + func testGpt4_32k_0613InitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-4-32k-0613")) + XCTAssertEqual(model, .gpt4_32k_0613) + } + + func testGpt3_5_turboInitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-3.5-turbo")) + XCTAssertEqual(model, .gpt3_5_turbo) + } + + func testGpt3_5_turbo_16kInitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-3.5-turbo-16k")) + XCTAssertEqual(model, .gpt3_5_turbo_16k) + } + + func testGpt3_5_turbo_16k_0613InitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-3.5-turbo-16k-0613")) + XCTAssertEqual(model, .gpt3_5_turbo_16k_0613) + } + + func testGpt3_5_turbo_0301InitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-3.5-turbo-0301")) + XCTAssertEqual(model, .gpt3_5_turbo_0301) + } + + func testGpt3_5_turbo_0613InitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-3.5-turbo-0613")) + XCTAssertEqual(model, .gpt3_5_turbo_0613) + } + + func testGpt3_5_turbo_1106InitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-3.5-turbo-1106")) + XCTAssertEqual(model, .gpt3_5_turbo_1106) + } + + func testGpt3_5_turbo_0125InitFromRawValue() throws { + let model = try XCTUnwrap(SwiftyGPTChatModel(rawValue: "gpt-3.5-turbo-0125")) + XCTAssertEqual(model, .gpt3_5_turbo_0125) + } +} + diff --git a/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatRequestBodyTests.swift b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatRequestBodyTests.swift new file mode 100644 index 0000000..33b2f4b --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatRequestBodyTests.swift @@ -0,0 +1,41 @@ +// +// SwiftyGPTChatRequestBodyTests.swift +// +// +// Created by Antonio Guerra on 12/04/24. +// + +@testable import SwiftyGPTChat +import XCTest + +final class SwiftyGPTChatRequestBodyTests: XCTestCase { + + func testCodableMessages() { + let requestBody = SwiftyGPTChatRequestBody(messages: [ + SwiftyGPTChatSystemMessage(content: "Test"), + SwiftyGPTChatAssistantMessage(content: "Test"), + SwiftyGPTChatUserMessage(content: "Test"), + SwiftyGPTChatToolMessage(content: "Test") + ], model: .gpt3_5_turbo) + + XCTAssertEqual(requestBody.codableMessages.count, 4) + XCTAssertEqual(requestBody.codableMessages[0], .system(SwiftyGPTChatSystemMessage(content: "Test"))) + XCTAssertEqual(requestBody.codableMessages[1], .assistant(SwiftyGPTChatAssistantMessage(content: "Test"))) + XCTAssertEqual(requestBody.codableMessages[2], .user(SwiftyGPTChatUserMessage(content: "Test"))) + XCTAssertEqual(requestBody.codableMessages[3], .tool(SwiftyGPTChatToolMessage(content: "Test"))) + } + + func testCodableMessagesWhenTypeIsUnknown() { + let requestBody = SwiftyGPTChatRequestBody(messages: [ + SwiftyGPTUnknownMessage(role: .user), + ], model: .gpt3_5_turbo) + + XCTAssertTrue(requestBody.codableMessages.isEmpty) + } + + private struct SwiftyGPTUnknownMessage: SwiftyGPTChatMessage { + var role: SwiftyGPTChatRole + } +} + + diff --git a/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatRequestTests.swift b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatRequestTests.swift new file mode 100644 index 0000000..4a9d11e --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatRequestTests.swift @@ -0,0 +1,43 @@ +// +// SwiftyGPTChatRequestTests.swift +// +// +// Created by Antonio Guerra on 15/04/24. +// + +@testable import SwiftyGPTChat +import XCTest + +final class SwiftyGPTChatRequestTests: XCTestCase { + + func testInitWhenOrganizationIsNil() throws { + let request = SwiftyGPTChatRequest(apiKey: "Test", body: Data()) + XCTAssertEqual(request.endpoint, URL(string: "https://api.openai.com/")) + XCTAssertEqual(request.path, "v1/chat/completions") + XCTAssertEqual(request.method, .post) + XCTAssertEqual(request.headers, [ + "Content-Type": "application/json", + "Authorization": "Bearer Test" + ]) + XCTAssertEqual(request.cachePolicy, .reloadIgnoringLocalCacheData) + XCTAssertEqual(request.timeout, 60.0) + XCTAssertEqual(request.body, Data()) + try XCTAssertEqual(request.url.absoluteString, "https://api.openai.com/v1/chat/completions") + } + + func testInitWhenOrganizationIsNotNil() throws { + let request = SwiftyGPTChatRequest(apiKey: "Test", organization: "Test", body: Data()) + XCTAssertEqual(request.endpoint, URL(string: "https://api.openai.com/")) + XCTAssertEqual(request.path, "v1/chat/completions") + XCTAssertEqual(request.method, .post) + XCTAssertEqual(request.headers, [ + "Content-Type": "application/json", + "Authorization": "Bearer Test", + "OpenAI-Organization": "Test" + ]) + XCTAssertEqual(request.cachePolicy, .reloadIgnoringLocalCacheData) + XCTAssertEqual(request.timeout, 60.0) + XCTAssertEqual(request.body, Data()) + try XCTAssertEqual(request.url.absoluteString, "https://api.openai.com/v1/chat/completions") + } +} diff --git a/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatResponseBodyTests.swift b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatResponseBodyTests.swift new file mode 100644 index 0000000..e7150bf --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatResponseBodyTests.swift @@ -0,0 +1,12 @@ +// +// SwiftyGPTChatResponseBodyTests.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +@testable import SwiftyGPTChat +import XCTest + +final class SwiftyGPTChatResponseBodyTests: XCTestCase { +} diff --git a/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatResponseFormatTests.swift b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatResponseFormatTests.swift new file mode 100644 index 0000000..c5bda94 --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatResponseFormatTests.swift @@ -0,0 +1,50 @@ +// +// SwiftyGPTChatResponseFormatTests.swift +// +// +// Created by Antonio Guerra on 27/04/24. +// + +import XCTest +@testable import SwiftyGPTChat + +final class SwiftyGPTChatResponseFormatTests: XCTestCase { + + var encoder: JSONEncoder! + + override func setUpWithError() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + self.encoder = encoder + } + + override func tearDownWithError() throws { + self.encoder = nil + } + + func testTextFormatInit() { + let format = SwiftyGPTChatResponseFormat.text + XCTAssertEqual(format.rawValue, "text") + } + + func testTextFormatEncode() throws { + let message = SwiftyGPTChatResponseFormat.text + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"type\":\"text\"}" + XCTAssertEqual(encodedString, expectedString) + } + + func testJsonFormatInit() { + let format = SwiftyGPTChatResponseFormat.json + XCTAssertEqual(format.rawValue, "json_object") + } + + func testJsonFormatEncode() throws { + let message = SwiftyGPTChatResponseFormat.json + let encodedMessage = try encoder.encode(message) + let encodedString = String(data: encodedMessage, encoding: .utf8) + let expectedString = "{\"type\":\"json_object\"}" + XCTAssertEqual(encodedString, expectedString) + } +} diff --git a/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatResponseTests.swift b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatResponseTests.swift new file mode 100644 index 0000000..8a96071 --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatResponseTests.swift @@ -0,0 +1,13 @@ +// +// SwiftyGPTChatResponseTests.swift +// +// +// Created by Antonio Guerra on 15/04/24. +// + +@testable import SwiftyGPTChat +import XCTest + +final class SwiftyGPTChatResponseTests: XCTestCase { +} + diff --git a/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatRoleTests.swift b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatRoleTests.swift new file mode 100644 index 0000000..4eb643a --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Models/SwiftyGPTChatRoleTests.swift @@ -0,0 +1,32 @@ +// +// SwiftyGPTChatRoleTests.swift +// +// +// Created by Antonio Guerra on 09/04/24. +// + +@testable import SwiftyGPTChat +import XCTest + +final class SwiftyGPTChatRoleTests: XCTestCase { + + func testSystemInitFromRawValue() throws { + let role = try XCTUnwrap(SwiftyGPTChatRole(rawValue: "system")) + XCTAssertEqual(role, .system) + } + + func testUserInitFromRawValue() throws { + let role = try XCTUnwrap(SwiftyGPTChatRole(rawValue: "user")) + XCTAssertEqual(role, .user) + } + + func testAssistantInitFromRawValue() throws { + let role = try XCTUnwrap(SwiftyGPTChatRole(rawValue: "assistant")) + XCTAssertEqual(role, .assistant) + } + + func testToolInitFromRawValue() throws { + let role = try XCTUnwrap(SwiftyGPTChatRole(rawValue: "tool")) + XCTAssertEqual(role, .tool) + } +} diff --git a/Tests/SwiftyGPTChatTests/Services/SwiftyGPTChatMockServiceTests.swift b/Tests/SwiftyGPTChatTests/Services/SwiftyGPTChatMockServiceTests.swift new file mode 100644 index 0000000..edff5ab --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Services/SwiftyGPTChatMockServiceTests.swift @@ -0,0 +1,24 @@ +// +// SwiftyGPTChatMockServiceTests.swift +// +// +// Created by Antonio Guerra on 14/04/24. +// + +@testable import SwiftyGPTChat +import XCTest + +final class SwiftyGPTChatMockServiceTests: XCTestCase { + + func testRequestWhenResponseIsSuccessful() async throws { + let mockedResponseBody = SwiftyGPTChatResponseSuccessBody(id: "Test", created: 0.0, model: .gpt3_5_turbo, fingerprint: "Test", object: "Test") + let service = SwiftyGPTChatMockService(responseBody: mockedResponseBody) + let requestBody = SwiftyGPTChatRequestBody(messages: [], model: .gpt3_5_turbo) + let genericResponseBody = try await service.request(body: requestBody) + let successResponseBody = try XCTUnwrap(genericResponseBody as? SwiftyGPTChatResponseSuccessBody) + XCTAssertEqual(mockedResponseBody, successResponseBody) + XCTAssertEqual(service.requestCallCount, 1) + XCTAssertTrue(service.requestCalled) + } +} + diff --git a/Tests/SwiftyGPTChatTests/Services/SwiftyGPTChatNetworkingServiceTests.swift b/Tests/SwiftyGPTChatTests/Services/SwiftyGPTChatNetworkingServiceTests.swift new file mode 100644 index 0000000..31c4e49 --- /dev/null +++ b/Tests/SwiftyGPTChatTests/Services/SwiftyGPTChatNetworkingServiceTests.swift @@ -0,0 +1,23 @@ +// +// SwiftyGPTChatNetworkingServiceTests.swift +// +// +// Created by Antonio Guerra on 26/04/24. +// + +import XCTest +@testable import SwiftyGPTChat +@testable import SwiftyGPTNetworking + +final class SwiftyGPTChatNetworkingServiceTests: XCTestCase { + + func testInit() { + let client = SwiftyGPTNetworkingClient() + let apiKey = "testApiKey" + let organization = "testOrganization" + let service = SwiftyGPTChatNetworkingService(client: client, apiKey: apiKey, organization: organization) + XCTAssertEqual(service.client, client) + XCTAssertEqual(service.apiKey, apiKey) + XCTAssertEqual(service.organization, organization) + } +} diff --git a/Tests/SwiftyGPTNetworkingTests/Clients/SwiftyGPTNetworkingClientTests.swift b/Tests/SwiftyGPTNetworkingTests/Clients/SwiftyGPTNetworkingClientTests.swift new file mode 100644 index 0000000..cddbd89 --- /dev/null +++ b/Tests/SwiftyGPTNetworkingTests/Clients/SwiftyGPTNetworkingClientTests.swift @@ -0,0 +1,39 @@ +// +// SwiftyGPTNetworkingClientTests.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +@testable import SwiftyGPTNetworking +import XCTest + +final class SwiftyGPTNetworkingClientTests: XCTestCase { + + var client: SwiftyGPTNetworkingClient! + + override func setUpWithError() throws { + self.client = SwiftyGPTNetworkingClient(session: URLSession.shared) + } + + override func tearDownWithError() throws { + self.client = nil + } + + func testSend() async throws { + let request = SwiftyGPTNetworkingTestRequest() + let response = try await client.send(request: request) + XCTAssertEqual(response.statusCode, 200) + let _ = try XCTUnwrap(UIImage(data: response.body)) + } + + private struct SwiftyGPTNetworkingTestRequest: SwiftyGPTNetworkingRequest { + let endpoint: URL? = URL(string: "https://picsum.photos/") + let path: String = "10" + let method: SwiftyGPTNetworkingMethod = .get + let body: Data? = nil + let headers: [String : String] = [:] + let cachePolicy: URLRequest.CachePolicy = .reloadIgnoringLocalAndRemoteCacheData + let timeout: TimeInterval = 60 + } +} diff --git a/Tests/SwiftyGPTNetworkingTests/Models/SwiftyGPTNetworkingRequestTests.swift b/Tests/SwiftyGPTNetworkingTests/Models/SwiftyGPTNetworkingRequestTests.swift new file mode 100644 index 0000000..2bf77f6 --- /dev/null +++ b/Tests/SwiftyGPTNetworkingTests/Models/SwiftyGPTNetworkingRequestTests.swift @@ -0,0 +1,56 @@ +// +// SwiftyGPTNetworkingRequestTests.swift +// +// +// Created by Antonio Guerra on 11/04/24. +// + +import XCTest +@testable import SwiftyGPTNetworking + +final class SwiftyGPTNetworkingRequestTests: XCTestCase { + + func testInit() throws { + let request = SwiftyGPTNetworkingTestRequest() + XCTAssertEqual(request.endpoint, URL(string: "https://picsum.photos/")) + XCTAssertEqual(request.path, "10") + XCTAssertEqual(request.method, .get) + XCTAssertEqual(request.body, Data()) + XCTAssertEqual(request.headers, ["Key":"Value"]) + XCTAssertEqual(request.cachePolicy, .reloadIgnoringLocalAndRemoteCacheData) + XCTAssertEqual(request.timeout, 60) + try XCTAssertEqual(request.url.absoluteString, "https://picsum.photos/10") + try XCTAssertEqual(request.underlyingRequest.url, URL(string: request.url.absoluteString)) + try XCTAssertEqual(request.underlyingRequest.httpMethod, request.method.rawValue) + try XCTAssertEqual(request.underlyingRequest.httpBody, request.body) + try XCTAssertEqual(request.underlyingRequest.allHTTPHeaderFields, request.headers) + try XCTAssertEqual(request.underlyingRequest.cachePolicy, request.cachePolicy) + try XCTAssertEqual(request.underlyingRequest.timeoutInterval, request.timeout) + } + + func testInitWhenRequestIsInvalid() { + let request = SwiftyGPTNetworkingInvalidRequest() + try XCTAssertThrowsError(request.url) + try XCTAssertThrowsError(request.underlyingRequest) + } + + private struct SwiftyGPTNetworkingTestRequest: SwiftyGPTNetworkingRequest { + let endpoint: URL? = URL(string: "https://picsum.photos/") + let path: String = "10" + let method: SwiftyGPTNetworkingMethod = .get + let body: Data? = Data() + let headers: [String : String] = ["Key":"Value"] + let cachePolicy: URLRequest.CachePolicy = .reloadIgnoringLocalAndRemoteCacheData + let timeout: TimeInterval = 60 + } + + private struct SwiftyGPTNetworkingInvalidRequest: SwiftyGPTNetworkingRequest { + let endpoint: URL? = URL(string: "file://picsum.photos/") + let path: String = "10" + let method: SwiftyGPTNetworkingMethod = .get + let body: Data? = Data() + let headers: [String : String] = ["Key":"Value"] + let cachePolicy: URLRequest.CachePolicy = .reloadIgnoringLocalAndRemoteCacheData + let timeout: TimeInterval = 60 + } +} diff --git a/Tests/SwiftyGPTTests/SwiftyGPTChatTests.swift b/Tests/SwiftyGPTTests/SwiftyGPTChatTests.swift deleted file mode 100644 index f086c60..0000000 --- a/Tests/SwiftyGPTTests/SwiftyGPTChatTests.swift +++ /dev/null @@ -1,135 +0,0 @@ -import XCTest -@testable import SwiftyGPT - -final class SwiftyGPTChatTests: SwiftyGPTTestCase { - - func testDefaultCompletion() throws { - let expectation = expectation(description: "DefaultChatCompletion") - swiftyGPT.chat(messages: [SwiftyGPTChatMessage(role: .user, content: "Hi, how are you?")]) { result in - switch result { - case .success(let response): - XCTAssertGreaterThanOrEqual(response.choices.count, 1) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - - waitForExpectations(timeout: 30, handler: nil) - } - - func testDefaultAsync() async { - let result: Result = await swiftyGPT.chat(messages: [SwiftyGPTChatMessage(role: .user, content: "Hi, how are you?")]) - switch result { - case .success(let response): - XCTAssertGreaterThanOrEqual(response.choices.count, 1) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } - - func testSingleMessageCompletion() { - let expectation = expectation(description: "SingleMessageCompletion") - swiftyGPT.chat(message: SwiftyGPTChatMessage(role: .user, content: "Hi, how are you?")) { result in - switch result { - case .success(let response): - XCTAssertEqual(response.choices.count, 1) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - - waitForExpectations(timeout: 30, handler: nil) - } - - func testSingleMessageAsync() async { - let result: Result = await swiftyGPT.chat(message: SwiftyGPTChatMessage(role: .user, content: "Hi, how are you?")) - switch result { - case .success(let response): - XCTAssertEqual(response.choices.count, 1) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } - - func testStringCompletion() { - let expectation = expectation(description: "StringCompletion") - swiftyGPT.chat(messages: ["Hi how are you ?", "I'm SwiftyGPT"]) { result in - switch result { - case .success(let response): - XCTAssertNotEqual(response, "") - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - waitForExpectations(timeout: 30, handler: nil) - } - - func testStringAsync() async { - let result: Result = await swiftyGPT.chat(messages: ["Hi how are you ?", "I'm SwiftyGPT"]) - switch result { - case .success(let response): - XCTAssertNotEqual(response, "") - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } - - func testSingleStringCompletion() { - let expectation = expectation(description: "SingleStringCompletion") - swiftyGPT.chat(message: "Hi how are you ?") { result in - switch result { - case .success(let response): - XCTAssertNotEqual(response, "") - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - waitForExpectations(timeout: 30, handler: nil) - } - - func testSingleStringAsync() async { - let result: Result = await swiftyGPT.chat(message: "Hi how are you ?") - switch result { - case .success(let response): - XCTAssertNotEqual(response, "") - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } -} diff --git a/Tests/SwiftyGPTTests/SwiftyGPTCompletionTests.swift b/Tests/SwiftyGPTTests/SwiftyGPTCompletionTests.swift deleted file mode 100644 index 9682033..0000000 --- a/Tests/SwiftyGPTTests/SwiftyGPTCompletionTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// SwiftyGPTCompletionTests.swift -// -// -// Created by Antonio Guerra on 12/04/23. -// - -import XCTest -@testable import SwiftyGPT - -final class SwiftyGPTCompletionTests: SwiftyGPTTestCase { - - func testDefaultCompletion() throws { - let expectation = expectation(description: "DefaultCompletion") - swiftyGPT.completion(prompt: "Say \"Hello\" in italian") { result in - switch result { - case .success(let response): - XCTAssertGreaterThanOrEqual(response.choices.count, 1) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - - waitForExpectations(timeout: 30, handler: nil) - } - - func testDefaultAsync() async throws { - let result: Result = await swiftyGPT.completion(prompt: "Say \"Hello\" in italian") - switch result { - case .success(let response): - XCTAssertGreaterThanOrEqual(response.choices.count, 1) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } -} diff --git a/Tests/SwiftyGPTTests/SwiftyGPTErrorTests.swift b/Tests/SwiftyGPTTests/SwiftyGPTErrorTests.swift deleted file mode 100644 index d897560..0000000 --- a/Tests/SwiftyGPTTests/SwiftyGPTErrorTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// SwiftyGPTErrorTests.swift -// -// -// Created by Antonio Guerra on 29/03/23. -// - -import XCTest -@testable import SwiftyGPT - -final class SwiftyGPTErrorTests: XCTestCase { - - var jsonString: String! - - override func setUpWithError() throws { - try super.setUpWithError() - jsonString = """ - { - "error": { - "message": "This model's maximum context length is 4097 tokens, however you requested 4451 tokens (357 in your prompt; 4094 for the completion). Please reduce your prompt; or completion length.", - "type": "invalid_request_error", - "param": null, - "code": null - } - } - """ - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - jsonString = nil - } - - func testDecode() throws { - let data = jsonString.data(using: .utf8) - XCTAssertNotNil(data) - let error = try JSONDecoder().decode(SwiftyGPTError.self, from: data!) - XCTAssertNotNil(error) - } -} diff --git a/Tests/SwiftyGPTTests/SwiftyGPTImageTests.swift b/Tests/SwiftyGPTTests/SwiftyGPTImageTests.swift deleted file mode 100644 index f618483..0000000 --- a/Tests/SwiftyGPTTests/SwiftyGPTImageTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// SwiftyGPTImageTests.swift -// -// -// Created by Antonio Guerra on 30/03/23. -// - -import XCTest -@testable import SwiftyGPT - -final class SwiftyGPTImageTests: SwiftyGPTTestCase { - - #if canImport(UIKit) - func testDefaultCompletion() throws { - let expectation = expectation(description: "DefaultImageCompletion") - swiftyGPT.image(prompt: "Draw an unicorn", choices: 2, size: .x256) { result in - switch result { - case .success(let images): - XCTAssertEqual(images.count, 2) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - waitForExpectations(timeout: 30, handler: nil) - } - - func testDefaultAsync() async throws { - let result: Result<[UIImage], Error> = await swiftyGPT.image(prompt: "Draw an unicorn", choices: 2, size: .x256) - switch result { - case .success(let images): - XCTAssertEqual(images.count, 2) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } - - func testSingleCompletion() throws { - let expectation = expectation(description: "SingleImageCompletion") - swiftyGPT.image(prompt: "Draw an unicorn", size: .x256) { result in - switch result { - case .success(let image): - XCTAssertNotNil(image) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - waitForExpectations(timeout: 30, handler: nil) - } - - func testSingleAsync() async throws { - let result: Result = await swiftyGPT.image(prompt: "Draw an unicorn", size: .x256) - switch result { - case .success(let image): - XCTAssertNotNil(image) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } - #endif -} diff --git a/Tests/SwiftyGPTTests/SwiftyGPTSentimentTests.swift b/Tests/SwiftyGPTTests/SwiftyGPTSentimentTests.swift deleted file mode 100644 index 760a3b3..0000000 --- a/Tests/SwiftyGPTTests/SwiftyGPTSentimentTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// SwiftyGPTSentimentTests.swift -// -// -// Created by Antonio Guerra on 13/04/23. -// - -import XCTest -@testable import SwiftyGPT - -final class SwiftyGPTSentimentTests: SwiftyGPTTestCase { - - func testDefaultCompletion() throws { - let expectation = expectation(description: "DefaultCompletion") - swiftyGPT.sentiment(text: "I loved the new Batman movie!", language: .english) { result in - switch result { - case .success(let response): - XCTAssertEqual(response, .positive) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - waitForExpectations(timeout: 30, handler: nil) - } - - func testDefaultAsync() async throws { - let result: Result = await swiftyGPT.sentiment(text: "I loved the new Batman movie!", language: .english) - switch result { - case .success(let response): - XCTAssertEqual(response, .positive) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } -} diff --git a/Tests/SwiftyGPTTests/SwiftyGPTSummaryTests.swift b/Tests/SwiftyGPTTests/SwiftyGPTSummaryTests.swift deleted file mode 100644 index b98735e..0000000 --- a/Tests/SwiftyGPTTests/SwiftyGPTSummaryTests.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// SwiftyGPTSummaryTests.swift -// -// -// Created by Antonio Guerra on 12/04/23. -// - -import XCTest -@testable import SwiftyGPT - -final class SwiftyGPTSummaryTests: SwiftyGPTTestCase { - - func testDefaultCompletion() throws { - let expectation = expectation(description: "DefaultCompletion") - - let text = """ - La mia istruttrice di nuoto Elke W. aveva un cane barbone bianco, piuttosto grande per la sua razza, di nome Martino. Lei stessa ammetteva che era un cane stupido e fifone, pieno di difetti che lei amava elencare anche a interlocutori occasionali o a perfetti sconosciuti che incontrava per strada. Martino aveva paura, tra le altre cose, degli oggetti appuntiti, delle oche (un cane barbone che aveva paura della oche!) e di entrare nei furgoni dalla parte posteriore. In compenso appena poteva si buttava nell’acqua a nuotare e non si faceva scoraggiare dalla stagione avversa e dalla veemenza dei flutti, talché un giorno di marzo fu proprio la padrona a dover entrare nell’acqua gelata fino alla vita e trascinarlo fuori dal mare per il collare, dopo che il povero Martino era stato sul punto di soccombere. Lo coperse di contumelie, un po’ in italiano e un po’ in dialetto, mentre lui uggiolava sotto un massaggio vigoroso a colpi di plaid. - """ - - swiftyGPT.summary(text: text, language: .italian) { result in - switch result { - case .success(let response): - XCTAssertLessThan(response.count, text.count) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - - waitForExpectations(timeout: 30, handler: nil) - } - - func testDefaultAsync() async throws { - let text = """ - La mia istruttrice di nuoto Elke W. aveva un cane barbone bianco, piuttosto grande per la sua razza, di nome Martino. Lei stessa ammetteva che era un cane stupido e fifone, pieno di difetti che lei amava elencare anche a interlocutori occasionali o a perfetti sconosciuti che incontrava per strada. Martino aveva paura, tra le altre cose, degli oggetti appuntiti, delle oche (un cane barbone che aveva paura della oche!) e di entrare nei furgoni dalla parte posteriore. In compenso appena poteva si buttava nell’acqua a nuotare e non si faceva scoraggiare dalla stagione avversa e dalla veemenza dei flutti, talché un giorno di marzo fu proprio la padrona a dover entrare nell’acqua gelata fino alla vita e trascinarlo fuori dal mare per il collare, dopo che il povero Martino era stato sul punto di soccombere. Lo coperse di contumelie, un po’ in italiano e un po’ in dialetto, mentre lui uggiolava sotto un massaggio vigoroso a colpi di plaid. - """ - - let result: Result = await swiftyGPT.summary(text: text, language: .italian) - switch result { - case .success(let response): - XCTAssertLessThan(response.count, text.count) - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } -} diff --git a/Tests/SwiftyGPTTests/SwiftyGPTTokenizerTest.swift b/Tests/SwiftyGPTTests/SwiftyGPTTokenizerTest.swift deleted file mode 100644 index 5f41630..0000000 --- a/Tests/SwiftyGPTTests/SwiftyGPTTokenizerTest.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// SwiftyGPTTokenizerTest.swift -// -// -// Created by Antonio Guerra on 13/04/23. -// - -import XCTest -@testable import SwiftyGPT - -final class SwiftyGPTTokenizerTest: XCTestCase { - var tokenizer: SwiftyGPTTokenizer! - - override func setUpWithError() throws { - try super.setUpWithError() - tokenizer = SwiftyGPTTokenizer() - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - tokenizer = nil - } - - func testTokenize() { - let text = """ - La mia istruttrice di nuoto Elke W. aveva un cane barbone bianco, piuttosto grande per la sua razza, di nome Martino. Lei stessa ammetteva che era un cane stupido e fifone, pieno di difetti che lei amava elencare anche a interlocutori occasionali o a perfetti sconosciuti che incontrava per strada. Martino aveva paura, tra le altre cose, degli oggetti appuntiti, delle oche (un cane barbone che aveva paura della oche!) e di entrare nei furgoni dalla parte posteriore. In compenso appena poteva si buttava nell’acqua a nuotare e non si faceva scoraggiare dalla stagione avversa e dalla veemenza dei flutti, talché un giorno di marzo fu proprio la padrona a dover entrare nell’acqua gelata fino alla vita e trascinarlo fuori dal mare per il collare, dopo che il povero Martino era stato sul punto di soccombere. Lo coperse di contumelie, un po’ in italiano e un po’ in dialetto, mentre lui uggiolava sotto un massaggio vigoroso a colpi di plaid. - """ - - let count = tokenizer.tokenize(text, language: .italian) - print(count) - XCTAssertGreaterThan(count, 160) - } -} diff --git a/Tests/SwiftyGPTTests/SwiftyGPTTranslationTests.swift b/Tests/SwiftyGPTTests/SwiftyGPTTranslationTests.swift deleted file mode 100644 index 1f3bf3f..0000000 --- a/Tests/SwiftyGPTTests/SwiftyGPTTranslationTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// SwiftyGPTTranslationTests.swift -// -// -// Created by Antonio Guerra on 12/04/23. -// - -import XCTest -@testable import SwiftyGPT - -final class SwiftyGPTTranslationTests: SwiftyGPTTestCase { - - func testDefaultCompletion() throws { - let expectation = expectation(description: "DefaultCompletion") - swiftyGPT.translation(text: "Hi, how are you ?", from: .english, to: .italian) { result in - switch result { - case .success(let response): - XCTAssertEqual(response.lowercased(), "ciao, come stai?") - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - waitForExpectations(timeout: 30, handler: nil) - } - - func testDefaultAsync() async throws { - let result: Result = await swiftyGPT.translation(text: "Hello", from: .english, to: .italian) - switch result { - case .success(let response): - XCTAssertEqual(response.lowercased(), "ciao") - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } -} diff --git a/Tests/SwiftyGPTTests/SwiftyGTPCorrectionTests.swift b/Tests/SwiftyGPTTests/SwiftyGTPCorrectionTests.swift deleted file mode 100644 index 3975fd1..0000000 --- a/Tests/SwiftyGPTTests/SwiftyGTPCorrectionTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// SwiftyGTPCorrectionTests.swift -// -// -// Created by Antonio Guerra on 13/04/23. -// - -import XCTest -@testable import SwiftyGPT - -final class SwiftyGTPCorrectionTests: SwiftyGPTTestCase { - - func testDefaultCompletion() throws { - let expectation = expectation(description: "DefaultCompletion") - swiftyGPT.correction(text: "She no went to the market", language: .english) { result in - switch result { - case .success(let response): - XCTAssertEqual(response.lowercased(), "she did not go to the market.") - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - expectation.fulfill() - } - waitForExpectations(timeout: 30, handler: nil) - } - - func testDefaultAsync() async throws { - let result: Result = await swiftyGPT.correction(text: "She no went to the market", language: .english) - switch result { - case .success(let response): - XCTAssertEqual(response.lowercased(), "she did not go to the market.") - case .failure(let error): - if let error = error as? SwiftyGPTError { - XCTFail(error.message) - } else { - XCTFail(error.localizedDescription) - } - } - } -} diff --git a/Tests/SwiftyGPTTests/Utils/OpenAI-Info.plist b/Tests/SwiftyGPTTests/Utils/OpenAI-Info.plist deleted file mode 100644 index 8decbec..0000000 --- a/Tests/SwiftyGPTTests/Utils/OpenAI-Info.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - API_KEY - - - diff --git a/Tests/SwiftyGPTTests/Utils/SwiftyGPTSecureTest.swift b/Tests/SwiftyGPTTests/Utils/SwiftyGPTSecureTest.swift deleted file mode 100644 index a55bb1e..0000000 --- a/Tests/SwiftyGPTTests/Utils/SwiftyGPTSecureTest.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// SwiftyGPTSecureTest.swift -// -// -// Created by Antonio Guerra on 30/03/23. -// - -import Foundation - -protocol SwiftyGPTSecureTest { - var apiKey: String { get } -} - -extension SwiftyGPTSecureTest { - var apiKey: String { - get { - guard let filePath = Bundle.module.path(forResource: "OpenAI-Info", ofType: "plist"), let plist = NSDictionary(contentsOfFile: filePath), let apiKey = plist.value(forKey: "API_KEY") as? String else { - return "" - } - return apiKey - } - } -} diff --git a/Tests/SwiftyGPTTests/Utils/SwiftyGPTTestCase.swift b/Tests/SwiftyGPTTests/Utils/SwiftyGPTTestCase.swift deleted file mode 100644 index 4311999..0000000 --- a/Tests/SwiftyGPTTests/Utils/SwiftyGPTTestCase.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// SwiftyGPTTestCase.swift -// -// -// Created by Antonio Guerra on 12/04/23. -// - -import XCTest -@testable import SwiftyGPT - -class SwiftyGPTTestCase: XCTestCase, SwiftyGPTSecureTest { - - var swiftyGPT: SwiftyGPT! - - override func setUpWithError() throws { - try super.setUpWithError() - swiftyGPT = SwiftyGPT(apiKey: apiKey) - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - swiftyGPT = nil - } -}