├── .github ├── social preview │ ├── CleverBird_social_preview.jpg │ └── CleverBird_social_preview.pxd └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── CleverBird │ ├── CleverBirdError.swift │ ├── OpenAIAPIConnection.swift │ ├── chat │ ├── ChatCompletionRequestParameters.swift │ ├── ChatCompletionResponse.swift │ ├── ChatMessage.swift │ ├── ChatModel.swift │ ├── ChatThread+complete.swift │ ├── ChatThread+tokenCount.swift │ ├── ChatThread.swift │ ├── Function.swift │ ├── FunctionCall.swift │ ├── FunctionCallMode.swift │ ├── FunctionRegistry.swift │ ├── JSONType.swift │ ├── JSONValue.swift │ ├── MessageContent.swift │ ├── OpenAIAPIConnection+createChatCompletionRequest.swift │ ├── Penalty.swift │ ├── Percentage.swift │ └── streaming │ │ ├── ChatStreamedResponseChunk.swift │ │ ├── ChatThread+withStreaming.swift │ │ ├── OpenAIAPIConnection+createChatCompletionAsyncByteStream.swift │ │ ├── StreamOptions.swift │ │ ├── StreamableChatThread+complete.swift │ │ └── StreamableChatThread.swift │ ├── embeddings │ ├── EmbeddedDocumentStore+persistence.swift │ ├── EmbeddedDocumentStore.SimilarityMetric.swift │ ├── EmbeddedDocumentStore.swift │ ├── EmbeddingModel.swift │ ├── EmbeddingRequestParameters.swift │ ├── EmbeddingResponse.swift │ ├── OpenAIAPIConnection+createEmbeddingRequest.swift │ └── typealiases.swift │ └── tokenization │ ├── Pattern.swift │ ├── Token.swift │ ├── TokenEncoder.swift │ └── resources │ ├── gpt3-encoder.json │ └── gpt3-vocab.bpe └── Tests └── CleverBirdTests ├── MessageContentTests.swift ├── MessageEncodingTests.swift ├── OpenAIChatThreadTests.swift └── TokenEncoderTests.swift /.github/social preview/CleverBird_social_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btfranklin/CleverBird/8b1fa839e9eb92bc889a42a7b887375805bb5064/.github/social preview/CleverBird_social_preview.jpg -------------------------------------------------------------------------------- /.github/social preview/CleverBird_social_preview.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btfranklin/CleverBird/8b1fa839e9eb92bc889a42a7b887375805bb5064/.github/social preview/CleverBird_social_preview.pxd -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | /*.jpg 11 | /*.png 12 | /*.pxd 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 B.T. Franklin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "get", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/kean/Get", 7 | "state" : { 8 | "revision" : "12830cc64f31789ae6f4352d2d51d03a25fc3741", 9 | "version" : "2.1.6" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CleverBird", 8 | platforms: [ 9 | .macOS(.v12), .iOS(.v15), .tvOS(.v16), .watchOS(.v9) 10 | ], 11 | products: [ 12 | .library( 13 | name: "CleverBird", 14 | targets: ["CleverBird"]), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/kean/Get", from: "2.1.6") 18 | ], 19 | targets: [ 20 | .target( 21 | name: "CleverBird", 22 | dependencies: [ 23 | .product(name: "Get", package: "Get") 24 | ], 25 | resources: [ 26 | .process("tokenization/resources/gpt3-encoder.json"), 27 | .process("tokenization/resources/gpt3-vocab.bpe"), 28 | ]), 29 | .testTarget( 30 | name: "CleverBirdTests", 31 | dependencies: ["CleverBird"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CleverBird 2 | 3 | ![CleverBird banner](https://raw.githubusercontent.com/btfranklin/CleverBird/main/.github/social%20preview/CleverBird_social_preview.jpg "CleverBird: Easily connect your Swift application to OpenAI's chat endpoints with a superior, best-in-class DX. 100% open source, 100% free.") 4 | 5 | [![Platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbtfranklin%2FCleverBird%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/btfranklin/CleverBird) 6 | [![Swift versions](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbtfranklin%2FCleverBird%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/btfranklin/CleverBird) 7 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/btfranklin/CleverBird/blob/main/LICENSE) 8 | [![Swift Package Manager compatible](https://img.shields.io/badge/SPM-compatible-brightgreen.svg?style=flat&colorA=28a745&&colorB=4E4E4E)](https://github.com/apple/swift-package-manager) 9 | [![GitHub tag](https://img.shields.io/github/tag/btfranklin/CleverBird.svg)](https://github.com/btfranklin/CleverBird) 10 | [![build](https://github.com/btfranklin/CleverBird/actions/workflows/build.yml/badge.svg)](https://github.com/btfranklin/CleverBird/actions/workflows/build.yml) 11 | 12 | `CleverBird` is a Swift Package that provides a convenient way to interact with OpenAI's chat APIs and perform various tasks, including token counting and encoding. The package is designed to deliver a superior Developer Experience (DX) by making the chat thread the center of the interactions. 13 | 14 | `CleverBird` includes support for document embeddings and similarity queries. This makes it a versatile tool for a broad range of applications, especially cases where chat prompts need enhanced contextual memory. 15 | 16 | `CleverBird` is focused narrowly on chat-based interactions, and making them awesome. 17 | 18 | Please note that `CleverBird` is an *unofficial* package, not provided by OpenAI itself. 19 | 20 | ## Features 21 | 22 | ### Core Features 23 | 24 | - Asynchronous API calls with Swift's async/await syntax 25 | - Streamed responses for real-time generated content 26 | - Built-in token counting for usage limit calculations 27 | 28 | ### Specialized Features 29 | 30 | - Token Encoding: Facilitates token counting and encoding through the `TokenEncoder` class. 31 | - Document Embedding and Similarity Queries: Utilize the `EmbeddedDocumentStore` class for managing and querying document similarities. 32 | 33 | ## Usage Instructions 34 | 35 | Import the `CleverBird` package: 36 | 37 | ```swift 38 | import CleverBird 39 | ``` 40 | 41 | Initialize an `OpenAIAPIConnection` with your API key. Please note that API keys should always be loaded from environment variables, and not hard-coded into your source. After you have loaded your API key, pass it to the initializer of the connection: 42 | 43 | ```swift 44 | let openAIAPIConnection = OpenAIAPIConnection(apiKey: ) 45 | ``` 46 | 47 | Create a `ChatThread` instance and add system, user, or assistant messages to the chat thread: 48 | 49 | ```swift 50 | let chatThread = ChatThread() 51 | .addSystemMessage(content: "You are a helpful assistant.") 52 | .addUserMessage(content: "Who won the world series in 2020?") 53 | ``` 54 | 55 | Generate a completion using the chat thread and passing the API connection: 56 | 57 | ```swift 58 | let completion = try await chatThread.complete(using: openAIAPIConnection) 59 | ``` 60 | 61 | The `complete(using:)` method also includes various optional parameters: 62 | 63 | ```swift 64 | let completion = chatThread.complete( 65 | using: openAIAPIConnection, 66 | model: .gpt4o, 67 | temperature: 0.7, 68 | maxTokens: 500 69 | ) 70 | ``` 71 | 72 | In the example above, we created a completion using a specific model, temperature, and maximum number of tokens. All parameters except `connection` are optional. The full list of parameters is as follows: 73 | 74 | - `connection`: The API connection object (required). 75 | - `model`: The model to use for the completion. 76 | - `temperature`: Controls randomness. Higher values (up to 1) generate more random outputs, while lower values generate more deterministic outputs. 77 | - `topP`: The nucleus sampling parameter. It specifies the probability mass to cover with the prediction. 78 | - `stop`: An array of strings. The model will stop generating when it encounters any of these strings. 79 | - `maxTokens`: The maximum number of tokens to generate. 80 | - `presencePenalty`: A penalty for using tokens that have already been used. 81 | - `frequencyPenalty`: A penalty for using frequent tokens. 82 | - `functions`: The tool functions (aka "actions") to make available to the model. 83 | - `functionCallMode`: The function calling mode: `.auto`, `.none`, or `.specific`. 84 | 85 | The response messages are automatically appended onto the thread, so 86 | you can continue interacting with it by just adding new user messages 87 | and requesting additional completions. 88 | 89 | You can customize each call to `complete(using:)` with different values for the same parameters on subsequent calls in the same thread, if you want: 90 | 91 | ```swift 92 | let completion = try await chatThread.complete( 93 | using: openAIAPIConnection, 94 | model: .gpt35Turbo, 95 | temperature: 0.5, 96 | maxTokens: 300 97 | ) 98 | ``` 99 | 100 | Generate a completion with streaming using the streaming version of a chat thread: 101 | 102 | ```swift 103 | let chatThread = ChatThread().withStreaming() 104 | let completionStream = try await chatThread.complete(using: openAIAPIConnection) 105 | for try await messageChunk in completionStream { 106 | print("Received message chunk: \(messageChunk)") 107 | } 108 | ``` 109 | 110 | Just like with the non-streamed completion, the message will be automatically 111 | appended onto the thread after it has finished streaming, but the stream 112 | allows you to see it as it's coming through. 113 | 114 | To include usage (the number of tokens used in the prompt and completion), add set `streamOptions` in the `complete` method. The usage is available as a property of `StreamableChatThread` after the stream has completed. 115 | 116 | ```swift 117 | let chatThread = ChatThread().withStreaming() 118 | let completionStream = try await chatThread.complete(using: openAIAPIConnection, includeUsage: true) 119 | for try await messageChunk in completionStream { 120 | print("Received message chunk: \(messageChunk)") 121 | } 122 | if let usage = completionStream.usage { 123 | print("Usage: \(usage)") 124 | } 125 | ``` 126 | 127 | Calculate the token count for messages in the chat thread: 128 | 129 | ```swift 130 | let tokenCount = try chatThread.tokenCount() 131 | ``` 132 | 133 | If you need to count tokens or encode/decode text outside of a chat thread, 134 | use the `TokenEncoder` class: 135 | 136 | ```swift 137 | let tokenEncoder = try TokenEncoder(model: .gpt3) 138 | let encodedTokens = try tokenEncoder.encode(text: "Hello, world!") 139 | let decodedText = try tokenEncoder.decode(tokens: encodedTokens) 140 | ``` 141 | 142 | ## Using Functions 143 | 144 | `CleverBird` supports Function Calls. This powerful feature allows developers to define their own custom commands, making it easier to control the behavior of the AI. Function Calls can be included in the `ChatThread` and used in the `complete()` method. 145 | 146 | First, define your function parameters and the function itself. The `Function.Parameters` class is used to set the properties and required parameters of your function. 147 | 148 | ```swift 149 | let getCurrentWeatherParameters = Function.Parameters( 150 | properties: [ 151 | "location": Function.Parameters.Property(type: .string, 152 | description: "The city and state, e.g. San Francisco, CA"), 153 | "format": Function.Parameters.Property(type: .string, 154 | description: "The temperature unit to use. Infer this from the user's location.", 155 | enumCases: ["celsius", "fahrenheit"]) 156 | ], 157 | required: ["location", "format"]) 158 | 159 | let getCurrentWeather = Function(name: "get_current_weather", 160 | description: "Get the current weather", 161 | parameters: getCurrentWeatherParameters) 162 | ``` 163 | 164 | Then, initialize your `ChatThread` with your API connection and an array of functions: 165 | 166 | ```swift 167 | let chatThread = ChatThread(functions: [getCurrentWeather]) 168 | .addSystemMessage(content: "You are a helpful assistant.") 169 | ``` 170 | 171 | Finally, call the `complete(using:)` function to generate a response. If the assistant needs to perform a function during the conversation, it will use the function definitions you provided. 172 | 173 | Please note that functions are only supported in non-streaming completions at this time. 174 | 175 | ## Using Embeddings 176 | 177 | The `EmbeddedDocumentStore` class provides a convenient way to manage and query a collection of documents based on their similarity. This class allows you to: 178 | 179 | - Add documents to an internal store. 180 | - Generate embeddings for those documents using a specified model. 181 | - Query the store for similar documents to a given input document. 182 | 183 | First, add an instance of the `EmbeddedDocumentStore` to your code: 184 | 185 | ```swift 186 | let openAIAPIConnection = OpenAIAPIConnection(apiKey: "your_api_key_here") 187 | let embeddedDocumentStore = EmbeddedDocumentStore(connection: connection) 188 | ``` 189 | 190 | You can add a single document or a batch of documents to the store. 191 | 192 | ```swift 193 | let singleDocument = "My single document" 194 | try await embeddedDocumentStore.embedAndStore(singleDocument) 195 | 196 | let documentCollection = ["First document", "Second document", "Third document"] 197 | try await embeddedDocumentStore.embedAndStore(documentCollection) 198 | 199 | ``` 200 | 201 | You can query the store for documents that are similar to an input document. 202 | 203 | ```swift 204 | let similarityResults = try await embeddedDocumentStore.queryDocumentSimilarity("Query text here") 205 | let mostSimilarResult = similarityResults.first?.document ?? "No result returned" 206 | ``` 207 | 208 | The store can be saved to and loaded from a file (represented in JSON format) for persistent storage. 209 | 210 | ```swift 211 | embeddedDocumentStore.save(to: fileURL) 212 | embeddedDocumentStore.load(from: fileURL) 213 | ``` 214 | 215 | ## License 216 | 217 | `CleverBird` was written by B.T. Franklin ([@btfranklin](https://github.com/btfranklin)) from 2023 onward and is licensed under the [MIT](https://opensource.org/licenses/MIT) license. See [LICENSE.md](LICENSE.md). 218 | -------------------------------------------------------------------------------- /Sources/CleverBird/CleverBirdError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum CleverBirdError: Error, Equatable { 4 | case requestFailed(message: String) 5 | case responseParsingFailed(message: String) 6 | case tokenEncoderCreationFailed(message: String) 7 | case tokenEncodingError(message: String) 8 | case invalidMessageContent 9 | case invalidFunctionMessage 10 | case invalidEmbeddingRequest(message: String) 11 | } 12 | -------------------------------------------------------------------------------- /Sources/CleverBird/OpenAIAPIConnection.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 12/23/22 2 | 3 | import Foundation 4 | import Get 5 | 6 | public class OpenAIAPIConnection { 7 | 8 | let apiKey: String 9 | let organization: String? 10 | let project: String? 11 | public let client: APIClient 12 | let requestHeaders: [String:String] 13 | 14 | public init(apiKey: String, 15 | organization: String? = nil, 16 | project: String? = nil, 17 | scheme: String = "https", 18 | host: String = "api.openai.com", 19 | port: Int = 443) { 20 | self.apiKey = apiKey 21 | self.organization = organization 22 | self.project = project 23 | 24 | var urlComponents = URLComponents() 25 | urlComponents.scheme = scheme 26 | urlComponents.host = host 27 | urlComponents.port = port 28 | let openAIAPIURL = urlComponents.url 29 | 30 | let clientConfiguration = APIClient.Configuration(baseURL: openAIAPIURL) 31 | clientConfiguration.encoder.keyEncodingStrategy = .convertToSnakeCase 32 | clientConfiguration.decoder.keyDecodingStrategy = .convertFromSnakeCase 33 | 34 | self.client = APIClient(configuration: clientConfiguration) 35 | 36 | var requestHeaders = [ 37 | "Content-Type": "application/json", 38 | "Authorization": "Bearer \(apiKey)" 39 | ] 40 | if let organization { 41 | requestHeaders["OpenAI-Organization"] = organization 42 | } 43 | if let project { 44 | requestHeaders["OpenAI-Project"] = project 45 | } 46 | self.requestHeaders = requestHeaders 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/ChatCompletionRequestParameters.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 4/15/23 2 | 3 | public struct ChatCompletionRequestParameters: Codable { 4 | public let model: ChatModel 5 | public let temperature: Percentage 6 | public let topP: Percentage? 7 | public let stream: Bool 8 | public let stop: [String]? 9 | public let maxTokens: Int? 10 | public let presencePenalty: Penalty? 11 | public let frequencyPenalty: Penalty? 12 | public let user: String? 13 | public let messages: [ChatMessage] 14 | public let functions: [Function]? 15 | public let functionCallMode: FunctionCallMode? 16 | public let streamOptions: StreamOptions? 17 | 18 | public init(model: ChatModel, 19 | temperature: Percentage, 20 | topP: Percentage? = nil, 21 | stream: Bool = false, 22 | stop: [String]? = nil, 23 | maxTokens: Int? = nil, 24 | presencePenalty: Penalty? = nil, 25 | frequencyPenalty: Penalty? = nil, 26 | user: String? = nil, 27 | messages: [ChatMessage], 28 | functions: [Function]? = nil, 29 | functionCallMode: FunctionCallMode? = nil, 30 | streamOptions: StreamOptions? = nil) { 31 | self.model = model 32 | self.temperature = temperature 33 | self.topP = topP 34 | self.stream = stream 35 | self.stop = stop 36 | self.maxTokens = maxTokens 37 | self.presencePenalty = presencePenalty 38 | self.frequencyPenalty = frequencyPenalty 39 | self.user = user 40 | self.messages = messages 41 | self.functions = functions 42 | self.functionCallMode = functionCallMode 43 | self.streamOptions = streamOptions 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/ChatCompletionResponse.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 5/5/23 2 | 3 | import Foundation 4 | 5 | public struct Usage: Codable { 6 | public let promptTokens: Int 7 | public let completionTokens: Int? 8 | public let totalTokens: Int 9 | } 10 | 11 | struct ChatCompletionResponse: Codable, Identifiable { 12 | 13 | struct Choice: Codable { 14 | 15 | enum FinishReason: String, Codable { 16 | case stop 17 | case maxTokens 18 | case functionCall = "function_call" 19 | } 20 | 21 | let message: ChatMessage 22 | let finishReason: FinishReason 23 | let functionCall: FunctionCall? 24 | 25 | enum CodingKeys: String, CodingKey { 26 | case message 27 | case finishReason 28 | case functionCall 29 | } 30 | } 31 | 32 | let choices: [Choice] 33 | let usage: Usage 34 | let id: String 35 | 36 | enum CodingKeys: String, CodingKey { 37 | case choices 38 | case usage 39 | case id 40 | } 41 | 42 | init(from decoder: Decoder) throws { 43 | let container = try decoder.container(keyedBy: CodingKeys.self) 44 | 45 | self.id = try container.decode(String.self, forKey: .id) 46 | 47 | var choicesContainer = try container.nestedUnkeyedContainer(forKey: .choices) 48 | var choices: [Choice] = [] 49 | while !choicesContainer.isAtEnd { 50 | let choiceContainer = try choicesContainer.nestedContainer(keyedBy: Choice.CodingKeys.self) 51 | 52 | var message = try choiceContainer.decode(ChatMessage.self, forKey: .message) 53 | message.id = id 54 | 55 | let finishReason = try choiceContainer.decode(Choice.FinishReason.self, forKey: .finishReason) 56 | 57 | let functionCall = try choiceContainer.decodeIfPresent(FunctionCall.self, forKey: .functionCall) 58 | 59 | let choice = Choice(message: message, finishReason: finishReason, functionCall: functionCall) 60 | choices.append(choice) 61 | } 62 | self.choices = choices 63 | 64 | self.usage = try container.decode(Usage.self, forKey: .usage) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/ChatMessage.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 5/5/23 2 | 3 | import Foundation 4 | 5 | public struct ChatMessage: Codable, Identifiable { 6 | 7 | public enum Role: String, Codable { 8 | case system 9 | case user 10 | case assistant 11 | case function 12 | } 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case role 16 | case content 17 | case functionCall 18 | case name 19 | } 20 | 21 | public var id: String 22 | 23 | /// The role of the message's author 24 | public let role: Role 25 | 26 | /// The contents of the message. `content` is required for all messages except assistant messages with function calls. 27 | public let content: Content? 28 | 29 | /// The name and arguments of a function that should be called, as generated by the model. 30 | public let functionCall: FunctionCall? 31 | 32 | /// The `name` of the author of this message. `name` is required if `role` is `function`, and it should be the name of the function whose response is in the `content`. 33 | public let name: String? 34 | 35 | public init(role: Role, 36 | content: String? = nil, 37 | id: String? = nil, 38 | functionCall: FunctionCall? = nil) throws { 39 | try self.init(role: role, media: content != nil ? .text(content!) : nil, id: id, functionCall: functionCall) 40 | } 41 | 42 | public init(role: Role, 43 | media: ChatMessage.Content?, 44 | id: String? = nil, 45 | functionCall: FunctionCall? = nil) throws { 46 | 47 | // Validation: Content is required for all messages except assistant messages with function calls. 48 | if media == nil && !(role == .assistant && functionCall != nil) { 49 | throw CleverBirdError.invalidMessageContent 50 | } 51 | 52 | self.role = role 53 | self.content = media 54 | self.name = functionCall?.name 55 | if role == .function { 56 | // If the role is "function" I need to set functionCall to nil, otherwise this will 57 | // be encoded into the message which leads to an error. 58 | self.functionCall = nil 59 | } else { 60 | self.functionCall = functionCall 61 | } 62 | 63 | if let id = id { 64 | self.id = id 65 | } else { 66 | var hasher = Hasher() 67 | hasher.combine(self.role) 68 | if let content { 69 | hasher.combine(content) 70 | } 71 | let hashValue = abs(hasher.finalize()) 72 | let timestamp = Int(Date.now.timeIntervalSince1970*10000) 73 | 74 | self.id = "chatmsg-\(hashValue)-\(timestamp)" 75 | } 76 | } 77 | 78 | public init(from decoder: Decoder) throws { 79 | let container = try decoder.container(keyedBy: CodingKeys.self) 80 | self.role = try container.decode(Role.self, forKey: .role) 81 | self.content = try container.decodeIfPresent(Content.self, forKey: .content) 82 | self.functionCall = try container.decodeIfPresent(FunctionCall.self, forKey: .functionCall) 83 | self.name = try container.decodeIfPresent(String.self, forKey: .name) 84 | self.id = "pending" 85 | } 86 | 87 | public func encode(to encoder: Encoder) throws { 88 | var container = encoder.container(keyedBy: CodingKeys.self) 89 | try container.encode(self.role, forKey: .role) 90 | // The content field needs to be present even if it is nil, in this case encode it to Null 91 | try container.encode(self.content, forKey: .content) 92 | try container.encodeIfPresent(self.functionCall, forKey: .functionCall) 93 | try container.encodeIfPresent(self.name, forKey: .name) 94 | } 95 | } 96 | 97 | extension ChatMessage: Equatable { 98 | public static func == (lhs: ChatMessage, rhs: ChatMessage) -> Bool { 99 | return lhs.id == rhs.id 100 | && lhs.role == rhs.role 101 | && lhs.content == rhs.content 102 | } 103 | } 104 | 105 | extension ChatMessage { 106 | 107 | public enum Content: Codable, Equatable, CustomStringConvertible, Hashable { 108 | 109 | case text(String) 110 | case media([MessageContent]) 111 | 112 | public init(from decoder: Decoder) throws { 113 | let container = try decoder.singleValueContainer() 114 | if let textContent = try? container.decode(String.self) { 115 | self = .text(textContent) 116 | } else if let chatContents = try? container.decode([MessageContent].self) { 117 | self = .media(chatContents) 118 | } else { 119 | throw DecodingError.typeMismatch(MessageContent.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported type for Content")) 120 | } 121 | } 122 | 123 | public func encode(to encoder: Encoder) throws { 124 | var container = encoder.singleValueContainer() 125 | switch self { 126 | case .text(let text): 127 | try container.encode(text) 128 | case .media(let contents): 129 | try container.encode(contents) 130 | } 131 | } 132 | 133 | public static func == (lhs: Content, rhs: Content) -> Bool { 134 | switch (lhs, rhs) { 135 | case (.text(let leftText), .text(let rightText)): 136 | return leftText == rightText 137 | case (.media(let leftMedia), .media(let rightMedia)): 138 | return leftMedia == rightMedia 139 | default: 140 | return false 141 | } 142 | } 143 | 144 | public var description: String { 145 | switch self { 146 | case .media(let messageContents): 147 | for messageContent in messageContents { 148 | if case .text(let textValue) = messageContent { 149 | return textValue 150 | } 151 | } 152 | return "" 153 | case .text(let text): 154 | return text 155 | } 156 | } 157 | } 158 | } 159 | 160 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/ChatModel.swift: -------------------------------------------------------------------------------- 1 | public enum ChatModel: Codable { 2 | case gpt35Turbo 3 | case gpt4oMini 4 | case gpt4 5 | case gpt4Turbo 6 | case gpt4o 7 | case specific(String) 8 | 9 | public init(from decoder: Decoder) throws { 10 | let container = try decoder.singleValueContainer() 11 | let modelString = try container.decode(String.self) 12 | 13 | switch modelString { 14 | case _ where modelString.starts(with: "gpt-3.5"): 15 | self = .gpt35Turbo 16 | case _ where modelString.starts(with: "gpt-4o-mini"): 17 | self = .gpt4oMini 18 | case _ where modelString.starts(with: "gpt-4o"): 19 | self = .gpt4o 20 | case _ where modelString.starts(with: "gpt-4-turbo"): 21 | self = .gpt4Turbo 22 | case _ where modelString.starts(with: "gpt-4"): 23 | self = .gpt4 24 | default: 25 | self = .specific(modelString) 26 | } 27 | } 28 | 29 | public func encode(to encoder: Encoder) throws { 30 | var container = encoder.singleValueContainer() 31 | let modelString = self.description 32 | 33 | try container.encode(modelString) 34 | } 35 | } 36 | 37 | extension ChatModel: CustomStringConvertible { 38 | public var description: String { 39 | switch self { 40 | case .gpt35Turbo: 41 | return "gpt-3.5-turbo" 42 | case .gpt4oMini: 43 | return "gpt-4o-mini" 44 | case .gpt4o: 45 | return "gpt-4o" 46 | case .gpt4Turbo: 47 | return "gpt-4-turbo" 48 | case .gpt4: 49 | return "gpt-4" 50 | case .specific(let string): 51 | return string 52 | } 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/ChatThread+complete.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 5/5/23 2 | 3 | extension ChatThread { 4 | public func complete(using connection: OpenAIAPIConnection, 5 | model: ChatModel = .gpt4o, 6 | temperature: Percentage = 0.7, 7 | topP: Percentage? = nil, 8 | stop: [String]? = nil, 9 | maxTokens: Int? = nil, 10 | presencePenalty: Penalty? = nil, 11 | frequencyPenalty: Penalty? = nil, 12 | functions: [Function]? = nil, 13 | functionCallMode: FunctionCallMode? = nil) async throws -> ChatMessage { 14 | try await completeIncludeUsage(using: connection, 15 | model: model, 16 | temperature: temperature, 17 | topP: topP, 18 | stop: stop, 19 | maxTokens: maxTokens, 20 | presencePenalty: presencePenalty, 21 | frequencyPenalty: frequencyPenalty, 22 | functions: functions, 23 | functionCallMode: functionCallMode).0 24 | } 25 | 26 | public func completeIncludeUsage(using connection: OpenAIAPIConnection, 27 | model: ChatModel = .gpt4o, 28 | temperature: Percentage = 0.7, 29 | topP: Percentage? = nil, 30 | stop: [String]? = nil, 31 | maxTokens: Int? = nil, 32 | presencePenalty: Penalty? = nil, 33 | frequencyPenalty: Penalty? = nil, 34 | functions: [Function]? = nil, 35 | functionCallMode: FunctionCallMode? = nil) async throws -> (ChatMessage, Usage) { 36 | let requestBody = ChatCompletionRequestParameters( 37 | model: model, 38 | temperature: temperature, 39 | topP: topP, 40 | stop: stop, 41 | maxTokens: maxTokens, 42 | presencePenalty: presencePenalty, 43 | frequencyPenalty: frequencyPenalty, 44 | user: self.user, 45 | messages: self.messages, 46 | functions: functions ?? self.functions, 47 | functionCallMode: functionCallMode 48 | ) 49 | 50 | do { 51 | // Set the functions in the FunctionRegistry before the request 52 | if let functions = requestBody.functions { 53 | FunctionRegistry.shared.setFunctions(functions) 54 | } 55 | 56 | // ...and be sure to clear them at the end. 57 | defer { 58 | FunctionRegistry.shared.clearFunctions() 59 | } 60 | 61 | let request = try await connection.createChatCompletionRequest(for: requestBody) 62 | let response = try await connection.client.send(request) 63 | let completion = response.value 64 | guard let firstChoiceMessage = completion.choices.first?.message else { 65 | throw CleverBirdError.responseParsingFailed(message: "No message choice was available in completion response.") 66 | } 67 | 68 | // Append the response message to the thread 69 | addMessage(firstChoiceMessage) 70 | 71 | return (firstChoiceMessage, completion.usage) 72 | 73 | } catch { 74 | throw CleverBirdError.requestFailed(message: error.localizedDescription) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/ChatThread+tokenCount.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 5/5/23 2 | 3 | import Foundation 4 | 5 | extension ChatThread { 6 | public func tokenCount(using model: ChatModel = .gpt4) throws -> Int { 7 | 8 | let tokenEncoder: TokenEncoder 9 | do { 10 | tokenEncoder = try TokenEncoder() 11 | } catch { 12 | throw CleverBirdError.tokenEncoderCreationFailed(message: error.localizedDescription) 13 | } 14 | 15 | var tokensPerMessage: Int 16 | 17 | switch model { 18 | case .gpt35Turbo: 19 | tokensPerMessage = 4 20 | case .gpt4, .gpt4Turbo: 21 | tokensPerMessage = 3 22 | case .gpt4o, .gpt4oMini: 23 | tokensPerMessage = 3 24 | case .specific(_): 25 | tokensPerMessage = 3 26 | } 27 | 28 | var numTokens = 0 29 | for message in messages { 30 | do { 31 | let roleTokens = try tokenEncoder.encode(text: message.role.rawValue).count 32 | let contentTokens: Int 33 | if let content = message.content { 34 | switch content { 35 | case .text(let text): 36 | contentTokens = try tokenEncoder.encode(text: text).count 37 | case .media(let media): 38 | var count = 0 39 | for medium in media { 40 | switch medium { 41 | case .text(let text): 42 | count += try tokenEncoder.encode(text: text).count 43 | case .imageUrl(let url): 44 | // See https://platform.openai.com/docs/guides/vision/calculating-costs 45 | switch url.detail { 46 | // TODO: calculate real values for auto and high 47 | case .auto: 48 | count += 1105 49 | case .high: 50 | count += 1105 51 | case .low: 52 | count += 85 53 | case .none: 54 | count += 1105 55 | } 56 | } 57 | } 58 | contentTokens = count 59 | } 60 | } else if let functionCall = message.functionCall { 61 | let jsonEncoder = JSONEncoder() 62 | let jsonData = try jsonEncoder.encode(functionCall) 63 | let jsonString = String(data: jsonData, encoding: .utf8)! 64 | contentTokens = try tokenEncoder.encode(text: jsonString).count 65 | } else { 66 | contentTokens = 0 67 | } 68 | 69 | numTokens += roleTokens + contentTokens + tokensPerMessage 70 | } catch { 71 | throw CleverBirdError.tokenEncodingError(message: error.localizedDescription) 72 | } 73 | } 74 | 75 | 76 | numTokens += 3 // every reply is primed with "assistant" 77 | 78 | if let functions { 79 | let jsonEncoder = JSONEncoder() 80 | let jsonData = try jsonEncoder.encode(functions) 81 | let jsonString = String(data: jsonData, encoding: .utf8) 82 | numTokens += try tokenEncoder.encode(text: jsonString!).count 83 | } 84 | 85 | return numTokens 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/ChatThread.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 5/5/23 2 | 3 | public class ChatThread: Codable { 4 | 5 | let user: String? 6 | 7 | var messages: [ChatMessage] = [] 8 | var functions: [Function]? 9 | 10 | public init(user: String? = nil, 11 | functions: [Function]? = nil) { 12 | self.user = user 13 | self.functions = functions 14 | } 15 | 16 | @discardableResult 17 | public func addSystemMessage(_ content: String) -> Self { 18 | do { 19 | try addMessage(ChatMessage(role: .system, content: content)) 20 | } catch { 21 | print(error.localizedDescription) 22 | } 23 | return self 24 | } 25 | 26 | @discardableResult 27 | public func addUserMessage(_ content: String) -> Self { 28 | do { 29 | try addMessage(ChatMessage(role: .user, content: content)) 30 | } catch { 31 | print(error.localizedDescription) 32 | } 33 | return self 34 | } 35 | 36 | @discardableResult 37 | public func addUserMessage(_ media: [MessageContent]) -> Self { 38 | do { 39 | try addMessage(ChatMessage(role: .user, media: .media(media))) 40 | } catch { 41 | print(error.localizedDescription) 42 | } 43 | return self 44 | } 45 | 46 | @discardableResult 47 | public func addAssistantMessage(_ content: String) -> Self { 48 | do { 49 | try addMessage(ChatMessage(role: .assistant, content: content)) 50 | } catch { 51 | print(error.localizedDescription) 52 | } 53 | return self 54 | } 55 | 56 | @discardableResult 57 | public func addMessage(_ message: ChatMessage) -> Self { 58 | messages.append(message) 59 | return self 60 | } 61 | 62 | public func getMessages() -> [ChatMessage] { 63 | messages 64 | } 65 | 66 | public func getNonSystemMessages() -> [ChatMessage] { 67 | messages.filter { $0.role != .system } 68 | } 69 | 70 | @discardableResult 71 | public func setFunctions(_ functions: [Function]?) -> Self { 72 | self.functions = functions 73 | return self 74 | } 75 | 76 | public func getFunctions() -> [Function]? { 77 | functions 78 | } 79 | 80 | @discardableResult 81 | public func addFunctionResponse(_ content: String, for functionCall: FunctionCall) -> ChatThread { 82 | do { 83 | let responseMessage = try ChatMessage(role: .function, 84 | content: content, 85 | functionCall: functionCall) 86 | messages.append(responseMessage) 87 | } catch { 88 | print(error.localizedDescription) 89 | } 90 | return self 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/Function.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 6/14/23 2 | 3 | /// A function that the AI can call 4 | public struct Function: Codable { 5 | 6 | static public let EMPTY_PARAMETERS = Function.Parameters( 7 | properties: [:], 8 | required: [] 9 | ) 10 | 11 | /// The name of the function. This should match with the name used in the chat message when the function is being called. 12 | public let name: String 13 | 14 | /// An optional description of what the function does. 15 | public let description: String? 16 | 17 | /// The parameters of the function 18 | public let parameters: Parameters 19 | 20 | public init(name: String, description: String? = nil, parameters: Parameters = EMPTY_PARAMETERS) { 21 | self.name = name 22 | self.description = description 23 | self.parameters = parameters 24 | } 25 | 26 | public struct Parameters: Codable { 27 | 28 | /// Parameter names mapped to their descriptions 29 | public let properties: [String: Property] 30 | 31 | /// Parameter names that are required for the function. If a parameter is not in this list, it is considered optional. 32 | public let required: [String] 33 | 34 | enum CodingKeys: String, CodingKey { 35 | case type 36 | case properties 37 | case required 38 | } 39 | 40 | public init(properties: [String:Property], required: [String]) { 41 | self.properties = properties 42 | self.required = required 43 | } 44 | 45 | public init(from decoder: Decoder) throws { 46 | let container = try decoder.container(keyedBy: CodingKeys.self) 47 | let type = try container.decode(String.self, forKey: .type) 48 | guard type == "object" else { 49 | throw DecodingError.dataCorruptedError( 50 | forKey: .type, 51 | in: container, 52 | debugDescription: "Expected 'object' for 'type' key" 53 | ) 54 | } 55 | properties = try container.decode([String: Property].self, forKey: .properties) 56 | required = try container.decode([String].self, forKey: .required) 57 | } 58 | 59 | public func encode(to encoder: Encoder) throws { 60 | var container = encoder.container(keyedBy: CodingKeys.self) 61 | try container.encode("object", forKey: .type) 62 | try container.encode(properties, forKey: .properties) 63 | try container.encode(required, forKey: .required) 64 | } 65 | 66 | public struct Property: Codable { 67 | 68 | /// The type of the parameter. It could be "string", "integer", etc., based on the JSON Schema types. 69 | public let type: JSONType 70 | 71 | /// The purpose of the parameter. This could help users understand what kind of input is expected. 72 | public let description: String? 73 | 74 | /// The allowed values for the parameter if it's an enum. The AI should choose from these values when invoking the function. 75 | public let enumCases: [String]? 76 | 77 | /// The definition of the individual items in the parameter if it's an array. The AI should populate the array with values based on this when invoking the function. 78 | public let items: ArrayItems? 79 | 80 | enum CodingKeys: String, CodingKey { 81 | case type 82 | case description 83 | case enumCases = "enum" 84 | case items 85 | } 86 | 87 | public init(type: JSONType, 88 | description: String? = nil, 89 | enumCases: [String]? = nil, 90 | items: ArrayItems? = nil) { 91 | self.type = type 92 | self.description = description 93 | self.enumCases = enumCases 94 | 95 | if items != nil && type != .array { 96 | fatalError("Array items can only be provided to properties of type 'array', but type is '\(type)'") 97 | } 98 | self.items = items 99 | } 100 | 101 | public struct ArrayItems: Codable { 102 | 103 | /// The type of the items in the array. It could be "string", "integer", etc., based on the JSON Schema types. 104 | public let type: JSONType 105 | 106 | /// A description of what a single item in the array represents 107 | public let description: String? 108 | 109 | public init(type: JSONType, 110 | description: String? = nil) { 111 | self.type = type 112 | self.description = description 113 | } 114 | 115 | } 116 | } 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/FunctionCall.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 6/15/23 2 | 3 | import Foundation 4 | 5 | public struct FunctionCall: Codable { 6 | public let name: String 7 | public let arguments: [String: JSONValue]? 8 | 9 | enum CodingKeys: String, CodingKey { 10 | case name 11 | case arguments 12 | } 13 | 14 | init(name: String, arguments: [String: JSONValue]? = nil) { 15 | self.name = name 16 | self.arguments = arguments 17 | } 18 | 19 | public init(from decoder: Decoder) throws { 20 | 21 | let container = try decoder.container(keyedBy: CodingKeys.self) 22 | let functionName = try container.decode(String.self, forKey: .name) 23 | 24 | // Find the corresponding Function object for this FunctionCall 25 | let function = FunctionRegistry.shared.getFunction(withName: functionName) 26 | 27 | // Decode the arguments as a JSON string 28 | let argumentsString = try container.decode(String.self, forKey: .arguments) 29 | 30 | if let data = argumentsString.data(using: .utf8) { 31 | 32 | // Parse the JSON string into a dictionary 33 | let argumentsDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 34 | 35 | guard let argumentsDict else { 36 | throw DecodingError.dataCorruptedError(forKey: .arguments, in: container, debugDescription: "Arguments were nil.") 37 | } 38 | 39 | // Initialize an empty dictionary to hold the decoded arguments 40 | var decodedArguments: [String: JSONValue] = [:] 41 | 42 | // Decode each argument 43 | for (argName, argValue) in argumentsDict { 44 | guard let argType = function?.parameters.properties[argName]?.type else { 45 | throw DecodingError.dataCorruptedError(forKey: .arguments, 46 | in: container, 47 | debugDescription: "No type provided for \(argName). Decoding not possible.") 48 | } 49 | let value = try JSONValue.createFromValue(argValue, ofType: argType) 50 | 51 | decodedArguments[argName] = value 52 | } 53 | 54 | arguments = decodedArguments 55 | } else { 56 | arguments = nil 57 | } 58 | 59 | self.name = functionName 60 | } 61 | 62 | public func encode(to encoder: Encoder) throws { 63 | var container = encoder.container(keyedBy: CodingKeys.self) 64 | try container.encode(self.name, forKey: .name) 65 | let argumentsAsString = try String(data: JSONEncoder().encode(self.arguments), encoding: .utf8) ?? "" 66 | try container.encode(argumentsAsString, forKey: .arguments) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/FunctionCallMode.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 6/14/23 2 | 3 | public enum FunctionCallMode: Codable { 4 | case auto 5 | case none 6 | case specific(FunctionName) 7 | 8 | public struct FunctionName: Codable { 9 | let name: String 10 | } 11 | 12 | public init(from decoder: Decoder) throws { 13 | let container = try decoder.singleValueContainer() 14 | if let string = try? container.decode(String.self) { 15 | switch string { 16 | case "auto": 17 | self = .auto 18 | case "none": 19 | self = .none 20 | default: 21 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid value for FunctionCallMode") 22 | } 23 | } else { 24 | self = .specific(try FunctionName(from: decoder)) 25 | } 26 | } 27 | 28 | public func encode(to encoder: Encoder) throws { 29 | var container = encoder.singleValueContainer() 30 | switch self { 31 | case .auto: 32 | try container.encode("auto") 33 | case .none: 34 | try container.encode("none") 35 | case .specific(let functionName): 36 | try functionName.encode(to: encoder) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/FunctionRegistry.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 6/16/23 2 | 3 | class FunctionRegistry { 4 | 5 | static let shared: FunctionRegistry = FunctionRegistry() 6 | 7 | private var functionsByName: [String:Function] = [:] 8 | 9 | private init() { 10 | // Private to prevent non-singleton use 11 | } 12 | 13 | func setFunctions(_ functions: [Function]) { 14 | functions.forEach { self.functionsByName[$0.name] = $0 } 15 | } 16 | 17 | func clearFunctions() { 18 | self.functionsByName.removeAll() 19 | } 20 | 21 | func getFunction(withName name: String) -> Function? { 22 | functionsByName[name] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/JSONType.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 6/15/23 2 | 3 | public enum JSONType: String, Codable { 4 | case null 5 | case string 6 | case boolean 7 | case number 8 | case integer 9 | case array 10 | } 11 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/JSONValue.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 6/15/23 2 | 3 | import Foundation 4 | 5 | public enum JSONValue: Codable { 6 | case null 7 | case string(String) 8 | case boolean(Bool) 9 | case number(Double) 10 | case integer(Int) 11 | case array([JSONValue]) 12 | 13 | var typeDescription: String { 14 | switch self { 15 | case .null: 16 | return "null" 17 | case .string(_): 18 | return "string" 19 | case .boolean(_): 20 | return "boolean" 21 | case .number(_): 22 | return "number" 23 | case .integer(_): 24 | return "integer" 25 | case .array(_): 26 | return "array" 27 | } 28 | } 29 | 30 | public init(from decoder: Decoder) throws { 31 | let container = try decoder.singleValueContainer() 32 | if let x = try? container.decode(String.self) { 33 | self = .string(x) 34 | } else if let x = try? container.decode(Bool.self) { 35 | self = .boolean(x) 36 | } else if let x = try? container.decode(Double.self) { 37 | self = .number(x) 38 | } else if let x = try? container.decode(Int.self) { 39 | self = .integer(x) 40 | } else if let x = try? container.decode([JSONValue].self) { 41 | self = .array(x) 42 | } else if container.decodeNil() { 43 | self = .null 44 | } else { 45 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Wrong type for JSONValue") 46 | } 47 | } 48 | 49 | public func encode(to encoder: Encoder) throws { 50 | var container = encoder.singleValueContainer() 51 | switch self { 52 | case .null: 53 | try container.encodeNil() 54 | case .string(let x): 55 | try container.encode(x) 56 | case .boolean(let x): 57 | try container.encode(x) 58 | case .number(let x): 59 | try container.encode(x) 60 | case .integer(let x): 61 | try container.encode(x) 62 | case .array(let x): 63 | try container.encode(x) 64 | } 65 | } 66 | 67 | static func createFromValue(_ value: Any, ofType type: JSONType) throws -> JSONValue { 68 | 69 | switch type { 70 | 71 | case .null: 72 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Value provided for null type")) 73 | 74 | case .string: 75 | guard value is String else { 76 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Value was not expected type of String")) 77 | } 78 | return .string(value as! String) 79 | 80 | case .boolean: 81 | guard value is Bool else { 82 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Value was not expected type of Bool")) 83 | } 84 | return .boolean(value as! Bool) 85 | 86 | case .number: 87 | guard value is Double else { 88 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Value was not expected type of Double")) 89 | } 90 | return .number(value as! Double) 91 | 92 | case .integer: 93 | guard value is Int else { 94 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Value was not expected type of Int")) 95 | } 96 | return .integer(value as! Int) 97 | 98 | case .array: 99 | guard value is [Any] else { 100 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Value was not expected type of [Any]")) 101 | } 102 | let arrayValue = value as! [Any] 103 | return .array(try arrayValue.map { item in 104 | try createFromValue(item, ofType: .string) 105 | }) 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/MessageContent.swift: -------------------------------------------------------------------------------- 1 | // Created by Ronald Mannak on 4/12/24. 2 | 3 | import Foundation 4 | 5 | public enum MessageContent: Hashable { 6 | case text(String) 7 | case imageUrl(URLDetail) 8 | } 9 | 10 | extension MessageContent { 11 | public enum ContentType: String, Codable, Hashable { 12 | case text 13 | case imageUrl = "image_url" 14 | } 15 | 16 | public struct URLDetail: Codable, Equatable, Hashable { 17 | 18 | public enum Detail: String, Codable { 19 | case low, high, auto 20 | } 21 | 22 | let url: String 23 | let detail: Detail? 24 | 25 | public init(url: String, detail: Detail? = nil) { 26 | self.url = url 27 | self.detail = detail 28 | } 29 | 30 | public init(url: URL, detail: Detail? = nil) { 31 | self.init(url: url.absoluteString, detail: detail) 32 | } 33 | 34 | public init(imageData: Data, detail: Detail? = nil) { 35 | let base64 = imageData.base64EncodedString() 36 | self.init(url: "data:image/jpeg;base64,\(base64)", detail: detail) 37 | } 38 | } 39 | } 40 | 41 | extension MessageContent: Codable { 42 | 43 | private enum CodingKeys: String, CodingKey { 44 | case type, text, imageUrl 45 | } 46 | 47 | public init(from decoder: Decoder) throws { 48 | let container = try decoder.container(keyedBy: CodingKeys.self) 49 | let type = try container.decode(ContentType.self, forKey: .type) 50 | 51 | switch type { 52 | case .text: 53 | let text = try container.decode(String.self, forKey: .text) 54 | self = .text(text) 55 | case .imageUrl: 56 | let imageUrl = try container.decode(URLDetail.self, forKey: .imageUrl) 57 | self = .imageUrl(imageUrl) 58 | } 59 | } 60 | 61 | public func encode(to encoder: Encoder) throws { 62 | var container = encoder.container(keyedBy: CodingKeys.self) 63 | switch self { 64 | case .text(let text): 65 | try container.encode(ContentType.text.rawValue, forKey: .type) 66 | try container.encode(text, forKey: .text) 67 | case .imageUrl(let urlDetail): 68 | try container.encode(ContentType.imageUrl.rawValue, forKey: .type) 69 | try container.encode(urlDetail, forKey: .imageUrl) 70 | } 71 | } 72 | } 73 | 74 | extension MessageContent: Equatable { 75 | public static func == (lhs: MessageContent, rhs: MessageContent) -> Bool { 76 | switch (lhs, rhs) { 77 | case (.text(let lhsText), .text(let rhsText)): 78 | return lhsText == rhsText 79 | case (.imageUrl(let lhsUrlDetail), .imageUrl(let rhsUrlDetail)): 80 | return lhsUrlDetail == rhsUrlDetail 81 | default: 82 | return false 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/OpenAIAPIConnection+createChatCompletionRequest.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 7/27/23 2 | 3 | import Get 4 | 5 | private let chatCompletionPath = "/v1/chat/completions" 6 | 7 | extension OpenAIAPIConnection { 8 | func createChatCompletionRequest(for body: Encodable) async throws -> Request { 9 | Request( 10 | path: chatCompletionPath, 11 | method: .post, 12 | body: body, 13 | headers: self.requestHeaders) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/Penalty.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a value that can be between `-2.0` and `2.0`. 4 | public struct Penalty: Equatable { 5 | public let value: Decimal 6 | 7 | /// Creates the penalty value, clamped between `-2.0` and `2.0`. 8 | public init(_ value: Decimal) { 9 | self.value = Self.clamp(value) 10 | } 11 | } 12 | 13 | extension Penalty { 14 | /// Clamps the value between `-2.0` and `2.0`. 15 | public static func clamp(_ value: Decimal) -> Decimal { 16 | return min(2.0, max(-2.0, value)) 17 | } 18 | } 19 | 20 | extension Penalty: ExpressibleByFloatLiteral { 21 | /// Allows creation of a ``Penalty`` directly with a `Double`. 22 | public init(floatLiteral value: Double) { 23 | self.init(Decimal(value)) 24 | } 25 | } 26 | 27 | extension Penalty: Codable { 28 | public init(from decoder: Decoder) throws { 29 | let container = try decoder.singleValueContainer() 30 | try self.init(container.decode(Decimal.self)) 31 | } 32 | 33 | public func encode(to encoder: Encoder) throws { 34 | var container = encoder.singleValueContainer() 35 | try container.encode(self.value) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/Percentage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a value that can be between `0.0` and `1.0`. 4 | public struct Percentage: Equatable { 5 | public let value: Decimal 6 | 7 | /// Creates the percentage, clamping between `0.0` and `1.0`. 8 | public init(_ value: Decimal) { 9 | self.value = Self.clamp(value) 10 | } 11 | } 12 | 13 | extension Percentage { 14 | /// Clamps the value between `0.0` and `1.0`. 15 | public static func clamp(_ value: Decimal) -> Decimal { 16 | return min(1.0, max(0.0, value)) 17 | } 18 | } 19 | 20 | extension Percentage: ExpressibleByFloatLiteral { 21 | /// Allows the ``Percentage`` to be created via direct assignment from a `Double`. 22 | public init(floatLiteral value: Double) { 23 | self.init(Decimal(value)) 24 | } 25 | } 26 | 27 | extension Percentage: Codable { 28 | public init(from decoder: Decoder) throws { 29 | let container = try decoder.singleValueContainer() 30 | try self.init(container.decode(Decimal.self)) 31 | } 32 | 33 | public func encode(to encoder: Encoder) throws { 34 | var container = encoder.singleValueContainer() 35 | try container.encode(self.value) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/streaming/ChatStreamedResponseChunk.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 5/5/23 2 | 3 | import Foundation 4 | 5 | struct ChatStreamedResponseChunk: Codable, Identifiable { 6 | struct Choice: Codable { 7 | struct Delta: Codable { 8 | let role: ChatMessage.Role? 9 | let content: String? 10 | } 11 | let delta: Delta 12 | 13 | enum FinishReason: String, Codable { 14 | case stop 15 | case length 16 | case contentFilter 17 | } 18 | let finishReason: FinishReason? 19 | } 20 | let choices: [Choice] 21 | let id: String 22 | let usage: Usage? 23 | } 24 | 25 | extension ChatStreamedResponseChunk { 26 | static private var CHUNK_DECODER: JSONDecoder = { 27 | let decoder = JSONDecoder() 28 | decoder.keyDecodingStrategy = .convertFromSnakeCase 29 | return decoder 30 | }() 31 | 32 | static func decode(from chunkString: String) -> ChatStreamedResponseChunk? { 33 | guard chunkString.hasPrefix("data: "), 34 | let data = chunkString.dropFirst(6).data(using: .utf8) else { 35 | return nil 36 | } 37 | return try? CHUNK_DECODER.decode(ChatStreamedResponseChunk.self, from: data) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/streaming/ChatThread+withStreaming.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 5/11/23 2 | 3 | extension ChatThread { 4 | public func withStreaming() -> StreamableChatThread { 5 | return StreamableChatThread(chatThread: self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/streaming/OpenAIAPIConnection+createChatCompletionAsyncByteStream.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 7/27/23 2 | 3 | import Foundation 4 | 5 | extension OpenAIAPIConnection { 6 | func createChatCompletionAsyncByteStream(for body: Encodable) async throws -> URLSession.AsyncBytes { 7 | 8 | let request = try await createChatCompletionRequest(for: body) 9 | let urlRequest = try await client.makeURLRequest(for: request) 10 | let (asyncByteStream, response) = try await client.session.bytes(for: urlRequest) 11 | 12 | guard let response = response as? HTTPURLResponse else { 13 | throw CleverBirdError.responseParsingFailed(message: "Expected response of type HTTPURLResponse, but received: \(response)") 14 | } 15 | 16 | guard (200...299).contains(response.statusCode) else { 17 | throw CleverBirdError.requestFailed(message: "Response status code: \(response.statusCode)") 18 | } 19 | 20 | return asyncByteStream 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/streaming/StreamOptions.swift: -------------------------------------------------------------------------------- 1 | // Created by Ronald Mannak on 5/6/24. 2 | 3 | import Foundation 4 | 5 | public struct StreamOptions: Codable { 6 | 7 | let includeUsage: Bool 8 | 9 | public init(includeUsage: Bool) { 10 | self.includeUsage = includeUsage 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/streaming/StreamableChatThread+complete.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 5/5/23 2 | 3 | import Foundation 4 | 5 | extension StreamableChatThread { 6 | 7 | public func complete(using connection: OpenAIAPIConnection, 8 | model: ChatModel = .gpt4o, 9 | temperature: Percentage = 0.7, 10 | topP: Percentage? = nil, 11 | stop: [String]? = nil, 12 | maxTokens: Int? = nil, 13 | presencePenalty: Penalty? = nil, 14 | frequencyPenalty: Penalty? = nil, 15 | includeUsage: Bool = false) async throws -> AsyncThrowingStream { 16 | 17 | let requestBody = ChatCompletionRequestParameters( 18 | model: model, 19 | temperature: temperature, 20 | topP: topP, 21 | stream: true, 22 | stop: stop, 23 | maxTokens: maxTokens, 24 | presencePenalty: presencePenalty, 25 | frequencyPenalty: frequencyPenalty, 26 | user: self.chatThread.user, 27 | messages: self.chatThread.messages, 28 | streamOptions: includeUsage ? StreamOptions(includeUsage: true) : nil 29 | ) 30 | 31 | // Define the callback closure that appends the message to the chat thread 32 | let addStreamedMessageToThread: (ChatMessage) -> Void = { message in 33 | self.addMessage(message) 34 | } 35 | 36 | let asyncByteStream = try await connection.createChatCompletionAsyncByteStream(for: requestBody) 37 | 38 | return AsyncThrowingStream { [weak self] continuation in 39 | guard let strongSelf = self else { 40 | // Finished due to deallocated thread 41 | continuation.finish() 42 | return 43 | } 44 | strongSelf.streamingTask = Task { 45 | 46 | var responseMessageId: String? 47 | var responseMessageRole: ChatMessage.Role? 48 | var responseMessageContent: String? 49 | 50 | defer { 51 | DispatchQueue.main.async { 52 | strongSelf.streamingTask = nil 53 | } 54 | if let responseMessageRole, let responseMessageContent { 55 | do { 56 | var streamedMessage = try ChatMessage(role: responseMessageRole, 57 | content: responseMessageContent, 58 | id: responseMessageId) 59 | streamedMessage.id = responseMessageId ?? "unspecified" 60 | addStreamedMessageToThread(streamedMessage) 61 | } catch { 62 | print("error while creating streamed message: \(error.localizedDescription)") 63 | } 64 | } 65 | } 66 | 67 | do { 68 | for try await line in asyncByteStream.lines { 69 | guard let responseChunk = ChatStreamedResponseChunk.decode(from: line) else { 70 | print(line) 71 | break 72 | } 73 | 74 | responseMessageId = responseChunk.id 75 | 76 | if let usage = responseChunk.usage { 77 | strongSelf.usage = usage 78 | } 79 | 80 | if let deltaRole = responseChunk.choices.first?.delta.role { 81 | responseMessageRole = deltaRole 82 | continue 83 | } 84 | 85 | guard let delta = responseChunk.choices.first?.delta else { 86 | continue 87 | } 88 | 89 | guard let deltaContent = delta.content else { 90 | continue 91 | } 92 | 93 | if let currentMessageContent = responseMessageContent { 94 | responseMessageContent = currentMessageContent + deltaContent 95 | } else { 96 | responseMessageContent = deltaContent 97 | } 98 | 99 | strongSelf.usage = responseChunk.usage 100 | 101 | continuation.yield(deltaContent) 102 | } 103 | // Finished normally 104 | continuation.finish() 105 | 106 | } catch { 107 | if Task.isCancelled { 108 | // Finished due to cancellation 109 | continuation.finish() 110 | } else { 111 | // Finished due to error 112 | continuation.finish(throwing: CleverBirdError.responseParsingFailed(message: error.localizedDescription)) 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | public func cancelStreaming() { 120 | self.streamingTask?.cancel() 121 | self.streamingTask = nil 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/CleverBird/chat/streaming/StreamableChatThread.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 5/11/23 2 | 3 | public final class StreamableChatThread { 4 | 5 | var streamingTask: Task? 6 | let chatThread: ChatThread 7 | 8 | public var usage: Usage? = nil 9 | 10 | init(chatThread: ChatThread) { 11 | self.chatThread = chatThread 12 | } 13 | 14 | @discardableResult 15 | public func addSystemMessage(_ content: String) -> Self { 16 | self.chatThread.addSystemMessage(content) 17 | return self 18 | } 19 | 20 | @discardableResult 21 | public func addUserMessage(_ content: String) -> Self { 22 | self.chatThread.addUserMessage(content) 23 | return self 24 | } 25 | 26 | @discardableResult 27 | public func addAssistantMessage(_ content: String) -> Self { 28 | self.chatThread.addAssistantMessage(content) 29 | return self 30 | } 31 | 32 | @discardableResult 33 | public func addMessage(_ message: ChatMessage) -> Self { 34 | self.chatThread.addMessage(message) 35 | return self 36 | } 37 | 38 | public func getMessages() -> [ChatMessage] { 39 | self.chatThread.messages 40 | } 41 | 42 | public func getNonSystemMessages() -> [ChatMessage] { 43 | self.chatThread.getNonSystemMessages() 44 | } 45 | 46 | public func tokenCount() throws -> Int { 47 | try self.chatThread.tokenCount() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/CleverBird/embeddings/EmbeddedDocumentStore+persistence.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 9/4/23 2 | 3 | import Foundation 4 | 5 | extension EmbeddedDocumentStore { 6 | 7 | public func save(to fileURL: URL) { 8 | do { 9 | let jsonData = try JSONEncoder().encode(self.dictionaryRepresentation) 10 | try jsonData.write(to: fileURL) 11 | } catch { 12 | print(error.localizedDescription) 13 | } 14 | } 15 | 16 | public func load(from fileURL: URL) { 17 | do { 18 | let jsonData = try Data(contentsOf: fileURL) 19 | let data = try JSONDecoder().decode([Document: Vector].self, from: jsonData) 20 | self.documents = data.map { $0.key } 21 | self.embeddings = data.map { $0.value } 22 | } catch { 23 | print(error.localizedDescription) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/CleverBird/embeddings/EmbeddedDocumentStore.SimilarityMetric.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 9/2/23 2 | 3 | import Accelerate 4 | 5 | extension EmbeddedDocumentStore { 6 | 7 | public enum SimilarityMetric { 8 | case dot 9 | case cosine 10 | case euclidean 11 | 12 | var function: SimilarityFunction { 13 | switch self { 14 | case .dot: 15 | return calculateDotProduct 16 | case .cosine: 17 | return calculateCosineSimilarity 18 | case .euclidean: 19 | return calculateEuclideanSimilarity 20 | } 21 | } 22 | 23 | // Compute the dot product of each vector in the array with a query vector 24 | private func calculateDotProduct(of vectors: [Vector], and queryVector: Vector) -> [Float] { 25 | return vectors.map { vector in 26 | var result: Float = 0.0 27 | 28 | // The dot product is a measure of the extent to which two vectors point in the same direction 29 | vDSP_dotpr(vector, 1, queryVector, 1, &result, vDSP_Length(vector.count)) 30 | return result 31 | } 32 | } 33 | 34 | // Calculate the cosine similarity between an array of vectors and a query vector 35 | private func calculateCosineSimilarity(of vectors: [Vector], and queryVector: Vector) -> [Float] { 36 | let normVectors = normalize(vectors: vectors) 37 | let normQueryVector = normalize(vector: queryVector) 38 | 39 | // Cosine similarity is often used in NLP tasks to measure how similar two documents are, irrespective of their size 40 | return calculateDotProduct(of: normVectors, and: normQueryVector) 41 | } 42 | 43 | // Calculate the Euclidean similarity between an array of vectors and a query vector 44 | private func calculateEuclideanSimilarity(of vectors: [Vector], and queryVector: Vector) -> [Float] { 45 | let diffVectors = vectors.map { zip($0, queryVector).map { $0.0 - $0.1 } } 46 | let distances = diffVectors.map { sqrt($0.map { $0 * $0 }.reduce(0, +)) } 47 | 48 | // Euclidean similarity: inverse of Euclidean distance, with a tweak for handling identical vectors 49 | return distances.map { distance in distance == 0 ? 1 : 1 / distance } 50 | } 51 | 52 | // Helper function to get the Euclidean norm (or length) of a vector 53 | private func calculateNorm(of vector: Vector) -> Float { 54 | var result: Float = 0.0 55 | 56 | // Taking the dot product of the vector with itself, then square rooting the result gives us the Euclidean norm 57 | vDSP_dotpr(vector, 1, vector, 1, &result, vDSP_Length(vector.count)) 58 | return sqrt(result) 59 | } 60 | 61 | // Convert a vector to a normalized vector 62 | private func normalize(vector: Vector) -> Vector { 63 | let normValue = calculateNorm(of: vector) 64 | 65 | // Each component of the vector is divided by its norm 66 | return vector.map { $0 / normValue } 67 | } 68 | 69 | // Normalize each vector in a 2D array 70 | private func normalize(vectors: [Vector]) -> [Vector] { 71 | 72 | // Normalization is applied to each vector in the array 73 | return vectors.map { normalize(vector: $0) } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/CleverBird/embeddings/EmbeddedDocumentStore.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 7/23/23 2 | 3 | public class EmbeddedDocumentStore { 4 | 5 | public struct DocumentSimilarityResult { 6 | public let document: Document 7 | public let similarity: Similarity 8 | } 9 | 10 | private let maxBatchSize = 2048 11 | 12 | let connection: OpenAIAPIConnection 13 | let model: EmbeddingModel 14 | let user: String? 15 | 16 | public internal(set) var documents: [Document] = [] 17 | public internal(set) var embeddings: [Vector] = [] 18 | public var dictionaryRepresentation: [Document: Vector] { 19 | return zip(documents, embeddings).reduce(into: [Document: Vector]()) { result, pair in 20 | result[pair.0] = pair.1 21 | } 22 | } 23 | 24 | private var similarityMetric: SimilarityMetric 25 | 26 | public init(connection: OpenAIAPIConnection, 27 | model: EmbeddingModel = .textEmbedding3Small, 28 | user: String? = nil, 29 | similarityMetric: SimilarityMetric = .cosine) { 30 | self.connection = connection 31 | self.model = model 32 | self.user = user 33 | self.similarityMetric = similarityMetric 34 | } 35 | 36 | public func embedAndStore(_ documents: [Document]) async throws { 37 | 38 | do { 39 | let embeddingReponse = try await embed(documents) 40 | 41 | for embeddingData in embeddingReponse.data { 42 | let index = embeddingData.index 43 | addDocument(documents[index], withEmbedding: embeddingData.embedding) 44 | } 45 | 46 | } catch { 47 | throw CleverBirdError.requestFailed(message: error.localizedDescription) 48 | } 49 | } 50 | 51 | public func embedAndStore(_ document: Document) async throws { 52 | try await embedAndStore([document]) 53 | } 54 | 55 | private func embed(_ documents: [Document]) async throws -> EmbeddingResponse { 56 | 57 | guard documents.count >= 1 && documents.count <= 2048 else { 58 | throw CleverBirdError.invalidEmbeddingRequest(message: "Number of documents to embed must be between 1 and 2048.") 59 | } 60 | 61 | let requestBody = EmbeddingRequestParameters( 62 | model: self.model, 63 | input: documents, 64 | user: self.user 65 | ) 66 | 67 | do { 68 | let request = try await self.connection.createEmbeddingRequest(for: requestBody) 69 | let response = try await self.connection.client.send(request) 70 | return response.value 71 | 72 | } catch { 73 | throw CleverBirdError.requestFailed(message: error.localizedDescription) 74 | } 75 | 76 | } 77 | 78 | public func addDocument(_ document: Document, withEmbedding embedding: Vector) { 79 | documents.append(document) 80 | embeddings.append(embedding) 81 | } 82 | 83 | public func removeDocument(at index: Int) { 84 | embeddings.remove(at: index) 85 | documents.remove(at: index) 86 | } 87 | 88 | public func queryDocumentSimilarity(_ queryDocument: Document, topK: Int = 5) async throws -> [DocumentSimilarityResult] { 89 | 90 | do { 91 | let queryEmbeddingResponse = try await embed([queryDocument]) 92 | guard let queryDocumentEmbeddingData = queryEmbeddingResponse.data.first else { 93 | throw CleverBirdError.requestFailed(message: "Embedding request returned empty results") 94 | } 95 | let queryDocumentEmbedding: Vector = queryDocumentEmbeddingData.embedding 96 | let (rankedResults, similarities) = sortVectors(self.embeddings, 97 | bySimilarityTo: queryDocumentEmbedding, 98 | topK: topK, 99 | metric: self.similarityMetric) 100 | return zip(rankedResults.map { documents[$0] }, similarities).map { DocumentSimilarityResult(document: $0, similarity: $1) } 101 | } catch { 102 | throw CleverBirdError.requestFailed(message: error.localizedDescription) 103 | } 104 | } 105 | 106 | // Ranking algorithm for sorting vectors by their similarity to a query vector 107 | private func sortVectors(_ vectors: [Vector], 108 | bySimilarityTo queryVector: Vector, 109 | topK: Int = 5, 110 | metric: SimilarityMetric = .cosine) -> ([Int], [Float]) { 111 | let similarities = metric.function(vectors, queryVector) 112 | 113 | // Sorting the vectors in descending order of similarity 114 | let sortedIndices = (0.. similarities[$1] } 115 | let topIndices = Array(sortedIndices.prefix(topK)) 116 | let topSimilarities = topIndices.map { similarities[$0] } 117 | return (topIndices, topSimilarities) 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /Sources/CleverBird/embeddings/EmbeddingModel.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 7/25/23 2 | 3 | public enum EmbeddingModel: Codable { 4 | case textEmbedding3Small 5 | case textEmbedding3Large 6 | case textEmbeddingAda002 7 | case specific(String) 8 | 9 | public init(from decoder: Decoder) throws { 10 | let container = try decoder.singleValueContainer() 11 | let modelString = try container.decode(String.self) 12 | 13 | switch modelString { 14 | case "text-embedding-3-small": 15 | self = .textEmbedding3Small 16 | case "text-embedding-3-large": 17 | self = .textEmbedding3Large 18 | case "text-embedding-ada-002": 19 | self = .textEmbeddingAda002 20 | default: 21 | self = .specific(modelString) 22 | } 23 | } 24 | 25 | public func encode(to encoder: Encoder) throws { 26 | var container = encoder.singleValueContainer() 27 | let modelString = self.description 28 | 29 | try container.encode(modelString) 30 | } 31 | } 32 | 33 | extension EmbeddingModel: CustomStringConvertible { 34 | public var description: String { 35 | switch self { 36 | case .textEmbedding3Small: 37 | return "text-embedding-3-small" 38 | case .textEmbedding3Large: 39 | return "text-embedding-3-large" 40 | case .textEmbeddingAda002: 41 | return "text-embedding-ada-002" 42 | case .specific(let string): 43 | return string 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/CleverBird/embeddings/EmbeddingRequestParameters.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 7/28/23 2 | 3 | public struct EmbeddingRequestParameters: Encodable { 4 | public let model: EmbeddingModel 5 | public let input: [String] 6 | public let dimensions: Int? 7 | public let user: String? 8 | 9 | public init(model: EmbeddingModel, 10 | input: [String], 11 | dimensions: Int? = nil, 12 | user: String? = nil) { 13 | self.model = model 14 | self.input = input 15 | self.dimensions = dimensions 16 | self.user = user 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/CleverBird/embeddings/EmbeddingResponse.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 7/27/23 2 | 3 | struct EmbeddingResponse: Codable { 4 | 5 | struct EmbeddingData: Codable { 6 | let embedding: Vector 7 | let index: Int 8 | } 9 | 10 | struct Usage: Codable { 11 | let promptTokens: Int 12 | let totalTokens: Int 13 | } 14 | 15 | let data: [EmbeddingData] 16 | let usage: Usage 17 | } 18 | -------------------------------------------------------------------------------- /Sources/CleverBird/embeddings/OpenAIAPIConnection+createEmbeddingRequest.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 7/27/23 2 | 3 | import Get 4 | 5 | private let embeddingsPath = "/v1/embeddings" 6 | 7 | extension OpenAIAPIConnection { 8 | func createEmbeddingRequest(for body: Encodable) async throws -> Request { 9 | Request( 10 | path: embeddingsPath, 11 | method: .post, 12 | body: body, 13 | headers: self.requestHeaders) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/CleverBird/embeddings/typealiases.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 8/31/23 2 | 3 | public typealias Document = String 4 | public typealias Vector = [Float] 5 | public typealias Similarity = Float 6 | public typealias SimilarityFunction = ([Vector], Vector) -> [Similarity] 7 | -------------------------------------------------------------------------------- /Sources/CleverBird/tokenization/Pattern.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A wrapper for simplified RegEx matching. It does not throw an exception when given a bad pattern, rather it triggers a `preconditionFailure` with the error message. This makes it useful for providing reusable constants which generally should not fail in the real world. 5 | */ 6 | struct Pattern: CustomStringConvertible { 7 | typealias Options = NSRegularExpression.Options 8 | typealias MatchingOptions = NSRegularExpression.MatchingOptions 9 | 10 | /// The underlying `NSRegularExpression` instance. 11 | let regex: NSRegularExpression 12 | 13 | /// Description of the pattern. 14 | var description: String { 15 | regex.pattern 16 | } 17 | 18 | /// Creates a pattern from a string. 19 | /// 20 | /// - Parameters: 21 | /// - pattern: The pattern to match. 22 | /// - options: The options to use when matching. 23 | init(_ pattern: String, options: Options = []) { 24 | do { 25 | let newRegex = try NSRegularExpression(pattern: pattern, options: options) 26 | regex = newRegex 27 | } catch { 28 | preconditionFailure("Invalid regular expression '\(pattern)': \(error)") 29 | } 30 | } 31 | 32 | /// Returns whether the pattern matches the given string. 33 | func hasMatch(in text: String, options: MatchingOptions = []) -> Bool { 34 | let range = NSRange(text.startIndex.. 0 36 | } 37 | 38 | /// Returns whether the pattern matches the given ``CustomStringConvertible`` value. 39 | /// 40 | /// - Parameters: 41 | /// - value: The value to match. 42 | /// - options: The options to use when matching. 43 | /// 44 | /// - Returns: Whether the pattern matches the given value. 45 | func hasMatch(in text: CustomStringConvertible, options: MatchingOptions = []) -> Bool { 46 | return hasMatch(in: String(describing: text), options: options) 47 | } 48 | 49 | /// Returns the first match of the pattern in the given string. 50 | /// 51 | /// - Parameters: 52 | /// - value: The value to match. 53 | /// - options: The options to use when matching. 54 | /// 55 | /// - Returns: The first match of the pattern in the given string. 56 | func matchGroups(in text: String, options: MatchingOptions = []) -> Result? { 57 | let range = NSRange(text.startIndex.. Result? { 66 | return matchGroups(in: String(describing: text), options: options) 67 | } 68 | 69 | func findAll(in text: String, options: MatchingOptions = []) -> [Result] { 70 | let results = regex.matches(in: text, options: options, range: NSRange(text.startIndex..., in: text)) 71 | return results.compactMap { 72 | Result(textCheckingResult: $0, original: text) 73 | // Range($0.range, in: value).map { value[$0] } 74 | } 75 | } 76 | 77 | func replace(in text: String, with replacement: String, options: MatchingOptions = []) -> String { 78 | let range = NSRange(text.startIndex.. Substring? { 95 | guard i < textCheckingResult.numberOfRanges else { 96 | return nil 97 | } 98 | 99 | if let group = Range(textCheckingResult.range(at: i), in: original) { 100 | return original[group] 101 | } 102 | return nil 103 | } 104 | 105 | subscript(i: Int, j: Int) -> (Substring, Substring)? { 106 | if let iValue = self[i], 107 | let jValue = self[j] 108 | { 109 | return (iValue, jValue) 110 | } else { 111 | return nil 112 | } 113 | } 114 | 115 | subscript(i: Int, j: Int, k: Int) -> (Substring, Substring, Substring)? { 116 | if let iValue = self[i], 117 | let jValue = self[j], 118 | let kValue = self[k] 119 | { 120 | return (iValue, jValue, kValue) 121 | } else { 122 | return nil 123 | } 124 | } 125 | 126 | subscript(name: String) -> Substring? { 127 | let nsrange = textCheckingResult.range(withName: name) 128 | guard nsrange.location != NSNotFound else { 129 | return nil 130 | } 131 | 132 | if let range = Range(nsrange, in: original) { 133 | return original[range] 134 | } else { 135 | return nil 136 | } 137 | } 138 | 139 | subscript(name1: String, name2: String) -> (Substring, Substring)? { 140 | if let value1 = self[name1], 141 | let value2 = self[name2] 142 | { 143 | return (value1, value2) 144 | } 145 | return nil 146 | } 147 | 148 | subscript(name1: String, name2: String, name3: String) -> (Substring, Substring, Substring)? { 149 | if let value1 = self[name1], 150 | let value2 = self[name2], 151 | let value3 = self[name3] 152 | { 153 | return (value1, value2, value3) 154 | } 155 | return nil 156 | } 157 | 158 | } 159 | } 160 | 161 | extension Pattern: ExpressibleByStringLiteral { 162 | typealias StringLiteralType = String 163 | 164 | init(stringLiteral value: String) { 165 | self.init(value) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Sources/CleverBird/tokenization/Token.swift: -------------------------------------------------------------------------------- 1 | public struct Token: Equatable, Hashable { 2 | public let value: Int 3 | 4 | public init(_ value: Int) { 5 | self.value = value 6 | } 7 | } 8 | 9 | extension Token: Codable { 10 | public init(from decoder: Decoder) throws { 11 | let container = try decoder.singleValueContainer() 12 | try self.init(container.decode(Int.self)) 13 | } 14 | 15 | public func encode(to encoder: Encoder) throws { 16 | var container = encoder.singleValueContainer() 17 | try container.encode(value) 18 | } 19 | } 20 | 21 | extension Token: ExpressibleByIntegerLiteral { 22 | public init(integerLiteral value: Int) { 23 | self.init(value) 24 | } 25 | } 26 | 27 | extension Token: RawRepresentable { 28 | public var rawValue: String { 29 | String(value) 30 | } 31 | 32 | public init?(rawValue: String) { 33 | guard let intValue = Int(rawValue) else { 34 | return nil 35 | } 36 | self.value = intValue 37 | } 38 | } 39 | 40 | extension Token: CustomStringConvertible { 41 | public var description: String { rawValue } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/CleverBird/tokenization/TokenEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | fileprivate struct SymbolPair: Hashable { 4 | let left: String 5 | let right: String 6 | 7 | @inlinable 8 | init(_ left: String, _ right: String) { 9 | self.left = left 10 | self.right = right 11 | } 12 | } 13 | 14 | fileprivate func bytesToUnicode() -> [UInt8: Character] { 15 | var dict = [UInt8: Character]() 16 | var n: UInt16 = 0 17 | 18 | for b: UInt8 in 0...255 { 19 | switch b { 20 | case 33...126, 161...172, 174...255: 21 | dict[b] = Character(UnicodeScalar(b)) 22 | default: 23 | dict[b] = Character(UnicodeScalar(256 + n)!) 24 | n += 1 25 | } 26 | } 27 | 28 | return dict 29 | } 30 | 31 | fileprivate func getPairs(in word: [String]) -> Set { 32 | var pairs = Set() 33 | var prevChar = word[0] 34 | 35 | for char in word[1.. String { 60 | var word: [String] = token.map { String($0) } 61 | var pairs: Set = getPairs(in: word) 62 | if pairs.isEmpty { 63 | return token 64 | } 65 | while true { 66 | guard let bigram = pairs.min(by: { bpeRanks[$0] ?? Int.max < bpeRanks[$1] ?? Int.max }), 67 | let _ = bpeRanks[bigram] else { break } 68 | 69 | let first = bigram.left 70 | let second = bigram.right 71 | var newWord = [String]() 72 | var i = 0 73 | 74 | while i < word.count { 75 | if let j = word[i...].firstIndex(of: first) { 76 | newWord.append(contentsOf: word[i.. [Token] { 102 | var bpeTokens: [Token] = [] 103 | var cache = [String: String]() 104 | 105 | for result in TokenEncoder.pattern.findAll(in: text) { 106 | let token = String(result.value.utf8.map { TokenEncoder.byteEncoder[$0]! }) 107 | 108 | if token.isEmpty { continue } 109 | 110 | let word = cache[token] ?? bpe(token: token) 111 | cache[token] = word 112 | 113 | let splitBpe = word.split(separator: " ") 114 | let encodedResult = try splitBpe.map { 115 | guard let value = encoder[String($0)] else { 116 | throw TokenEncoder.Error.invalidEncoding(value: String($0)) 117 | } 118 | return value 119 | } 120 | bpeTokens.append(contentsOf: encodedResult) 121 | } 122 | return bpeTokens 123 | } 124 | 125 | public func decode(tokens: [Token]) throws -> String { 126 | let text = try tokens.map { 127 | guard let value = decoder[$0] else { 128 | throw TokenEncoder.Error.invalidToken(value: $0) 129 | } 130 | return value 131 | }.joined() 132 | let decoded = try text.map { 133 | let char = $0 134 | guard let byte = TokenEncoder.byteDecoder[char] else { 135 | throw TokenEncoder.Error.invalidCharacter(value: $0) 136 | } 137 | return byte 138 | } 139 | let decodedData = Data(decoded) 140 | return String(decoding: decodedData, as: UTF8.self) 141 | } 142 | } 143 | 144 | extension TokenEncoder { 145 | public enum Error: Swift.Error, Equatable { 146 | case missingResource(name: String) 147 | case invalidBytePair(value: String) 148 | case invalidEncoding(value: String) 149 | case invalidToken(value: Token) 150 | case invalidCharacter(value: Character) 151 | } 152 | } 153 | 154 | extension TokenEncoder { 155 | public enum Model: String { 156 | case gpt3 157 | 158 | func encoder() throws -> [String: Token] { 159 | guard let encoderPath = Bundle.module.url(forResource: "\(rawValue)-encoder", withExtension: "json") else { 160 | throw TokenEncoder.Error.missingResource(name: "\(rawValue)-encoder.json") 161 | } 162 | let encoderData = try Data(contentsOf: encoderPath) 163 | return try JSONDecoder().decode([String: Token].self, from: encoderData) 164 | } 165 | 166 | fileprivate func bpeRanks() throws -> [SymbolPair: Int] { 167 | guard let bpePath = Bundle.module.url(forResource: "\(rawValue)-vocab", withExtension: "bpe") else { 168 | throw TokenEncoder.Error.missingResource(name: "\(rawValue)-vocab.bpe") 169 | } 170 | let bpeData = try String(contentsOf: bpePath, encoding: .utf8) 171 | 172 | let bpeMerges = try bpeData 173 | .split(separator: "\n") 174 | .dropFirst().dropLast() 175 | .map { 176 | let split = $0.split(separator: " ") 177 | guard split.count == 2, 178 | let left = split.first, let right = split.last else { 179 | throw TokenEncoder.Error.invalidBytePair(value: String($0)) 180 | } 181 | return SymbolPair(String(left), String(right)) 182 | } 183 | 184 | return bpeMerges.enumerated().reduce(into: [SymbolPair: Int]()) { result, item in 185 | result[item.element] = item.offset 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Tests/CleverBirdTests/MessageContentTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Ronald Mannak on 4/12/24. 2 | 3 | import Foundation 4 | import XCTest 5 | @testable import CleverBird 6 | 7 | class MessageContentTests: XCTestCase { 8 | 9 | func testURL() { 10 | let content = MessageContent.URLDetail(url: URL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg")!) 11 | XCTAssertEqual(content.url, "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg") 12 | } 13 | 14 | func testBase64() { 15 | let data = "Hello, world".data(using: .utf8)! 16 | let content = MessageContent.URLDetail(imageData: data) 17 | XCTAssertEqual(content.url, "data:image/jpeg;base64,SGVsbG8sIHdvcmxk") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/CleverBirdTests/MessageEncodingTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Ronald Mannak on 4/12/24. 2 | 3 | import XCTest 4 | @testable import CleverBird 5 | 6 | class ContentEncodingTests: XCTestCase { 7 | 8 | let text = """ 9 | { 10 | "type": "text", 11 | "text": "What’s in this image?" 12 | } 13 | """.data(using: .utf8)! 14 | 15 | let imageURL = """ 16 | { 17 | "type": "image_url", 18 | "image_url": { 19 | "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", 20 | } 21 | } 22 | """.data(using: .utf8)! 23 | 24 | let imageURLDetail = """ 25 | { 26 | "type": "image_url", 27 | "image_url": { 28 | "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", 29 | "detail": "high" 30 | } 31 | } 32 | """.data(using: .utf8)! 33 | 34 | let imageData = """ 35 | { 36 | "type": "image_url", 37 | "image_url": { 38 | "url": "data:image/jpeg;base64,SGVsbG8sIHdvcmxk" 39 | } 40 | } 41 | """.data(using: .utf8)! 42 | 43 | var encoder: JSONEncoder! 44 | var decoder: JSONDecoder! 45 | 46 | override func setUp() { 47 | encoder = JSONEncoder() 48 | decoder = JSONDecoder() 49 | encoder.keyEncodingStrategy = .convertToSnakeCase 50 | decoder.keyDecodingStrategy = .convertFromSnakeCase 51 | } 52 | 53 | func testTextDecoding() throws { 54 | let object = try decoder.decode(MessageContent.self, from: text) 55 | switch object { 56 | case .imageUrl(_): 57 | XCTFail() 58 | case .text(let text): 59 | XCTAssertEqual(text, "What’s in this image?") 60 | } 61 | } 62 | 63 | func testImageURLDecoding() throws { 64 | _ = try decoder.decode(MessageContent.self, from: imageURL) 65 | } 66 | 67 | func testImageURLDetailDecoding() throws { 68 | _ = try decoder.decode(MessageContent.self, from: imageURLDetail) 69 | } 70 | 71 | func testImageDataDecoding() throws { 72 | _ = try decoder.decode(MessageContent.self, from: imageData) 73 | } 74 | 75 | func testTextEncoding() throws { 76 | let content = MessageContent.text("What’s in this image?") 77 | let json = try encoder.encode(content) 78 | 79 | let object = try decoder.decode(MessageContent.self, from: json) 80 | switch object { 81 | case .imageUrl(_): 82 | XCTFail() 83 | case .text(let text): 84 | XCTAssertEqual(text, "What’s in this image?") 85 | } 86 | } 87 | 88 | func testImageURLEncoding() throws { 89 | let content = MessageContent.imageUrl(MessageContent.URLDetail(url: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg")) 90 | let json = try encoder.encode(content) 91 | let object = try decoder.decode(MessageContent.self, from: json) 92 | 93 | switch object { 94 | case .imageUrl(let detail): 95 | XCTAssertEqual(detail.url, "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg") 96 | XCTAssertEqual(detail.detail, nil) 97 | case .text(_): 98 | XCTFail() 99 | } 100 | } 101 | 102 | func testImageURLDetailEncoding() throws { 103 | let content = MessageContent.imageUrl(MessageContent.URLDetail(url: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", detail: .high)) 104 | let json = try encoder.encode(content) 105 | let object = try decoder.decode(MessageContent.self, from: json) 106 | 107 | switch object { 108 | case .imageUrl(let detail): 109 | XCTAssertEqual(detail.url, "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg") 110 | XCTAssertEqual(detail.detail, .high) 111 | case .text(_): 112 | XCTFail() 113 | } 114 | } 115 | 116 | func testImageDataEncoding() throws { 117 | let content = MessageContent.imageUrl(MessageContent.URLDetail(url: "data:image/jpeg;base64,aGVsbG8sIHdvcmxk")) 118 | let json = try encoder.encode(content) 119 | let object = try decoder.decode(MessageContent.self, from: json) 120 | 121 | switch object { 122 | case .imageUrl(let detail): 123 | XCTAssertEqual(detail.url, "data:image/jpeg;base64,aGVsbG8sIHdvcmxk") 124 | XCTAssertEqual(detail.detail, nil) 125 | case .text(_): 126 | XCTFail() 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Tests/CleverBirdTests/OpenAIChatThreadTests.swift: -------------------------------------------------------------------------------- 1 | // Created by B.T. Franklin on 4/15/23 2 | 3 | import XCTest 4 | @testable import CleverBird 5 | 6 | class OpenAIChatThreadTests: XCTestCase { 7 | 8 | func testThreadLength() async { 9 | let userMessageContent = "Who won the world series in 2020?" 10 | let chatThread = ChatThread() 11 | .addSystemMessage("You are a helpful assistant.") 12 | .addUserMessage(userMessageContent) 13 | 14 | XCTAssertEqual(2, chatThread.getMessages().count) 15 | XCTAssertEqual(1, chatThread.getNonSystemMessages().count) 16 | XCTAssertEqual(userMessageContent, chatThread.getNonSystemMessages().first?.content?.description) 17 | } 18 | 19 | func testTokenCount() throws { 20 | let chatThread = ChatThread() 21 | .addSystemMessage("You are a helpful assistant.") 22 | .addUserMessage("Who won the world series in 2020?") 23 | let tokenCount = try chatThread.tokenCount() 24 | 25 | XCTAssertEqual(tokenCount, 25, "Unexpected token count") 26 | } 27 | 28 | func testFunctionCallMessage() throws { 29 | 30 | let getCurrentWeatherParameters = Function.Parameters( 31 | properties: [ 32 | "location": Function.Parameters.Property(type: .string, 33 | description: "The city and state, e.g. San Francisco, CA"), 34 | "format": Function.Parameters.Property(type: .string, 35 | description: "The temperature unit to use. Infer this from the users location.", 36 | enumCases: ["celsius", "fahrenheit"]) 37 | ], 38 | required: ["location", "format"]) 39 | 40 | let getCurrentWeather = Function(name: "get_current_weather", 41 | description: "Get the current weather", 42 | parameters: getCurrentWeatherParameters) 43 | 44 | let getNDayWeatherForecastParameters = Function.Parameters( 45 | properties: [ 46 | "location": Function.Parameters.Property(type: .string, 47 | description: "The city and state, e.g. San Francisco, CA"), 48 | "format": Function.Parameters.Property(type: .string, 49 | description: "The temperature unit to use. Infer this from the users location.", 50 | enumCases: ["celsius", "fahrenheit"]), 51 | "num_days": Function.Parameters.Property(type: .integer, 52 | description: "The number of days to forecast") 53 | ], 54 | required: ["location", "format", "num_days"]) 55 | 56 | let getNDayWeatherForecast = Function(name: "get_n_day_weather_forecast", 57 | description: "Get an N-day weather forecast", 58 | parameters: getNDayWeatherForecastParameters) 59 | 60 | let functionCall = FunctionCall(name: "testFunc", arguments: ["arg1": .string("value1")]) 61 | _ = ChatThread() 62 | .addSystemMessage("You are a helpful assistant.") 63 | .setFunctions([getCurrentWeather, getNDayWeatherForecast]) 64 | .addMessage(try! ChatMessage(role: .assistant, functionCall: functionCall)) 65 | } 66 | 67 | func testInvalidMessageCreation() { 68 | XCTAssertThrowsError(try ChatMessage(role: .assistant)) { error in 69 | XCTAssertEqual(error as? CleverBirdError, CleverBirdError.invalidMessageContent) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/CleverBirdTests/TokenEncoderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CleverBird 3 | 4 | final class TokenEncoderTests: XCTestCase { 5 | 6 | func testEncodingAndDecoding() throws { 7 | let testCases = [ 8 | "Hello, world!", 9 | "This is a test string.", 10 | "Testing Unicode characters: 😃🌍🚀", 11 | "A more complex example: Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 12 | """ 13 | The term "Swifty" refers to writing code in a way that follows the design patterns, idioms, and best practices of the Swift programming language. Writing Swifty code usually means adhering to the following principles: 14 | 15 | Clarity: Code should be easy to read and understand. Prefer clear and expressive names for variables, functions, and types. 16 | Safety: Swift emphasizes safety by using strong typing, optionals, and error handling to minimize the chance of runtime errors. 17 | Conciseness: Code should be concise, yet expressive, using features like type inference, trailing closures, and other Swift idioms. 18 | Performance: Swift is designed for high performance, so Swifty code should take advantage of Swift's optimizations and not unnecessarily sacrifice performance. 19 | Protocol-oriented programming: Swift encourages the use of protocols and protocol extensions to create flexible, reusable components. 20 | Use of Swift-specific features: Swift has many powerful features, like generics, closures, and pattern matching. Swifty code should make good use of these features where appropriate. 21 | """, 22 | ] 23 | 24 | let tokenEncoder = try TokenEncoder(model: .gpt3) 25 | 26 | for text in testCases { 27 | let encoded = try tokenEncoder.encode(text: text) 28 | let decoded = try tokenEncoder.decode(tokens: encoded) 29 | XCTAssertEqual(text, decoded) 30 | } 31 | } 32 | } 33 | --------------------------------------------------------------------------------