From 19dbc765c46c0cb091ac5d73f1eddea932b511b9 Mon Sep 17 00:00:00 2001 From: Kevin Hermawan <84965338+kevinhermawan@users.noreply.github.com> Date: Sat, 11 Nov 2023 00:27:24 +0700 Subject: [PATCH] first commit --- .github/workflows/code-quality.yml | 28 ++++ .gitignore | 8 ++ LICENSE | 21 +++ Package.resolved | 14 ++ Package.swift | 29 ++++ README.md | 63 +++++++++ .../Extensions/JSONDecoder+Default.swift | 23 ++++ .../Extensions/JSONEncoder+Default.swift | 17 +++ Sources/OllamaKit/OllamaKit.swift | 124 ++++++++++++++++++ .../RequestData/OKCopyModelRequestData.swift | 21 +++ .../OKDeleteModelRequestData.swift | 19 +++ .../RequestData/OKGenerateRequestData.swift | 53 ++++++++ .../RequestData/OKModelInfoRequestData.swift | 19 +++ .../Responses/OKGenerateResponse.swift | 25 ++++ .../Responses/OKModelInfoResponse.swift | 18 +++ .../OllamaKit/Responses/OKModelResponse.swift | 22 ++++ Sources/OllamaKit/Utils/OKRouter.swift | 82 ++++++++++++ Tests/OllamaKitTests/OllamaKitTests.swift | 53 ++++++++ 18 files changed, 639 insertions(+) create mode 100644 .github/workflows/code-quality.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/OllamaKit/Extensions/JSONDecoder+Default.swift create mode 100644 Sources/OllamaKit/Extensions/JSONEncoder+Default.swift create mode 100644 Sources/OllamaKit/OllamaKit.swift create mode 100644 Sources/OllamaKit/RequestData/OKCopyModelRequestData.swift create mode 100644 Sources/OllamaKit/RequestData/OKDeleteModelRequestData.swift create mode 100644 Sources/OllamaKit/RequestData/OKGenerateRequestData.swift create mode 100644 Sources/OllamaKit/RequestData/OKModelInfoRequestData.swift create mode 100644 Sources/OllamaKit/Responses/OKGenerateResponse.swift create mode 100644 Sources/OllamaKit/Responses/OKModelInfoResponse.swift create mode 100644 Sources/OllamaKit/Responses/OKModelResponse.swift create mode 100644 Sources/OllamaKit/Utils/OKRouter.swift create mode 100644 Tests/OllamaKitTests/OllamaKitTests.swift diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..863c13d --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,28 @@ +name: Code Quality + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test-ios: + runs-on: macos-13 + + steps: + - uses: actions/checkout@v3 + + - name: Build and test + run: xcodebuild test -scheme OllamaKit -destination 'platform=iOS Simulator,name=iPhone 14 Pro' + + test-macos: + runs-on: macos-13 + + steps: + - uses: actions/checkout@v3 + + - name: Build and test + run: xcodebuild test -scheme OllamaKit -destination 'platform=macOS,arch=x86_64' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..328be88 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Kevin Hermawan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..bc7ce25 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", + "version" : "5.8.1" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..6fc61f5 --- /dev/null +++ b/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "OllamaKit", + platforms: [ + .iOS(.v13), + .macOS(.v11), + .macCatalyst(.v13) + ], + products: [ + .library( + name: "OllamaKit", + targets: ["OllamaKit"]), + ], + dependencies: [ + .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.8.1")) + ], + targets: [ + .target( + name: "OllamaKit", + dependencies: ["Alamofire"]), + .testTarget( + name: "OllamaKitTests", + dependencies: ["OllamaKit", "Alamofire"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..673b1bb --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# OllamaKit + +A Swift library for interacting with the Ollama API. + +## Overview + +`OllamaKit` is a Swift library for interacting with the Ollama API, built on top of the powerful [Alamofire](https://github.com/Alamofire/Alamofire) networking framework. It extends Alamofire's capabilities to provide a streamlined and intuitive interface for managing network interactions and data processing specific to the [Ollama API](https://github.com/jmorganca/ollama/blob/main/docs/api.md). + +## Primary Use + +`OllamaKit` is primarily designed for use within [Ollamac](https://github.com/kevinhermawan/Ollamac), a macOS application dedicated to interacting with Ollama models. While the library offers comprehensive functionalities for Ollama API interaction, its features and optimizations are specifically aligned with the requirements of `Ollamac`. This focus ensures that `OllamaKit` provides an ideal toolset for `Ollamac`, facilitating efficient and effective model management and interaction. + +## Requirements + +- iOS 13.0+ / macOS 11+ + +## Installation + +You can add `OllamaKit` as a dependency to your project using Swift Package Manager by adding it to the dependencies value of your `Package.swift`. + +```swift +dependencies: [ + .package(url: "https://github.com/kevinhermawan/OllamaKit.git", .upToNextMajor(from: "1.0.0")) +] +``` + +Alternatively, in Xcode: + +1. Open your project in Xcode. +2. Click on `File` -> `Swift Packages` -> `Add Package Dependency...` +3. Enter the repository URL: `https://github.com/kevinhermawan/OllamaKit.git` +4. Choose the version you want to add. You probably want to add the latest version. +5. Click `Add Package`. + +## Acknowledgements + +- [Alamofire](https://github.com/Alamofire/Alamofire) + +## License + +``` +MIT License + +Copyright (c) 2023 Kevin Hermawan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/Sources/OllamaKit/Extensions/JSONDecoder+Default.swift b/Sources/OllamaKit/Extensions/JSONDecoder+Default.swift new file mode 100644 index 0000000..97a1b55 --- /dev/null +++ b/Sources/OllamaKit/Extensions/JSONDecoder+Default.swift @@ -0,0 +1,23 @@ +// +// JSONDecoder+Default.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Foundation + +internal extension JSONDecoder { + static var `default`: JSONDecoder { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSZZZZZ" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(dateFormatter) + decoder.keyDecodingStrategy = .convertFromSnakeCase + + return decoder + } +} diff --git a/Sources/OllamaKit/Extensions/JSONEncoder+Default.swift b/Sources/OllamaKit/Extensions/JSONEncoder+Default.swift new file mode 100644 index 0000000..ff0a2e5 --- /dev/null +++ b/Sources/OllamaKit/Extensions/JSONEncoder+Default.swift @@ -0,0 +1,17 @@ +// +// JSONEncoder+Default.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Foundation + +internal extension JSONEncoder { + static var `default`: JSONEncoder { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + + return encoder + } +} diff --git a/Sources/OllamaKit/OllamaKit.swift b/Sources/OllamaKit/OllamaKit.swift new file mode 100644 index 0000000..ae9ad9a --- /dev/null +++ b/Sources/OllamaKit/OllamaKit.swift @@ -0,0 +1,124 @@ +// +// OllamaKit.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Alamofire +import Foundation + +/// A Swift library for interacting with the Ollama API. +/// +/// `OllamaKit` simplifies the process of connecting Swift applications to the Ollama API, abstracting the complexities of network requests and data handling. +public struct OllamaKit { + private var router: OKRouter.Type + private var decoder: JSONDecoder = .default + + /// Initializes a new instance of `OllamaKit` with the specified base URL for the Ollama API. + /// + /// This initializer configures `OllamaKit` with a base URL, laying the groundwork for all network interactions with the Ollama API. It ensures that the library is properly set up to communicate with the API endpoints. + /// + /// - Parameter baseURL: The base URL to be used for Ollama API requests. + public init(baseURL: URL) { + let router = OKRouter.self + router.baseURL = baseURL + + self.router = router + } +} + +extension OllamaKit { + /// Checks the reachability of the Ollama API. + /// + /// This asynchronous method performs a network request to determine if the Ollama API is reachable from the current client. It can be used to verify network connectivity and API availability before attempting further API interactions. + /// + /// - Returns: A Boolean value indicating whether the Ollama API is reachable (`true`) or not (`false`). + public func reachable() async -> Bool { + let request = AF.request(router.root).validate() + let response = request.serializingData() + + do { + _ = try await response.value + + return true + } catch { + return false + } + } +} + +extension OllamaKit { + /// Establishes a stream to the Ollama API for generating responses based on the provided data. + /// + /// This method continuously streams responses as they are generated by the Ollama API, + /// with the final response including detailed data about the generation process. + /// + /// - Parameters: + /// - data: The data used to generate the stream. + /// - stream: A closure that processes the streaming data. + public func generate(data: OKGenerateRequestData, stream: @escaping (DataStreamRequest.Stream) -> Void) { + let request = AF.streamRequest(router.generate(data: data)).validate() + request.responseStreamDecodable(of: OKGenerateResponse.self, stream: stream) + } +} + +extension OllamaKit { + /// Asynchronously retrieves a list of available models from the Ollama API. + /// + /// This method returns an `OKModelResponse` containing the details of the available models, + /// making it easy to understand what models are currently accessible. + /// + /// - Returns: An `OKModelResponse` object listing the available models. + public func models() async throws -> OKModelResponse { + let request = AF.request(router.models).validate() + let response = request.serializingDecodable(OKModelResponse.self, decoder: decoder) + + return try await response.value + } +} + +extension OllamaKit { + /// Asynchronously fetches detailed information about a specific model from the Ollama API. + /// + /// This method provides comprehensive details about the model, such as its modelfile, template, and parameters. + /// + /// - Parameter data: The data specifying the model to inquire about. + /// - Returns: An `OKModelInfoResponse` containing detailed information about the model. + public func modelInfo(data: OKModelInfoRequestData) async throws -> OKModelInfoResponse { + let request = AF.request(router.modelInfo(data: data)).validate() + let response = request.serializingDecodable(OKModelInfoResponse.self, decoder: decoder) + + return try await response.value + } +} + +extension OllamaKit { + /// Facilitates the duplication of an existing model, creating a new instance under a different name. + /// + /// This asynchronous method makes it straightforward to copy a model, requiring only the necessary parameters for the operation. + /// + /// - Parameter data: The data required for the model copy operation. + /// - Throws: An error if the copy operation fails. + public func copyModel(data: OKCopyModelRequestData) async throws -> Void { + let request = AF.request(router.copyModel(data: data)).validate() + let response = request.serializingData() + + _ = try await response.value + } +} + +extension OllamaKit { + /// Removes a specified model and its data from the Ollama API. + /// + /// This asynchronous method allows for the deletion of a model, requiring the model name to be specified for a successful operation. + /// + /// - Parameter data: The data specifying the model to be deleted. + /// - Throws: An error if the deletion fails. + public func deleteModel(data: OKDeleteModelRequestData) async throws -> Void { + let request = AF.request(router.deleteModel(data: data)).validate() + let response = request.serializingData() + + _ = try await response.value + } +} diff --git a/Sources/OllamaKit/RequestData/OKCopyModelRequestData.swift b/Sources/OllamaKit/RequestData/OKCopyModelRequestData.swift new file mode 100644 index 0000000..3b9ad6f --- /dev/null +++ b/Sources/OllamaKit/RequestData/OKCopyModelRequestData.swift @@ -0,0 +1,21 @@ +// +// OKCopyModelRequestData.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Foundation + +/// A structure representing the request data for copying a model via the Ollama API. +/// +/// This structure holds the information necessary to duplicate a model, including the source model's name and the desired destination name. +public struct OKCopyModelRequestData: Encodable { + public let source: String + public let destination: String + + public init(source: String, destination: String) { + self.source = source + self.destination = destination + } +} diff --git a/Sources/OllamaKit/RequestData/OKDeleteModelRequestData.swift b/Sources/OllamaKit/RequestData/OKDeleteModelRequestData.swift new file mode 100644 index 0000000..6864298 --- /dev/null +++ b/Sources/OllamaKit/RequestData/OKDeleteModelRequestData.swift @@ -0,0 +1,19 @@ +// +// OKDeleteModelRequestData.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Foundation + +/// A structure representing the request data for deleting a model through the Ollama API. +/// +/// This structure encapsulates the name of the model to be deleted, providing a straightforward way to specify which model should be removed. +public struct OKDeleteModelRequestData: Encodable { + public let name: String + + public init(name: String) { + self.name = name + } +} diff --git a/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift b/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift new file mode 100644 index 0000000..3c22f2f --- /dev/null +++ b/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift @@ -0,0 +1,53 @@ +// +// OKGenerateRequestData.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Foundation + +/// A structure representing the data required to generate responses from the Ollama API. +/// +/// It includes the model name, prompt, and other optional parameters that tailor the generation process, such as format and context. +public struct OKGenerateRequestData: Encodable { + private let stream: Bool + + public let model: String + public let prompt: String + public var format: Format? + public var system: String? + public var template: String? + public var options: Options? + public var context: [Int]? + public var raw: Bool? + + public enum Format: String, Encodable { + case json + } + + public struct Options: Encodable { + public var mirostat: Int? + public var mirostatEta: Double? + public var mirostatTau: Double? + public var numCtx: Int? + public var numGqa: Int? + public var numGpu: Int? + public var numThread: Int? + public var repeatLastN: Int? + public var repeatPenalty: Int? + public var temperature: Double? + public var seed: Int? + public var stop: String? + public var tfsZ: Double? + public var numPredict: Int? + public var topK: Int? + public var topP: Double? + } + + public init(model: String, prompt: String) { + self.stream = true + self.model = model + self.prompt = prompt + } +} diff --git a/Sources/OllamaKit/RequestData/OKModelInfoRequestData.swift b/Sources/OllamaKit/RequestData/OKModelInfoRequestData.swift new file mode 100644 index 0000000..df35a15 --- /dev/null +++ b/Sources/OllamaKit/RequestData/OKModelInfoRequestData.swift @@ -0,0 +1,19 @@ +// +// OKModelInfoRequestData.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Foundation + +/// A structure representing the request data for fetching model information from the Ollama API. +/// +/// This structure is used to specify the name of the model for which detailed information is requested. +public struct OKModelInfoRequestData: Encodable { + public let name: String + + public init(name: String) { + self.name = name + } +} diff --git a/Sources/OllamaKit/Responses/OKGenerateResponse.swift b/Sources/OllamaKit/Responses/OKGenerateResponse.swift new file mode 100644 index 0000000..e7fd3fc --- /dev/null +++ b/Sources/OllamaKit/Responses/OKGenerateResponse.swift @@ -0,0 +1,25 @@ +// +// OKGenerateResponse.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Foundation + +/// A structure representing the response from a generate request to the Ollama API. +/// +/// Contains details of the generation process, including the model used, response content, and various performance metrics. +public struct OKGenerateResponse: Decodable { + public let model: String + public let createdAt: Date + public let response: String + public let done: Bool + public let context: [Int]? + public let totalDuration: Int? + public let loadDuration: Int? + public let promptEvalCount: Int? + public let promptEvalDuration: Int? + public let evalCount: Int? + public let evalDuration: Int? +} diff --git a/Sources/OllamaKit/Responses/OKModelInfoResponse.swift b/Sources/OllamaKit/Responses/OKModelInfoResponse.swift new file mode 100644 index 0000000..fd4fe19 --- /dev/null +++ b/Sources/OllamaKit/Responses/OKModelInfoResponse.swift @@ -0,0 +1,18 @@ +// +// OKModelInfoResponse.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Foundation + +/// A structure encapsulating detailed information about a specific model from the Ollama API. +/// +/// Includes the model's license, template, modelfile, and operational parameters. +public struct OKModelInfoResponse: Decodable { + public let license: String + public let template: String + public let modelfile: String + public let parameters: String +} diff --git a/Sources/OllamaKit/Responses/OKModelResponse.swift b/Sources/OllamaKit/Responses/OKModelResponse.swift new file mode 100644 index 0000000..1ddc63d --- /dev/null +++ b/Sources/OllamaKit/Responses/OKModelResponse.swift @@ -0,0 +1,22 @@ +// +// OKModelResponse.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Foundation + +/// A structure representing a list of models available through the Ollama API. +/// +/// Contains an array of `OKModelResponse.Model` structures, each detailing a specific model's name, digest, size, and last modification date. +public struct OKModelResponse: Decodable { + public let models: [Model] + + public struct Model: Decodable { + public let name: String + public let digest: String + public let size: Int + public let modifiedAt: Date + } +} diff --git a/Sources/OllamaKit/Utils/OKRouter.swift b/Sources/OllamaKit/Utils/OKRouter.swift new file mode 100644 index 0000000..72952b0 --- /dev/null +++ b/Sources/OllamaKit/Utils/OKRouter.swift @@ -0,0 +1,82 @@ +// +// OKRouter.swift +// +// +// Created by Kevin Hermawan on 10/11/23. +// + +import Alamofire +import Foundation + +internal enum OKRouter { + static var baseURL = URL(string: "http://localhost:11434")! + + case root + case models + case modelInfo(data: OKModelInfoRequestData) + case generate(data: OKGenerateRequestData) + case copyModel(data: OKCopyModelRequestData) + case deleteModel(data: OKDeleteModelRequestData) + + internal var path: String { + switch self { + case .root: + return "/" + case .models: + return "/api/tags" + case .modelInfo: + return "/api/show" + case .generate: + return "/api/generate" + case .copyModel: + return "/api/copy" + case .deleteModel: + return "/api/delete" + } + } + + internal var method: HTTPMethod { + switch self { + case .root: + return .head + case .models: + return .get + case .modelInfo: + return .post + case .generate: + return .post + case .copyModel: + return .post + case .deleteModel: + return .delete + } + } + + internal var headers: HTTPHeaders { + ["Content-Type": "application/json"] + } +} + +extension OKRouter: URLRequestConvertible { + func asURLRequest() throws -> URLRequest { + let url = OKRouter.baseURL.appendingPathComponent(path) + var request = URLRequest(url: url) + request.method = method + request.headers = headers + + switch self { + case .modelInfo(let data): + request.httpBody = try JSONEncoder.default.encode(data) + case .generate(let data): + request.httpBody = try JSONEncoder.default.encode(data) + case .copyModel(let data): + request.httpBody = try JSONEncoder.default.encode(data) + case .deleteModel(let data): + request.httpBody = try JSONEncoder.default.encode(data) + default: + break + } + + return request + } +} diff --git a/Tests/OllamaKitTests/OllamaKitTests.swift b/Tests/OllamaKitTests/OllamaKitTests.swift new file mode 100644 index 0000000..a5576e0 --- /dev/null +++ b/Tests/OllamaKitTests/OllamaKitTests.swift @@ -0,0 +1,53 @@ +import XCTest +@testable import OllamaKit +import Alamofire + +final class OllamaKitTests: XCTestCase { + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testGenerateSuccess() async throws { + + } + + func testGenerateFailure() async throws { + + } + + func testModelsSuccess() async throws { + + } + + func testModelsFailure() async throws { + + } + + func testModelInfoSuccess() async throws { + + } + + func testModelInfoFailure() async throws { + + } + + func testCopyModelSuccess() async throws { + + } + + func testCopyModelFailure() async throws { + + } + + func testDeleteModelSuccess() async throws { + + } + + func testDeleteModelFailure() async throws { + + } +}