From df165e0a7150aa269263b077a4a7759992ad4ab0 Mon Sep 17 00:00:00 2001 From: 1amageek Date: Thu, 17 Oct 2024 14:19:10 +0900 Subject: [PATCH 01/11] Update Swift tools version and add baseURL parameter --- Package.swift | 2 +- Sources/OllamaKit/OllamaKit+Chat.swift | 8 ++++++-- Sources/OllamaKit/OllamaKit+CopyModel.swift | 8 ++++++-- Sources/OllamaKit/OllamaKit+DeleteModel.swift | 8 ++++++-- Sources/OllamaKit/OllamaKit+Embeddings.swift | 8 ++++++-- Sources/OllamaKit/OllamaKit+Generate.swift | 8 ++++++-- Sources/OllamaKit/OllamaKit+ModelInfo.swift | 8 ++++++-- Sources/OllamaKit/OllamaKit+Models.swift | 8 ++++++-- Sources/OllamaKit/OllamaKit+Reachable.swift | 8 ++++++-- Sources/OllamaKit/OllamaKit.swift | 14 +++----------- .../Completion/OKCompletionOptions.swift | 2 +- .../OllamaKit/RequestData/OKChatRequestData.swift | 6 +++--- .../RequestData/OKCopyModelRequestData.swift | 2 +- .../RequestData/OKDeleteModelRequestData.swift | 2 +- .../RequestData/OKEmbeddingsRequestData.swift | 2 +- .../RequestData/OKGenerateRequestData.swift | 2 +- .../RequestData/OKModelInfoRequestData.swift | 2 +- .../Completion/OKCompletionResponse.swift | 2 +- Sources/OllamaKit/Responses/OKChatResponse.swift | 8 ++++---- .../OllamaKit/Responses/OKEmbeddingsResponse.swift | 2 +- .../OllamaKit/Responses/OKGenerateResponse.swift | 2 +- .../OllamaKit/Responses/OKModelInfoResponse.swift | 2 +- Sources/OllamaKit/Responses/OKModelResponse.swift | 4 ++-- Sources/OllamaKit/Utils/OKHTTPClient.swift | 2 +- Sources/OllamaKit/Utils/OKJSONValue.swift | 2 +- Sources/OllamaKit/Utils/OKRouter.swift | 6 +++--- Sources/OllamaKit/Utils/StreamingDelegate.swift | 4 ++-- 27 files changed, 78 insertions(+), 54 deletions(-) diff --git a/Package.swift b/Package.swift index 1177966..9a0a5f8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/OllamaKit/OllamaKit+Chat.swift b/Sources/OllamaKit/OllamaKit+Chat.swift index 475d784..712d490 100644 --- a/Sources/OllamaKit/OllamaKit+Chat.swift +++ b/Sources/OllamaKit/OllamaKit+Chat.swift @@ -100,7 +100,9 @@ extension OllamaKit { /// - Returns: An `AsyncThrowingStream` emitting the live stream of chat responses from the Ollama API. public func chat(data: OKChatRequestData) -> AsyncThrowingStream { do { - let request = try OKRouter.chat(data: data).asURLRequest() + let request = try OKRouter.chat(data: data).asURLRequest( + baseURL: baseURL + ) return OKHTTPClient.shared.stream(request: request, with: OKChatResponse.self) } catch { @@ -197,7 +199,9 @@ extension OllamaKit { /// - Returns: An `AnyPublisher` emitting the live stream of chat responses from the Ollama API. public func chat(data: OKChatRequestData) -> AnyPublisher { do { - let request = try OKRouter.chat(data: data).asURLRequest() + let request = try OKRouter.chat(data: data).asURLRequest( + baseURL: baseURL + ) return OKHTTPClient.shared.stream(request: request, with: OKChatResponse.self) } catch { diff --git a/Sources/OllamaKit/OllamaKit+CopyModel.swift b/Sources/OllamaKit/OllamaKit+CopyModel.swift index 268a170..b68271d 100644 --- a/Sources/OllamaKit/OllamaKit+CopyModel.swift +++ b/Sources/OllamaKit/OllamaKit+CopyModel.swift @@ -22,7 +22,9 @@ extension OllamaKit { /// - Parameter data: The ``OKCopyModelRequestData`` containing the details needed to copy the model. /// - Throws: An error if the request to copy the model fails. public func copyModel(data: OKCopyModelRequestData) async throws -> Void { - let request = try OKRouter.copyModel(data: data).asURLRequest() + let request = try OKRouter.copyModel(data: data).asURLRequest( + baseURL: baseURL + ) try await OKHTTPClient.shared.send(request: request) } @@ -48,7 +50,9 @@ extension OllamaKit { /// - Returns: A `AnyPublisher` that completes when the copy operation is done. public func copyModel(data: OKCopyModelRequestData) -> AnyPublisher { do { - let request = try OKRouter.copyModel(data: data).asURLRequest() + let request = try OKRouter.copyModel(data: data).asURLRequest( + baseURL: baseURL + ) return OKHTTPClient.shared.send(request: request) } catch { diff --git a/Sources/OllamaKit/OllamaKit+DeleteModel.swift b/Sources/OllamaKit/OllamaKit+DeleteModel.swift index 55abcd1..0320ce8 100644 --- a/Sources/OllamaKit/OllamaKit+DeleteModel.swift +++ b/Sources/OllamaKit/OllamaKit+DeleteModel.swift @@ -22,7 +22,9 @@ extension OllamaKit { /// - Parameter data: The ``OKDeleteModelRequestData`` containing the details needed to delete the model. /// - Throws: An error if the request to delete the model fails. public func deleteModel(data: OKDeleteModelRequestData) async throws -> Void { - let request = try OKRouter.deleteModel(data: data).asURLRequest() + let request = try OKRouter.deleteModel(data: data).asURLRequest( + baseURL: baseURL + ) try await OKHTTPClient.shared.send(request: request) } @@ -48,7 +50,9 @@ extension OllamaKit { /// - Returns: A `AnyPublisher` that completes when the deletion operation is done. public func deleteModel(data: OKDeleteModelRequestData) -> AnyPublisher { do { - let request = try OKRouter.deleteModel(data: data).asURLRequest() + let request = try OKRouter.deleteModel(data: data).asURLRequest( + baseURL: baseURL + ) return OKHTTPClient.shared.send(request: request) } catch { diff --git a/Sources/OllamaKit/OllamaKit+Embeddings.swift b/Sources/OllamaKit/OllamaKit+Embeddings.swift index c16f76a..1cdc537 100644 --- a/Sources/OllamaKit/OllamaKit+Embeddings.swift +++ b/Sources/OllamaKit/OllamaKit+Embeddings.swift @@ -23,7 +23,9 @@ extension OllamaKit { /// - Returns: An ``OKEmbeddingsResponse`` containing the embeddings from the model. /// - Throws: An error if the request fails or the response can't be decoded. public func embeddings(data: OKEmbeddingsRequestData) async throws -> OKEmbeddingsResponse { - let request = try OKRouter.embeddings(data: data).asURLRequest() + let request = try OKRouter.embeddings(data: data).asURLRequest( + baseURL: baseURL + ) return try await OKHTTPClient.shared.send(request: request, with: OKEmbeddingsResponse.self) } @@ -49,7 +51,9 @@ extension OllamaKit { /// - Returns: A `AnyPublisher` that emits embeddings. public func embeddings(data: OKEmbeddingsRequestData) -> AnyPublisher { do { - let request = try OKRouter.embeddings(data: data).asURLRequest() + let request = try OKRouter.embeddings(data: data).asURLRequest( + baseURL: baseURL + ) return OKHTTPClient.shared.send(request: request, with: OKEmbeddingsResponse.self) } catch { diff --git a/Sources/OllamaKit/OllamaKit+Generate.swift b/Sources/OllamaKit/OllamaKit+Generate.swift index b2eeeac..c2044c4 100644 --- a/Sources/OllamaKit/OllamaKit+Generate.swift +++ b/Sources/OllamaKit/OllamaKit+Generate.swift @@ -32,7 +32,9 @@ extension OllamaKit { /// - Returns: An `AsyncThrowingStream` emitting the live stream of responses from the Ollama API. public func generate(data: OKGenerateRequestData) -> AsyncThrowingStream { do { - let request = try OKRouter.generate(data: data).asURLRequest() + let request = try OKRouter.generate(data: data).asURLRequest( + baseURL: baseURL + ) return OKHTTPClient.shared.stream(request: request, with: OKGenerateResponse.self) } catch { @@ -63,7 +65,9 @@ extension OllamaKit { /// - Returns: An `AnyPublisher` emitting the live stream of responses from the Ollama API. public func generate(data: OKGenerateRequestData) -> AnyPublisher { do { - let request = try OKRouter.generate(data: data).asURLRequest() + let request = try OKRouter.generate(data: data).asURLRequest( + baseURL: baseURL + ) return OKHTTPClient.shared.stream(request: request, with: OKGenerateResponse.self) } catch { diff --git a/Sources/OllamaKit/OllamaKit+ModelInfo.swift b/Sources/OllamaKit/OllamaKit+ModelInfo.swift index aae471b..9e9c9b7 100644 --- a/Sources/OllamaKit/OllamaKit+ModelInfo.swift +++ b/Sources/OllamaKit/OllamaKit+ModelInfo.swift @@ -23,7 +23,9 @@ extension OllamaKit { /// - Returns: An ``OKModelInfoResponse`` containing detailed information about the model. /// - Throws: An error if the request fails or the response can't be decoded. public func modelInfo(data: OKModelInfoRequestData) async throws -> OKModelInfoResponse { - let request = try OKRouter.modelInfo(data: data).asURLRequest() + let request = try OKRouter.modelInfo(data: data).asURLRequest( + baseURL: baseURL + ) return try await OKHTTPClient.shared.send(request: request, with: OKModelInfoResponse.self) } @@ -49,7 +51,9 @@ extension OllamaKit { /// - Returns: A `AnyPublisher` that emits detailed information about the model. public func modelInfo(data: OKModelInfoRequestData) -> AnyPublisher { do { - let request = try OKRouter.modelInfo(data: data).asURLRequest() + let request = try OKRouter.modelInfo(data: data).asURLRequest( + baseURL: baseURL + ) return OKHTTPClient.shared.send(request: request, with: OKModelInfoResponse.self) } catch { diff --git a/Sources/OllamaKit/OllamaKit+Models.swift b/Sources/OllamaKit/OllamaKit+Models.swift index 4d253a3..39ec744 100644 --- a/Sources/OllamaKit/OllamaKit+Models.swift +++ b/Sources/OllamaKit/OllamaKit+Models.swift @@ -21,7 +21,9 @@ extension OllamaKit { /// - Returns: An ``OKModelResponse`` object listing the available models. /// - Throws: An error if the request fails or the response can't be decoded. public func models() async throws -> OKModelResponse { - let request = try OKRouter.models.asURLRequest() + let request = try OKRouter.models.asURLRequest( + baseURL: baseURL + ) return try await OKHTTPClient.shared.send(request: request, with: OKModelResponse.self) } @@ -45,7 +47,9 @@ extension OllamaKit { /// - Returns: A `AnyPublisher` that emits the list of available models. public func models() -> AnyPublisher { do { - let request = try OKRouter.models.asURLRequest() + let request = try OKRouter.models.asURLRequest( + baseURL: baseURL + ) return OKHTTPClient.shared.send(request: request, with: OKModelResponse.self) } catch { diff --git a/Sources/OllamaKit/OllamaKit+Reachable.swift b/Sources/OllamaKit/OllamaKit+Reachable.swift index 926688e..cecc031 100644 --- a/Sources/OllamaKit/OllamaKit+Reachable.swift +++ b/Sources/OllamaKit/OllamaKit+Reachable.swift @@ -21,7 +21,9 @@ extension OllamaKit { /// - Returns: `true` if the Ollama API is reachable, `false` otherwise. public func reachable() async -> Bool { do { - let request = try OKRouter.root.asURLRequest() + let request = try OKRouter.root.asURLRequest( + baseURL: baseURL + ) try await OKHTTPClient.shared.send(request: request) return true @@ -47,7 +49,9 @@ extension OllamaKit { /// - Returns: A `AnyPublisher` that emits `true` if the API is reachable, `false` otherwise. public func reachable() -> AnyPublisher { do { - let request = try OKRouter.root.asURLRequest() + let request = try OKRouter.root.asURLRequest( + baseURL: baseURL + ) return OKHTTPClient.shared.send(request: request) .map { _ in true } diff --git a/Sources/OllamaKit/OllamaKit.swift b/Sources/OllamaKit/OllamaKit.swift index 4312913..8168d4a 100644 --- a/Sources/OllamaKit/OllamaKit.swift +++ b/Sources/OllamaKit/OllamaKit.swift @@ -9,7 +9,7 @@ import Foundation /// Provides a streamlined way to access the Ollama API, encapsulating the complexities of network communication and data processing. public struct OllamaKit { - var router: OKRouter.Type + var baseURL: URL = URL(string: "http://localhost:11434")! var decoder: JSONDecoder = .default /// Initializes a new instance of `OllamaKit` with the default base URL for the Ollama API. @@ -17,12 +17,7 @@ public struct OllamaKit { /// ```swift /// let ollamaKit = OllamaKit() /// ``` - public init() { - let router = OKRouter.self - router.baseURL = URL(string: "http://localhost:11434")! - - self.router = router - } + public init() { } /// Initializes a new instance of `OllamaKit` with a custom base URL for the Ollama API. /// @@ -33,9 +28,6 @@ public struct OllamaKit { /// /// - Parameter baseURL: The base URL to use for API requests. public init(baseURL: URL) { - let router = OKRouter.self - router.baseURL = baseURL - - self.router = router + self.baseURL = baseURL } } diff --git a/Sources/OllamaKit/RequestData/Completion/OKCompletionOptions.swift b/Sources/OllamaKit/RequestData/Completion/OKCompletionOptions.swift index 260bab3..7dbdad0 100644 --- a/Sources/OllamaKit/RequestData/Completion/OKCompletionOptions.swift +++ b/Sources/OllamaKit/RequestData/Completion/OKCompletionOptions.swift @@ -8,7 +8,7 @@ import Foundation // A structure that encapsulates options for controlling the behavior of content generation in the Ollama API. -public struct OKCompletionOptions: Encodable { +public struct OKCompletionOptions: Encodable, Sendable { /// Optional integer to enable Mirostat sampling for controlling perplexity. /// (0 = disabled, 1 = Mirostat, 2 = Mirostat 2.0) /// Mirostat sampling helps regulate the unpredictability of the output, diff --git a/Sources/OllamaKit/RequestData/OKChatRequestData.swift b/Sources/OllamaKit/RequestData/OKChatRequestData.swift index 75e683e..1c98611 100644 --- a/Sources/OllamaKit/RequestData/OKChatRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKChatRequestData.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates data for chat requests to the Ollama API. -public struct OKChatRequestData { +public struct OKChatRequestData: Sendable { private let stream: Bool /// A string representing the model identifier to be used for the chat session. @@ -31,7 +31,7 @@ public struct OKChatRequestData { } /// A structure that represents a single message in the chat request. - public struct Message: Encodable { + public struct Message: Encodable, Sendable { /// A ``Role`` value indicating the sender of the message (system, assistant, user). public let role: Role @@ -48,7 +48,7 @@ public struct OKChatRequestData { } /// An enumeration that represents the role of the message sender. - public enum Role: String, Encodable { + public enum Role: String, Encodable, Sendable { /// Indicates the message is from the system. case system diff --git a/Sources/OllamaKit/RequestData/OKCopyModelRequestData.swift b/Sources/OllamaKit/RequestData/OKCopyModelRequestData.swift index c2132ad..5ed683f 100644 --- a/Sources/OllamaKit/RequestData/OKCopyModelRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKCopyModelRequestData.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates the necessary data to request a model copy operation in the Ollama API. -public struct OKCopyModelRequestData: Encodable { +public struct OKCopyModelRequestData: Encodable, Sendable { /// A string representing the identifier of the source model to be copied. public let source: String diff --git a/Sources/OllamaKit/RequestData/OKDeleteModelRequestData.swift b/Sources/OllamaKit/RequestData/OKDeleteModelRequestData.swift index 37f01d0..0b88217 100644 --- a/Sources/OllamaKit/RequestData/OKDeleteModelRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKDeleteModelRequestData.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates the necessary data to request a model deletion in the Ollama API. -public struct OKDeleteModelRequestData: Encodable { +public struct OKDeleteModelRequestData: Encodable, Sendable { /// A string representing the identifier of the model to be deleted. public let name: String diff --git a/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift b/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift index 1aa8774..5c3101e 100644 --- a/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates the data required for generating embeddings using the Ollama API. -public struct OKEmbeddingsRequestData: Encodable { +public struct OKEmbeddingsRequestData: Encodable, Sendable { /// A string representing the identifier of the model. public let model: String diff --git a/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift b/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift index d3a2100..0516aec 100644 --- a/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates the data required for generating responses using the Ollama API. -public struct OKGenerateRequestData { +public struct OKGenerateRequestData: Sendable { private let stream: Bool /// A string representing the identifier of the model. diff --git a/Sources/OllamaKit/RequestData/OKModelInfoRequestData.swift b/Sources/OllamaKit/RequestData/OKModelInfoRequestData.swift index 90095ce..ab8d156 100644 --- a/Sources/OllamaKit/RequestData/OKModelInfoRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKModelInfoRequestData.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that encapsulates the data necessary for requesting information about a specific model from the Ollama API. -public struct OKModelInfoRequestData: Encodable { +public struct OKModelInfoRequestData: Encodable, Sendable { /// A string representing the identifier of the model for which information is requested. public let name: String diff --git a/Sources/OllamaKit/Responses/Completion/OKCompletionResponse.swift b/Sources/OllamaKit/Responses/Completion/OKCompletionResponse.swift index f51cbec..df0b17a 100644 --- a/Sources/OllamaKit/Responses/Completion/OKCompletionResponse.swift +++ b/Sources/OllamaKit/Responses/Completion/OKCompletionResponse.swift @@ -8,7 +8,7 @@ import Foundation /// A protocol that defines the response structure for a completion request in the Ollama API. -protocol OKCompletionResponse: Decodable { +protocol OKCompletionResponse: Decodable, Sendable { /// The identifier of the model used for generating the response. var model: String { get } diff --git a/Sources/OllamaKit/Responses/OKChatResponse.swift b/Sources/OllamaKit/Responses/OKChatResponse.swift index 662eba3..2aaa3d8 100644 --- a/Sources/OllamaKit/Responses/OKChatResponse.swift +++ b/Sources/OllamaKit/Responses/OKChatResponse.swift @@ -44,7 +44,7 @@ public struct OKChatResponse: OKCompletionResponse, Decodable { public let evalDuration: Int? /// A structure that represents a single response message. - public struct Message: Decodable { + public struct Message: Decodable, Sendable { /// The role of the message sender (system, assistant, user). public var role: Role @@ -55,7 +55,7 @@ public struct OKChatResponse: OKCompletionResponse, Decodable { public var toolCalls: [ToolCall]? /// An enumeration representing the role of the message sender. - public enum Role: String, Decodable { + public enum Role: String, Decodable, Sendable { /// The message is from the system. case system @@ -67,12 +67,12 @@ public struct OKChatResponse: OKCompletionResponse, Decodable { } /// A structure that represents a tool call in the response. - public struct ToolCall: Decodable { + public struct ToolCall: Decodable, Sendable { /// An optional ``Function`` structure representing the details of the tool call. public let function: Function? /// A structure that represents the details of a tool call. - public struct Function: Decodable { + public struct Function: Decodable, Sendable { /// The name of the tool being called. public let name: String? diff --git a/Sources/OllamaKit/Responses/OKEmbeddingsResponse.swift b/Sources/OllamaKit/Responses/OKEmbeddingsResponse.swift index a85d82c..fe7f6b0 100644 --- a/Sources/OllamaKit/Responses/OKEmbeddingsResponse.swift +++ b/Sources/OllamaKit/Responses/OKEmbeddingsResponse.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents the response to an embedding request from the Ollama API. -public struct OKEmbeddingsResponse: Decodable { +public struct OKEmbeddingsResponse: Decodable, Sendable { /// An array of doubles representing the embeddings of the input prompt. public let embedding: [Double]? diff --git a/Sources/OllamaKit/Responses/OKGenerateResponse.swift b/Sources/OllamaKit/Responses/OKGenerateResponse.swift index 02f0095..f1de025 100644 --- a/Sources/OllamaKit/Responses/OKGenerateResponse.swift +++ b/Sources/OllamaKit/Responses/OKGenerateResponse.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents the response to a content generation request from the Ollama API. -public struct OKGenerateResponse: OKCompletionResponse, Decodable { +public struct OKGenerateResponse: OKCompletionResponse, Decodable, Sendable { /// The identifier of the model used for generating the content. public let model: String diff --git a/Sources/OllamaKit/Responses/OKModelInfoResponse.swift b/Sources/OllamaKit/Responses/OKModelInfoResponse.swift index 947bfef..5131f7a 100644 --- a/Sources/OllamaKit/Responses/OKModelInfoResponse.swift +++ b/Sources/OllamaKit/Responses/OKModelInfoResponse.swift @@ -8,7 +8,7 @@ import Foundation /// A structure that represents the response containing information about a specific model from the Ollama API. -public struct OKModelInfoResponse: Decodable { +public struct OKModelInfoResponse: Decodable, Sendable { /// A string detailing the licensing information for the model. public let license: String diff --git a/Sources/OllamaKit/Responses/OKModelResponse.swift b/Sources/OllamaKit/Responses/OKModelResponse.swift index 2661bc1..36b5ef8 100644 --- a/Sources/OllamaKit/Responses/OKModelResponse.swift +++ b/Sources/OllamaKit/Responses/OKModelResponse.swift @@ -8,12 +8,12 @@ import Foundation /// A structure that represents the available models from the Ollama API. -public struct OKModelResponse: Decodable { +public struct OKModelResponse: Decodable, Sendable { /// An array of ``Model`` instances, each representing a specific model available in the Ollama API. public let models: [Model] /// A structure that details individual models. - public struct Model: Decodable { + public struct Model: Decodable, Sendable { /// A string representing the name of the model. public let name: String diff --git a/Sources/OllamaKit/Utils/OKHTTPClient.swift b/Sources/OllamaKit/Utils/OKHTTPClient.swift index eafb8d8..f84809a 100644 --- a/Sources/OllamaKit/Utils/OKHTTPClient.swift +++ b/Sources/OllamaKit/Utils/OKHTTPClient.swift @@ -8,7 +8,7 @@ import Combine import Foundation -internal struct OKHTTPClient { +internal struct OKHTTPClient: Sendable { private let decoder: JSONDecoder = .default static let shared = OKHTTPClient() } diff --git a/Sources/OllamaKit/Utils/OKJSONValue.swift b/Sources/OllamaKit/Utils/OKJSONValue.swift index 634c159..32032d5 100644 --- a/Sources/OllamaKit/Utils/OKJSONValue.swift +++ b/Sources/OllamaKit/Utils/OKJSONValue.swift @@ -7,7 +7,7 @@ import Foundation -public enum OKJSONValue: Codable { +public enum OKJSONValue: Codable, Sendable { case string(String) case number(Double) case integer(Int) diff --git a/Sources/OllamaKit/Utils/OKRouter.swift b/Sources/OllamaKit/Utils/OKRouter.swift index a3e9e8b..8f9c719 100644 --- a/Sources/OllamaKit/Utils/OKRouter.swift +++ b/Sources/OllamaKit/Utils/OKRouter.swift @@ -8,7 +8,7 @@ import Foundation internal enum OKRouter { - static var baseURL = URL(string: "http://localhost:11434")! + case root case models @@ -67,8 +67,8 @@ internal enum OKRouter { } extension OKRouter { - func asURLRequest() throws -> URLRequest { - let url = OKRouter.baseURL.appendingPathComponent(path) + func asURLRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL.appendingPathComponent(path) var request = URLRequest(url: url) request.httpMethod = method diff --git a/Sources/OllamaKit/Utils/StreamingDelegate.swift b/Sources/OllamaKit/Utils/StreamingDelegate.swift index 493613d..a0ea39d 100644 --- a/Sources/OllamaKit/Utils/StreamingDelegate.swift +++ b/Sources/OllamaKit/Utils/StreamingDelegate.swift @@ -5,10 +5,10 @@ // Created by Kevin Hermawan on 09/06/24. // -import Combine +@preconcurrency import Combine import Foundation -internal class StreamingDelegate: NSObject, URLSessionDataDelegate { +internal class StreamingDelegate: NSObject, URLSessionDataDelegate, @unchecked Sendable { private let subject = PassthroughSubject() func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { From a2c146c13a6b5e518efbd5993ab3c251cbffe9b7 Mon Sep 17 00:00:00 2001 From: 1amageek Date: Sun, 20 Oct 2024 23:15:48 +0900 Subject: [PATCH 02/11] Add options parameter to request data initializers --- .../RequestData/OKChatRequestData.swift | 23 ++++++++++++++- .../RequestData/OKEmbeddingsRequestData.swift | 16 +++++++++- .../RequestData/OKGenerateRequestData.swift | 29 +++++++++++++++++-- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/Sources/OllamaKit/RequestData/OKChatRequestData.swift b/Sources/OllamaKit/RequestData/OKChatRequestData.swift index 1c98611..6fe4d28 100644 --- a/Sources/OllamaKit/RequestData/OKChatRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKChatRequestData.swift @@ -23,11 +23,32 @@ public struct OKChatRequestData: Sendable { /// Optional ``OKCompletionOptions`` providing additional configuration for the chat request. public var options: OKCompletionOptions? - public init(model: String, messages: [Message], tools: [OKJSONValue]? = nil) { + public init( + model: String, + messages: [Message], + tools: [OKJSONValue]? = nil, + options: OKCompletionOptions? = nil + ) { self.stream = tools == nil self.model = model self.messages = messages self.tools = tools + self.options = options + } + + public init( + model: String, + messages: [Message], + tools: [OKJSONValue]? = nil, + with configureOptions: @Sendable (inout OKCompletionOptions) -> Void = { _ in () } + ) { + self.stream = tools == nil + self.model = model + self.messages = messages + self.tools = tools + var options = OKCompletionOptions() + configureOptions(&options) + self.options = options } /// A structure that represents a single message in the chat request. diff --git a/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift b/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift index 5c3101e..532a95a 100644 --- a/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift @@ -21,8 +21,22 @@ public struct OKEmbeddingsRequestData: Encodable, Sendable { /// Optionally control how long the model will stay loaded into memory following the request (default: 5m) public var keepAlive: String? - public init(model: String, prompt: String) { + public init(model: String, prompt: String, options: OKCompletionOptions? = nil, keepAlive: String? = nil) { self.model = model self.prompt = prompt + self.options = options + self.keepAlive = keepAlive + } + + public init( + model: String, + prompt: String, + with configureOptions: @Sendable (inout OKCompletionOptions) -> Void = { _ in () } + ) { + self.model = model + self.prompt = prompt + var options = OKCompletionOptions() + configureOptions(&options) + self.options = options } } diff --git a/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift b/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift index 0516aec..55ff27f 100644 --- a/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift @@ -17,7 +17,7 @@ public struct OKGenerateRequestData: Sendable { /// A string containing the initial input or prompt. public let prompt: String - /// /// An optional array of base64-encoded images. + /// An optional array of base64-encoded images. public let images: [String]? /// An optional string specifying the system message. @@ -29,11 +29,36 @@ public struct OKGenerateRequestData: Sendable { /// Optional ``OKCompletionOptions`` providing additional configuration for the generation request. public var options: OKCompletionOptions? - public init(model: String, prompt: String, images: [String]? = nil) { + public init( + model: String, + prompt: String, + images: [String]? = nil, + system: String? = nil, + context: [Int]? = nil, + options: OKCompletionOptions? = nil + ) { self.stream = true self.model = model self.prompt = prompt self.images = images + self.system = system + self.context = context + self.options = options + } + + public init( + model: String, + prompt: String, + images: [String]? = nil, + with configureOptions: @Sendable (inout OKCompletionOptions) -> Void = { _ in () } + ) { + self.stream = true + self.model = model + self.prompt = prompt + self.images = images + var options = OKCompletionOptions() + configureOptions(&options) + self.options = options } } From e19b5cf3d1bd723d43ed425dbc3768d61aaa9aad Mon Sep 17 00:00:00 2001 From: 1amageek Date: Mon, 4 Nov 2024 22:48:26 +0900 Subject: [PATCH 03/11] Change Double properties to Float for consistency --- Playground/OKPlayground/Views/ChatView.swift | 2 +- .../Completion/OKCompletionOptions.swift | 16 ++++++++-------- .../Responses/OKEmbeddingsResponse.swift | 2 +- Sources/OllamaKit/Utils/OKJSONValue.swift | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Playground/OKPlayground/Views/ChatView.swift b/Playground/OKPlayground/Views/ChatView.swift index e6a91f1..09e73b5 100644 --- a/Playground/OKPlayground/Views/ChatView.swift +++ b/Playground/OKPlayground/Views/ChatView.swift @@ -13,7 +13,7 @@ struct ChatView: View { @Environment(ViewModel.self) private var viewModel @State private var model: String? = nil - @State private var temperature: Double = 0.5 + @State private var temperature: Float = 0.5 @State private var prompt = "" @State private var response = "" @State private var cancellables = Set() diff --git a/Sources/OllamaKit/RequestData/Completion/OKCompletionOptions.swift b/Sources/OllamaKit/RequestData/Completion/OKCompletionOptions.swift index 7dbdad0..665bbbe 100644 --- a/Sources/OllamaKit/RequestData/Completion/OKCompletionOptions.swift +++ b/Sources/OllamaKit/RequestData/Completion/OKCompletionOptions.swift @@ -19,13 +19,13 @@ public struct OKCompletionOptions: Encodable, Sendable { /// (Lower values result in slower adjustments, higher values increase responsiveness.) /// This parameter, `mirostatEta`, adjusts how quickly the algorithm reacts to feedback /// from the generated text. A default value of 0.1 provides a moderate adjustment speed. - public var mirostatEta: Double? + public var mirostatEta: Float? /// Optional double controlling the balance between coherence and diversity. /// (Lower values lead to more focused and coherent text) /// The `mirostatTau` parameter sets the target perplexity level, influencing how /// creative or constrained the text generation should be. Default is 5.0. - public var mirostatTau: Double? + public var mirostatTau: Float? /// Optional integer setting the size of the context window for token generation. /// This defines the number of previous tokens the model considers when generating new tokens. @@ -40,13 +40,13 @@ public struct OKCompletionOptions: Encodable, Sendable { /// Optional double setting the penalty strength for repetitions. /// A higher value increases the penalty for repeated tokens, discouraging repetition. /// The default value is 1.1, providing moderate repetition control. - public var repeatPenalty: Double? + public var repeatPenalty: Float? /// Optional double to control the model's creativity. /// (Higher values increase creativity and randomness) /// The `temperature` parameter adjusts the randomness of predictions; higher values /// like 0.8 make outputs more creative and diverse. The default is 0.7. - public var temperature: Double? + public var temperature: Float? /// Optional integer for setting a random number seed for generation consistency. /// Specifying a seed ensures the same output for the same prompt and parameters, @@ -61,7 +61,7 @@ public struct OKCompletionOptions: Encodable, Sendable { /// Optional double for tail free sampling, reducing impact of less probable tokens. /// `tfsZ` adjusts how much the model avoids unlikely tokens, with higher values /// reducing their influence. A value of 1.0 disables this feature. - public var tfsZ: Double? + public var tfsZ: Float? /// Optional integer for the maximum number of tokens to predict. /// `numPredict` sets the upper limit for the number of tokens to generate. @@ -76,14 +76,14 @@ public struct OKCompletionOptions: Encodable, Sendable { /// Optional double working with top-k to balance text diversity and focus. /// `topP` (nucleus sampling) retains tokens that cumulatively account for a certain /// probability mass, adding flexibility beyond `topK`. A value like 0.9 increases diversity. - public var topP: Double? + public var topP: Float? /// Optional double for the minimum probability threshold for token inclusion. /// `minP` ensures that tokens below a certain probability threshold are excluded, /// focusing the model's output on more probable sequences. Default is 0.0, meaning no filtering. - public var minP: Double? + public var minP: Float? - public init(mirostat: Int? = nil, mirostatEta: Double? = nil, mirostatTau: Double? = nil, numCtx: Int? = nil, repeatLastN: Int? = nil, repeatPenalty: Double? = nil, temperature: Double? = nil, seed: Int? = nil, stop: String? = nil, tfsZ: Double? = nil, numPredict: Int? = nil, topK: Int? = nil, topP: Double? = nil, minP: Double? = nil) { + public init(mirostat: Int? = nil, mirostatEta: Float? = nil, mirostatTau: Float? = nil, numCtx: Int? = nil, repeatLastN: Int? = nil, repeatPenalty: Float? = nil, temperature: Float? = nil, seed: Int? = nil, stop: String? = nil, tfsZ: Float? = nil, numPredict: Int? = nil, topK: Int? = nil, topP: Float? = nil, minP: Float? = nil) { self.mirostat = mirostat self.mirostatEta = mirostatEta self.mirostatTau = mirostatTau diff --git a/Sources/OllamaKit/Responses/OKEmbeddingsResponse.swift b/Sources/OllamaKit/Responses/OKEmbeddingsResponse.swift index fe7f6b0..413558c 100644 --- a/Sources/OllamaKit/Responses/OKEmbeddingsResponse.swift +++ b/Sources/OllamaKit/Responses/OKEmbeddingsResponse.swift @@ -11,5 +11,5 @@ import Foundation public struct OKEmbeddingsResponse: Decodable, Sendable { /// An array of doubles representing the embeddings of the input prompt. - public let embedding: [Double]? + public let embedding: [Float]? } diff --git a/Sources/OllamaKit/Utils/OKJSONValue.swift b/Sources/OllamaKit/Utils/OKJSONValue.swift index 32032d5..9e6a879 100644 --- a/Sources/OllamaKit/Utils/OKJSONValue.swift +++ b/Sources/OllamaKit/Utils/OKJSONValue.swift @@ -9,7 +9,7 @@ import Foundation public enum OKJSONValue: Codable, Sendable { case string(String) - case number(Double) + case number(Float) case integer(Int) case boolean(Bool) case array([OKJSONValue]) @@ -20,7 +20,7 @@ public enum OKJSONValue: Codable, Sendable { if let value = try? container.decode(String.self) { self = .string(value) - } else if let value = try? container.decode(Double.self) { + } else if let value = try? container.decode(Float.self) { self = .number(value) } else if let value = try? container.decode(Int.self) { self = .integer(value) From f94f93dae6abc0ea5e5a43a45b29dbbd7c4320b6 Mon Sep 17 00:00:00 2001 From: 1amageek Date: Sat, 9 Nov 2024 13:42:33 +0900 Subject: [PATCH 04/11] Make OllamaKit conform to Sendable protocol --- Sources/OllamaKit/OllamaKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OllamaKit/OllamaKit.swift b/Sources/OllamaKit/OllamaKit.swift index 8168d4a..ba95ad5 100644 --- a/Sources/OllamaKit/OllamaKit.swift +++ b/Sources/OllamaKit/OllamaKit.swift @@ -8,7 +8,7 @@ import Foundation /// Provides a streamlined way to access the Ollama API, encapsulating the complexities of network communication and data processing. -public struct OllamaKit { +public struct OllamaKit: Sendable { var baseURL: URL = URL(string: "http://localhost:11434")! var decoder: JSONDecoder = .default From 370ef5c2575c97ea3240e4c15cdef187f8d278c1 Mon Sep 17 00:00:00 2001 From: 1amageek Date: Sat, 9 Nov 2024 14:48:57 +0900 Subject: [PATCH 05/11] Change embedding and temperature types to Float --- Playground/OKPlayground/Views/EmbeddingsView.swift | 2 +- Playground/OKPlayground/Views/GenerateView.swift | 2 +- Sources/OllamaKit/RequestData/OKChatRequestData.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Playground/OKPlayground/Views/EmbeddingsView.swift b/Playground/OKPlayground/Views/EmbeddingsView.swift index 27a3160..27503cf 100644 --- a/Playground/OKPlayground/Views/EmbeddingsView.swift +++ b/Playground/OKPlayground/Views/EmbeddingsView.swift @@ -14,7 +14,7 @@ struct EmbeddingsView: View { @State private var model: String? = nil @State private var prompt = "" - @State private var embedding = [Double]() + @State private var embedding = [Float]() @State private var cancellables = Set() var body: some View { diff --git a/Playground/OKPlayground/Views/GenerateView.swift b/Playground/OKPlayground/Views/GenerateView.swift index 3923d8f..81ccabc 100644 --- a/Playground/OKPlayground/Views/GenerateView.swift +++ b/Playground/OKPlayground/Views/GenerateView.swift @@ -13,7 +13,7 @@ struct GenerateView: View { @Environment(ViewModel.self) private var viewModel @State private var model: String? = nil - @State private var temperature: Double = 0.5 + @State private var temperature: Float = 0.5 @State private var prompt = "" @State private var response = "" @State private var cancellables = Set() diff --git a/Sources/OllamaKit/RequestData/OKChatRequestData.swift b/Sources/OllamaKit/RequestData/OKChatRequestData.swift index 6fe4d28..b5d14e1 100644 --- a/Sources/OllamaKit/RequestData/OKChatRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKChatRequestData.swift @@ -40,7 +40,7 @@ public struct OKChatRequestData: Sendable { model: String, messages: [Message], tools: [OKJSONValue]? = nil, - with configureOptions: @Sendable (inout OKCompletionOptions) -> Void = { _ in () } + with configureOptions: @Sendable (inout OKCompletionOptions) -> Void ) { self.stream = tools == nil self.model = model From 3cf06912f42609f170a2ac3764983d893f671958 Mon Sep 17 00:00:00 2001 From: 1amageek Date: Sat, 9 Nov 2024 18:09:54 +0900 Subject: [PATCH 06/11] Refactor Role enum to conform to RawRepresentable --- .../OllamaKit/Responses/OKChatResponse.swift | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Sources/OllamaKit/Responses/OKChatResponse.swift b/Sources/OllamaKit/Responses/OKChatResponse.swift index 2aaa3d8..2a28968 100644 --- a/Sources/OllamaKit/Responses/OKChatResponse.swift +++ b/Sources/OllamaKit/Responses/OKChatResponse.swift @@ -55,7 +55,8 @@ public struct OKChatResponse: OKCompletionResponse, Decodable { public var toolCalls: [ToolCall]? /// An enumeration representing the role of the message sender. - public enum Role: String, Decodable, Sendable { + public enum Role: RawRepresentable, Decodable, Sendable { + /// The message is from the system. case system @@ -64,6 +65,37 @@ public struct OKChatResponse: OKCompletionResponse, Decodable { /// The message is from the user. case user + + /// A custom role with a specified name. + case custom(String) + + // Initializer for RawRepresentable conformance + public init?(rawValue: String) { + switch rawValue { + case "system": + self = .system + case "assistant": + self = .assistant + case "user": + self = .user + default: + self = .custom(rawValue) + } + } + + // Computed property to get the raw value as a string. + public var rawValue: String { + switch self { + case .system: + return "system" + case .assistant: + return "assistant" + case .user: + return "user" + case .custom(let value): + return value + } + } } /// A structure that represents a tool call in the response. From 71d8e7ddb54e53b2722032c3930496201440866a Mon Sep 17 00:00:00 2001 From: 1amageek Date: Sat, 9 Nov 2024 18:34:00 +0900 Subject: [PATCH 07/11] Add custom role support to OKChatRequestData --- .../RequestData/OKChatRequestData.swift | 61 +++++++++++++++++-- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/Sources/OllamaKit/RequestData/OKChatRequestData.swift b/Sources/OllamaKit/RequestData/OKChatRequestData.swift index b5d14e1..d3c5fb9 100644 --- a/Sources/OllamaKit/RequestData/OKChatRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKChatRequestData.swift @@ -68,16 +68,48 @@ public struct OKChatRequestData: Sendable { self.images = images } - /// An enumeration that represents the role of the message sender. - public enum Role: String, Encodable, Sendable { - /// Indicates the message is from the system. + /// An enumeration representing the role of the message sender. + public enum Role: RawRepresentable, Encodable, Sendable { + + /// The message is from the system. case system - /// Indicates the message is from the assistant. + /// The message is from the assistant. case assistant - /// Indicates the message is from the user. + /// The message is from the user. case user + + /// A custom role with a specified name. + case custom(String) + + // Initializer for RawRepresentable conformance + public init?(rawValue: String) { + switch rawValue { + case "system": + self = .system + case "assistant": + self = .assistant + case "user": + self = .user + default: + self = .custom(rawValue) + } + } + + // Computed property to get the raw value as a string. + public var rawValue: String { + switch self { + case .system: + return "system" + case .assistant: + return "assistant" + case .user: + return "user" + case .custom(let value): + return value + } + } } } } @@ -99,3 +131,22 @@ extension OKChatRequestData: Encodable { case stream, model, messages, tools } } + +extension OKChatRequestData.Message { + + public static func system(_ content: String, images: [String]? = nil) -> OKChatRequestData.Message { + .init(role: .system, content: content, images: images) + } + + public static func user(_ content: String, images: [String]? = nil) -> OKChatRequestData.Message { + .init(role: .user, content: content, images: images) + } + + public static func assistant(_ content: String, images: [String]? = nil) -> OKChatRequestData.Message { + .init(role: .assistant, content: content, images: images) + } + + public static func custom(name: String, _ content: String, images: [String]? = nil) -> OKChatRequestData.Message { + .init(role: .custom(name), content: content, images: images) + } +} From f021e813b9d3368ecfae01d082372b4bc86029e2 Mon Sep 17 00:00:00 2001 From: 1amageek Date: Sun, 10 Nov 2024 23:06:43 +0900 Subject: [PATCH 08/11] Remove default value for configureOptions parameter --- Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift | 2 +- Sources/OllamaKit/RequestData/OKGenerateRequestData.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift b/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift index 532a95a..e80bc95 100644 --- a/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKEmbeddingsRequestData.swift @@ -31,7 +31,7 @@ public struct OKEmbeddingsRequestData: Encodable, Sendable { public init( model: String, prompt: String, - with configureOptions: @Sendable (inout OKCompletionOptions) -> Void = { _ in () } + with configureOptions: @Sendable (inout OKCompletionOptions) -> Void ) { self.model = model self.prompt = prompt diff --git a/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift b/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift index 55ff27f..964d2a2 100644 --- a/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift @@ -50,7 +50,7 @@ public struct OKGenerateRequestData: Sendable { model: String, prompt: String, images: [String]? = nil, - with configureOptions: @Sendable (inout OKCompletionOptions) -> Void = { _ in () } + with configureOptions: @Sendable (inout OKCompletionOptions) -> Void ) { self.stream = true self.model = model From b49015d9379d0562c055cfd52d6bafa4651bb1de Mon Sep 17 00:00:00 2001 From: 1amageek Date: Sat, 11 Jan 2025 22:29:14 +0900 Subject: [PATCH 09/11] Refactors chat API to use new OKTool structure --- Sources/OllamaKit/OllamaKit+Chat.swift | 201 ++++++------------ .../RequestData/OKChatRequestData.swift | 6 +- Sources/OllamaKit/Utils/OKTool.swift | 46 ++++ 3 files changed, 110 insertions(+), 143 deletions(-) create mode 100644 Sources/OllamaKit/Utils/OKTool.swift diff --git a/Sources/OllamaKit/OllamaKit+Chat.swift b/Sources/OllamaKit/OllamaKit+Chat.swift index 197e2b1..c7e75ec 100644 --- a/Sources/OllamaKit/OllamaKit+Chat.swift +++ b/Sources/OllamaKit/OllamaKit+Chat.swift @@ -9,95 +9,52 @@ import Combine import Foundation extension OllamaKit { - /// Establishes an asynchronous stream for chat responses from the Ollama API, based on the provided data. + /// Starts a stream for chat responses from the Ollama API. /// - /// This method sets up a streaming connection using Swift's concurrency features, allowing for real-time data handling as chat responses are generated by the Ollama API. - /// - /// Example usage - /// - /// ```swift - /// let ollamaKit = OllamaKit() - /// let chatData = OKChatRequestData(/* parameters */) - /// - /// Task { - /// do { - /// for try await response in ollamaKit.chat(data: chatData) { - /// // Handle each chat response - /// } - /// } catch { - /// // Handle error - /// } - /// } - /// ``` - /// - /// Example usage with tools + /// This method allows real-time handling of chat responses using Swift's concurrency. /// + /// Example usage: /// ```swift /// let ollamaKit = OllamaKit() /// let chatData = OKChatRequestData( - /// /* parameters */, + /// model: "example-model", + /// messages: [ + /// .user("What's the weather like in Tokyo?") + /// ], /// tools: [ - /// .object([ - /// "type": .string("function"), - /// "function": .object([ - /// "name": .string("get_current_weather"), - /// "description": .string("Get the current weather for a location"), - /// "parameters": .object([ - /// "type": .string("object"), - /// "properties": .object([ - /// "location": .object([ - /// "type": .string("string"), - /// "description": .string("The location to get the weather for, e.g. San Francisco, CA") - /// ]), - /// "format": .object([ - /// "type": .string("string"), - /// "description": .string("The format to return the weather in, e.g. 'celsius' or 'fahrenheit'"), - /// "enum": .array([.string("celsius"), .string("fahrenheit")]) - /// ]) + /// .function( + /// OKFunction( + /// name: "get_current_weather", + /// description: "Fetch current weather information.", + /// parameters: .object([ + /// "location": .object([ + /// "type": .string("string"), + /// "description": .string("The location to get the weather for, e.g., Tokyo") /// ]), - /// "required": .array([.string("location"), .string("format")]) - /// ]) - /// ]) - /// ]) + /// "format": .object([ + /// "type": .string("string"), + /// "description": .string("The format for the weather, e.g., 'celsius'."), + /// "enum": .array([.string("celsius"), .string("fahrenheit")]) + /// ]) + /// ], required: ["location", "format"]) + /// ) + /// ) /// ] /// ) /// /// Task { /// do { /// for try await response in ollamaKit.chat(data: chatData) { - /// if let toolCalls = response.message?.toolCalls { - /// for toolCall in toolCalls { - /// if let function = toolCall.function { - /// print("Tool called: \(function.name ?? "")") - /// - /// if let arguments = function.arguments { - /// switch arguments { - /// case .object(let argDict): - /// if let location = argDict["location"], case .string(let locationValue) = location { - /// print("Location: \(locationValue)") - /// } - /// - /// if let format = argDict["format"], case .string(let formatValue) = format { - /// print("Format: \(formatValue)") - /// } - /// default: - /// print("Unexpected arguments format") - /// } - /// } else { - /// print("No arguments provided") - /// } - /// } - /// } - /// } + /// // Handle each response here + /// print(response) /// } /// } catch { - /// // Handle error + /// print("Error: \(error)") /// } /// } /// ``` - /// - /// - Parameter data: The ``OKChatRequestData`` used to initiate the chat streaming from the Ollama API. - /// - Returns: An `AsyncThrowingStream` emitting the live stream of chat responses from the Ollama API. + /// - Parameter data: The ``OKChatRequestData`` containing chat request details. + /// - Returns: An `AsyncThrowingStream` emitting chat responses from the Ollama API. public func chat(data: OKChatRequestData) -> AsyncThrowingStream { do { let request = try OKRouter.chat(data: data).asURLRequest(with: baseURL) @@ -109,91 +66,55 @@ extension OllamaKit { } } - /// Establishes a Combine publisher for streaming chat responses from the Ollama API, based on the provided data. - /// - /// This method sets up a streaming connection using the Combine framework, facilitating real-time data handling as chat responses are generated by the Ollama API. + /// Publishes a stream of chat responses from the Ollama API using Combine. /// - /// Example usage - /// - /// ```swift - /// let ollamaKit = OllamaKit() - /// let chatData = OKChatRequestData(/* parameters */) - /// - /// ollamaKit.chat(data: chatData) - /// .sink(receiveCompletion: { completion in - /// // Handle completion or error - /// }, receiveValue: { chatResponse in - /// // Handle each chat response - /// }) - /// .store(in: &cancellables) - /// ``` - /// - /// Example usage with tools + /// Enables real-time data handling through Combine's reactive streams. /// + /// Example usage: /// ```swift /// let ollamaKit = OllamaKit() /// let chatData = OKChatRequestData( - /// /* parameters */, + /// model: "example-model", + /// messages: [ + /// .user("What's the weather like in Tokyo?") + /// ], /// tools: [ - /// .object([ - /// "type": .string("function"), - /// "function": .object([ - /// "name": .string("get_current_weather"), - /// "description": .string("Get the current weather for a location"), - /// "parameters": .object([ - /// "type": .string("object"), - /// "properties": .object([ - /// "location": .object([ - /// "type": .string("string"), - /// "description": .string("The location to get the weather for, e.g. San Francisco, CA") - /// ]), - /// "format": .object([ - /// "type": .string("string"), - /// "description": .string("The format to return the weather in, e.g. 'celsius' or 'fahrenheit'"), - /// "enum": .array([.string("celsius"), .string("fahrenheit")]) - /// ]) + /// .function( + /// OKFunction( + /// name: "get_current_weather", + /// description: "Fetch current weather information.", + /// parameters: .object([ + /// "location": .object([ + /// "type": .string("string"), + /// "description": .string("The location to get the weather for, e.g., Tokyo") /// ]), - /// "required": .array([.string("location"), .string("format")]) - /// ]) - /// ]) - /// ]) + /// "format": .object([ + /// "type": .string("string"), + /// "description": .string("The format for the weather, e.g., 'celsius'."), + /// "enum": .array([.string("celsius"), .string("fahrenheit")]) + /// ]) + /// ], required: ["location", "format"]) + /// ) + /// ) /// ] /// ) /// /// ollamaKit.chat(data: chatData) /// .sink(receiveCompletion: { completion in - /// // Handle completion or error - /// }, receiveValue: { chatResponse in - /// if let toolCalls = chatResponse.message?.toolCalls { - /// for toolCall in toolCalls { - /// if let function = toolCall.function { - /// print("Tool called: \(function.name ?? "")") - /// - /// if let arguments = function.arguments { - /// switch arguments { - /// case .object(let argDict): - /// if let location = argDict["location"], case .string(let locationValue) = location { - /// print("Location: \(locationValue)") - /// } - /// - /// if let format = argDict["format"], case .string(let formatValue) = format { - /// print("Format: \(formatValue)") - /// } - /// default: - /// print("Unexpected arguments format") - /// } - /// } else { - /// print("No arguments provided") - /// } - /// } - /// } + /// switch completion { + /// case .finished: + /// print("Stream finished") + /// case .failure(let error): + /// print("Error: \(error)") /// } + /// }, receiveValue: { response in + /// // Handle each response here + /// print(response) /// }) /// .store(in: &cancellables) /// ``` - /// - /// - Parameter data: The ``OKChatRequestData`` used to initiate the chat streaming from the Ollama API. - /// - Returns: An `AnyPublisher` emitting the live stream of chat responses from the Ollama API. + /// - Parameter data: The ``OKChatRequestData`` containing chat request details. + /// - Returns: An `AnyPublisher` emitting chat responses from the Ollama API. public func chat(data: OKChatRequestData) -> AnyPublisher { do { let request = try OKRouter.chat(data: data).asURLRequest(with: baseURL) diff --git a/Sources/OllamaKit/RequestData/OKChatRequestData.swift b/Sources/OllamaKit/RequestData/OKChatRequestData.swift index 5fcd928..9e3960e 100644 --- a/Sources/OllamaKit/RequestData/OKChatRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKChatRequestData.swift @@ -18,7 +18,7 @@ public struct OKChatRequestData: Sendable { public let messages: [Message] /// An optional array of ``OKJSONValue`` representing the tools available for tool calling in the chat. - public let tools: [OKJSONValue]? + public let tools: [OKTool]? /// Optional ``OKJSONValue`` representing the JSON schema for the response. /// Be sure to also include "return as JSON" in your prompt @@ -31,7 +31,7 @@ public struct OKChatRequestData: Sendable { public init( model: String, messages: [Message], - tools: [OKJSONValue]? = nil, + tools: [OKTool]? = nil, format: OKJSONValue? = nil, options: OKCompletionOptions? = nil ) { @@ -46,7 +46,7 @@ public struct OKChatRequestData: Sendable { public init( model: String, messages: [Message], - tools: [OKJSONValue]? = nil, + tools: [OKTool]? = nil, format: OKJSONValue? = nil, with configureOptions: @Sendable (inout OKCompletionOptions) -> Void ) { diff --git a/Sources/OllamaKit/Utils/OKTool.swift b/Sources/OllamaKit/Utils/OKTool.swift new file mode 100644 index 0000000..0ebc8c5 --- /dev/null +++ b/Sources/OllamaKit/Utils/OKTool.swift @@ -0,0 +1,46 @@ +// +// OKTool.swift +// OllamaKit +// +// Created by Norikazu Muramoto on 2025/01/11. +// + +import Foundation + +/// Represents a tool that can be used in the Ollama API chat. +public struct OKTool: Encodable, Sendable { + /// The type of the tool (e.g., "function"). + public let type: String + + /// The function details associated with the tool. + public let function: OKFunction + + public init(type: String, function: OKFunction) { + self.type = type + self.function = function + } + + /// Convenience method for creating a tool with type "function". + public static func function(_ function: OKFunction) -> OKTool { + return OKTool(type: "function", function: function) + } +} + +/// Represents a function used as a tool in the Ollama API chat. +public struct OKFunction: Encodable, Sendable { + /// The name of the function. + public let name: String + + /// A description of what the function does. + public let description: String + + /// Parameters required by the function, defined as a JSON schema. + public let parameters: OKJSONValue + + public init(name: String, description: String, parameters: OKJSONValue) { + self.name = name + self.description = description + self.parameters = parameters + } +} + From 691604d081e1964f518013e3a291f628a3ac6828 Mon Sep 17 00:00:00 2001 From: 1amageek Date: Mon, 13 Jan 2025 11:48:29 +0900 Subject: [PATCH 10/11] Add JSONSchema dependency and update related code --- Package.resolved | 12 +- Package.swift | 7 +- Package@swift-5.9.swift | 7 +- .../Views/ChatWithFormatView.swift | 25 ++- .../Views/ChatWithToolsView.swift | 43 ++-- Sources/OllamaKit/OllamaKit+Chat.swift | 54 +++-- .../RequestData/OKChatRequestData.swift | 11 +- .../RequestData/OKGenerateRequestData.swift | 9 +- .../OllamaKit/Responses/OKChatResponse.swift | 1 + Sources/OllamaKit/Utils/OKTool.swift | 5 +- Tests/OllamaKitTests/OKChatRequestTests.swift | 203 ++++++++++++++++++ 11 files changed, 302 insertions(+), 75 deletions(-) create mode 100644 Tests/OllamaKitTests/OKChatRequestTests.swift diff --git a/Package.resolved b/Package.resolved index e65252d..b933a40 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "01431723e770c237a52728e57f3f88f4b5a006d0bcafdaf43fb4b69479b0f188", "pins" : [ { "identity" : "swift-docc-plugin", @@ -17,7 +18,16 @@ "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", "version" : "1.0.0" } + }, + { + "identity" : "swift-json-schema", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kevinhermawan/swift-json-schema.git", + "state" : { + "revision" : "4a9284e44d8ef2bfc863734e9ff535909ae6b27a", + "version" : "2.0.1" + } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 9a0a5f8..2b109af 100644 --- a/Package.swift +++ b/Package.swift @@ -16,12 +16,15 @@ let package = Package( targets: ["OllamaKit"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-docc-plugin.git", .upToNextMajor(from: "1.3.0")) + .package(url: "https://github.com/apple/swift-docc-plugin.git", .upToNextMajor(from: "1.3.0")), + .package(url: "https://github.com/kevinhermawan/swift-json-schema.git", .upToNextMajor(from: "2.0.1")) ], targets: [ .target( name: "OllamaKit", - dependencies: []), + dependencies: [ + .product(name: "JSONSchema", package: "swift-json-schema") + ]), .testTarget( name: "OllamaKitTests", dependencies: ["OllamaKit"]), diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 1177966..84bc4d4 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -16,12 +16,15 @@ let package = Package( targets: ["OllamaKit"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-docc-plugin.git", .upToNextMajor(from: "1.3.0")) + .package(url: "https://github.com/apple/swift-docc-plugin.git", .upToNextMajor(from: "1.3.0")), + .package(url: "https://github.com/kevinhermawan/swift-json-schema.git", .upToNextMajor(from: "2.0.1")) ], targets: [ .target( name: "OllamaKit", - dependencies: []), + dependencies: [ + .product(name: "JSONSchema", package: "swift-json-schema") + ]), .testTarget( name: "OllamaKitTests", dependencies: ["OllamaKit"]), diff --git a/Playground/OKPlayground/Views/ChatWithFormatView.swift b/Playground/OKPlayground/Views/ChatWithFormatView.swift index fd33391..51c1807 100644 --- a/Playground/OKPlayground/Views/ChatWithFormatView.swift +++ b/Playground/OKPlayground/Views/ChatWithFormatView.swift @@ -8,6 +8,7 @@ import Combine import OllamaKit import SwiftUI +import JSONSchema struct ChatWithFormatView: View { @@ -133,19 +134,17 @@ struct ChatWithFormatView: View { .store(in: &cancellables) } - private func getFormat() -> OKJSONValue { - return - .object(["type": .string("array"), - "items": .object([ - "type" : .string("object"), - "properties": .object([ - "id": .object(["type" : .string("string")]), - "country": .object(["type" : .string("string")]), - "capital": .object(["type" : .string("string")]), - ]), - "required": .array([.string("id"), .string("country"), .string("capital")]) - ]) - ]) + private func getFormat() -> JSONSchema { + return .array( + items:.object( + properties: [ + "id": .string(), + "country": .string(), + "capital": .string() + ], + required: ["id", "country", "capital"] + ) + ) } private func decodeResponse(_ content: String) { diff --git a/Playground/OKPlayground/Views/ChatWithToolsView.swift b/Playground/OKPlayground/Views/ChatWithToolsView.swift index 3717901..0de3805 100644 --- a/Playground/OKPlayground/Views/ChatWithToolsView.swift +++ b/Playground/OKPlayground/Views/ChatWithToolsView.swift @@ -105,37 +105,32 @@ struct ChatWithToolsView: View { .store(in: &cancellables) } - private func getTools() -> [OKJSONValue] { + private func getTools() -> [OKTool] { return [ - .object([ - "type": .string("function"), - "function": .object([ - "name": .string("get_current_weather"), - "description": .string("Get the current weather for a location"), - "parameters": .object([ - "type": .string("object"), - "properties": .object([ - "location": .object([ - "type": .string("string"), - "description": .string("The location to get the weather for, e.g. San Francisco, CA") - ]), - "format": .object([ - "type": .string("string"), - "description": .string("The format to return the weather in, e.g. 'celsius' or 'fahrenheit'"), - "enum": .array([.string("celsius"), .string("fahrenheit")]) - ]) - ]), - "required": .array([.string("location"), .string("format")]) - ]) - ]) - ]) + .function( + .init( + name: "get_current_weather", + description: "Get the current weather for a location", + parameters: + .object( + properties: [ + "location": .string( + description: "The location to get the weather for, e.g. San Francisco, CA" + ), + "format": .enum(description: "The format to return the weather in, e.g. 'celsius' or 'fahrenheit'", values: [.string("celsius"), .string("fahrenheit")]) + ], + required: ["location", "format"] + ) + ) + ) + ] } private func setResponses(_ function: OKChatResponse.Message.ToolCall.Function) { self.toolCalledResponse = function.name ?? "" self.argumentsResponse = "\(function.arguments ?? .string("No arguments"))" - + if let arguments = function.arguments { switch arguments { case .object(let argDict): diff --git a/Sources/OllamaKit/OllamaKit+Chat.swift b/Sources/OllamaKit/OllamaKit+Chat.swift index c7e75ec..fbaac31 100644 --- a/Sources/OllamaKit/OllamaKit+Chat.swift +++ b/Sources/OllamaKit/OllamaKit+Chat.swift @@ -26,17 +26,22 @@ extension OllamaKit { /// OKFunction( /// name: "get_current_weather", /// description: "Fetch current weather information.", - /// parameters: .object([ - /// "location": .object([ - /// "type": .string("string"), - /// "description": .string("The location to get the weather for, e.g., Tokyo") - /// ]), - /// "format": .object([ - /// "type": .string("string"), - /// "description": .string("The format for the weather, e.g., 'celsius'."), - /// "enum": .array([.string("celsius"), .string("fahrenheit")]) - /// ]) - /// ], required: ["location", "format"]) + /// parameters: .object( + /// description: "Parameters for fetching weather", + /// properties: [ + /// "location": .string( + /// description: "The location to get the weather for, e.g., Tokyo" + /// ), + /// "format": .enum( + /// description: "The format for the weather, e.g., 'celsius'.", + /// values: [ + /// .string("celsius"), + /// .string("fahrenheit") + /// ] + /// ) + /// ], + /// required: ["location", "format"] + /// ) /// ) /// ) /// ] @@ -83,17 +88,22 @@ extension OllamaKit { /// OKFunction( /// name: "get_current_weather", /// description: "Fetch current weather information.", - /// parameters: .object([ - /// "location": .object([ - /// "type": .string("string"), - /// "description": .string("The location to get the weather for, e.g., Tokyo") - /// ]), - /// "format": .object([ - /// "type": .string("string"), - /// "description": .string("The format for the weather, e.g., 'celsius'."), - /// "enum": .array([.string("celsius"), .string("fahrenheit")]) - /// ]) - /// ], required: ["location", "format"]) + /// parameters: .object( + /// description: "Parameters for fetching weather", + /// properties: [ + /// "location": .string( + /// description: "The location to get the weather for, e.g., Tokyo" + /// ), + /// "format": .enum( + /// description: "The format for the weather, e.g., 'celsius'.", + /// values: [ + /// .string("celsius"), + /// .string("fahrenheit") + /// ] + /// ) + /// ], + /// required: ["location", "format"] + /// ) /// ) /// ) /// ] diff --git a/Sources/OllamaKit/RequestData/OKChatRequestData.swift b/Sources/OllamaKit/RequestData/OKChatRequestData.swift index 9e3960e..9ffdcfb 100644 --- a/Sources/OllamaKit/RequestData/OKChatRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKChatRequestData.swift @@ -6,6 +6,7 @@ // import Foundation +import JSONSchema /// A structure that encapsulates data for chat requests to the Ollama API. public struct OKChatRequestData: Sendable { @@ -17,12 +18,12 @@ public struct OKChatRequestData: Sendable { /// An array of ``Message`` instances representing the content to be sent to the Ollama API. public let messages: [Message] - /// An optional array of ``OKJSONValue`` representing the tools available for tool calling in the chat. + /// An optional array of ``OKTool`` representing the tools available for tool calling in the chat. public let tools: [OKTool]? - /// Optional ``OKJSONValue`` representing the JSON schema for the response. + /// Optional ``JSONSchema`` representing the JSON schema for the response. /// Be sure to also include "return as JSON" in your prompt - public let format: OKJSONValue? + public let format: JSONSchema? /// Optional ``OKCompletionOptions`` providing additional configuration for the chat request. public var options: OKCompletionOptions? @@ -32,7 +33,7 @@ public struct OKChatRequestData: Sendable { model: String, messages: [Message], tools: [OKTool]? = nil, - format: OKJSONValue? = nil, + format: JSONSchema? = nil, options: OKCompletionOptions? = nil ) { self.stream = tools == nil @@ -47,7 +48,7 @@ public struct OKChatRequestData: Sendable { model: String, messages: [Message], tools: [OKTool]? = nil, - format: OKJSONValue? = nil, + format: JSONSchema? = nil, with configureOptions: @Sendable (inout OKCompletionOptions) -> Void ) { self.stream = tools == nil diff --git a/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift b/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift index 1c6ea16..059eafb 100644 --- a/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift +++ b/Sources/OllamaKit/RequestData/OKGenerateRequestData.swift @@ -6,6 +6,7 @@ // import Foundation +import JSONSchema /// A structure that encapsulates the data required for generating responses using the Ollama API. public struct OKGenerateRequestData: Sendable { @@ -20,9 +21,9 @@ public struct OKGenerateRequestData: Sendable { /// An optional array of base64-encoded images. public let images: [String]? - /// Optional ``OKJSONValue`` representing the JSON schema for the response. + /// Optional ``JSONSchema`` representing the JSON schema for the response. /// Be sure to also include "return as JSON" in your prompt - public let format: OKJSONValue? + public let format: JSONSchema? /// An optional string specifying the system message. public var system: String? @@ -39,7 +40,7 @@ public struct OKGenerateRequestData: Sendable { images: [String]? = nil, system: String? = nil, context: [Int]? = nil, - format: OKJSONValue? = nil, + format: JSONSchema? = nil, options: OKCompletionOptions? = nil ) { self.stream = true @@ -56,7 +57,7 @@ public struct OKGenerateRequestData: Sendable { model: String, prompt: String, images: [String]? = nil, - format: OKJSONValue? = nil, + format: JSONSchema? = nil, with configureOptions: @Sendable (inout OKCompletionOptions) -> Void ) { self.stream = true diff --git a/Sources/OllamaKit/Responses/OKChatResponse.swift b/Sources/OllamaKit/Responses/OKChatResponse.swift index aaa4429..82c73b6 100644 --- a/Sources/OllamaKit/Responses/OKChatResponse.swift +++ b/Sources/OllamaKit/Responses/OKChatResponse.swift @@ -6,6 +6,7 @@ // import Foundation +import JSONSchema /// A structure that represents the response to a chat request from the Ollama API. public struct OKChatResponse: OKCompletionResponse, Decodable, Sendable { diff --git a/Sources/OllamaKit/Utils/OKTool.swift b/Sources/OllamaKit/Utils/OKTool.swift index 0ebc8c5..1f5afd6 100644 --- a/Sources/OllamaKit/Utils/OKTool.swift +++ b/Sources/OllamaKit/Utils/OKTool.swift @@ -6,6 +6,7 @@ // import Foundation +import JSONSchema /// Represents a tool that can be used in the Ollama API chat. public struct OKTool: Encodable, Sendable { @@ -35,9 +36,9 @@ public struct OKFunction: Encodable, Sendable { public let description: String /// Parameters required by the function, defined as a JSON schema. - public let parameters: OKJSONValue + public let parameters: JSONSchema - public init(name: String, description: String, parameters: OKJSONValue) { + public init(name: String, description: String, parameters: JSONSchema) { self.name = name self.description = description self.parameters = parameters diff --git a/Tests/OllamaKitTests/OKChatRequestTests.swift b/Tests/OllamaKitTests/OKChatRequestTests.swift new file mode 100644 index 0000000..c84d077 --- /dev/null +++ b/Tests/OllamaKitTests/OKChatRequestTests.swift @@ -0,0 +1,203 @@ +import Testing +import Foundation +import JSONSchema +@testable import OllamaKit + +@Test("Basic chat request initialization") +func testBasicChatRequestInit() { + let messages = [ + OKChatRequestData.Message.user("Hello, how are you?") + ] + + let chatRequest = OKChatRequestData( + model: "llama2", + messages: messages + ) + + #expect(chatRequest.model == "llama2") + #expect(chatRequest.messages.count == 1) + #expect(chatRequest.messages[0].role.rawValue == "user") + #expect(chatRequest.messages[0].content == "Hello, how are you?") + #expect(chatRequest.tools == nil) + #expect(chatRequest.format == nil) +} + +@Test("Chat request with all message types") +func testChatRequestWithAllMessageTypes() { + let messages: [OKChatRequestData.Message] = [ + .system("You are a helpful assistant."), + .user("What's the weather?"), + .assistant("The weather is sunny."), + .custom(name: "weather_bot", "Temperature is 25°C") + ] + + let chatRequest = OKChatRequestData( + model: "llama2", + messages: messages + ) + + #expect(chatRequest.messages.count == 4) + #expect(chatRequest.messages[0].role.rawValue == "system") + #expect(chatRequest.messages[1].role.rawValue == "user") + #expect(chatRequest.messages[2].role.rawValue == "assistant") + #expect(chatRequest.messages[3].role.rawValue == "weather_bot") +} + +@Test("Chat request with tools and JSON schema") +func testChatRequestWithToolsAndSchema() { + let weatherFunction = OKFunction( + name: "get_weather", + description: "Get current weather", + parameters: .object( + description: "Weather parameters", + properties: [ + "location": .string(description: "City name"), + "unit": .string(description: "Temperature unit") + ], + required: ["location"] + ) + ) + + let responseSchema = JSONSchema.object( + description: "Weather response", + properties: [ + "temperature": .number(description: "Current temperature"), + "condition": .string(description: "Weather condition") + ], + required: ["temperature", "condition"] + ) + + let chatRequest = OKChatRequestData( + model: "llama2", + messages: [.user("What's the weather in Tokyo?")], + tools: [.function(weatherFunction)], + format: responseSchema + ) + + #expect(chatRequest.tools?.count == 1) + #expect(chatRequest.tools?[0].type == "function") + #expect(chatRequest.tools?[0].function.name == "get_weather") + #expect(chatRequest.format != nil) +} + +@Test("Chat request with options configuration") +func testChatRequestWithOptions() { + let chatRequest = OKChatRequestData( + model: "llama2", + messages: [.user("Hello")], + with: { options in + options.temperature = 0.7 + options.topP = 0.9 + options.seed = 42 + } + ) + + #expect(chatRequest.options?.temperature == 0.7) + #expect(chatRequest.options?.topP == 0.9) + #expect(chatRequest.options?.seed == 42) +} + +@Test("Chat request with images") +func testChatRequestWithImages() { + let imageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" + + let messages = [ + OKChatRequestData.Message.user("What's in this image?", images: [imageBase64]) + ] + + let chatRequest = OKChatRequestData( + model: "llama2", + messages: messages + ) + + #expect(chatRequest.messages[0].images?.count == 1) + #expect(chatRequest.messages[0].images?[0] == imageBase64) +} + +@Test("Chat request encoding") +func testChatRequestEncoding() throws { + let messages = [ + OKChatRequestData.Message.user("Hello") + ] + + let chatRequest = OKChatRequestData( + model: "llama2", + messages: messages + ) + + let encoder = JSONEncoder() + let data = try encoder.encode(chatRequest) + let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] + + #expect(json?["model"] as? String == "llama2") + #expect(json?["stream"] as? Bool == true) + #expect((json?["messages"] as? [[String: Any]])?.count == 1) +} + +@Test("Message role raw value conversion") +func testMessageRoleRawValue() { + let systemRole = OKChatRequestData.Message.Role.system + let customRole = OKChatRequestData.Message.Role.custom("test_bot") + + #expect(systemRole.rawValue == "system") + #expect(customRole.rawValue == "test_bot") + + let fromRawSystem = OKChatRequestData.Message.Role(rawValue: "system")! + let fromRawCustom = OKChatRequestData.Message.Role(rawValue: "test_bot")! + + #expect(fromRawSystem == .system) + #expect(fromRawCustom == .custom("test_bot")) +} + +@Test("Complex tool configuration") +func testComplexTools() { + let complexFunction = OKFunction( + name: "analyze_data", + description: "Analyze complex data structure", + parameters: .object( + description: "Analysis parameters", + properties: [ + "data": .array( + description: "Input data points", + items: .object( + properties: [ + "value": .number(), + "label": .string(), + "metadata": .object( + properties: [ + "timestamp": .string(description: "ISO8601 formatted timestamp"), + "source": .string() + ] + ) + ], + required: ["value", "label"] + ) + ), + "options": .object( + properties: [ + "algorithm": .enum( + values: [ + .string("mean"), + .string("median"), + .string("mode") + ] + ), + "precision": .integer(minimum: 0, maximum: 10) + ] + ) + ], + required: ["data"] + ) + ) + + let chatRequest = OKChatRequestData( + model: "llama2", + messages: [.user("Analyze this data")], + tools: [.function(complexFunction)] + ) + + let encoder = JSONEncoder() + #expect(throws: Never.self) { + _ = try encoder.encode(chatRequest) + } +} From 16b30e8f8b2f48dbf169e5ada274478680ea2d8c Mon Sep 17 00:00:00 2001 From: 1amageek Date: Fri, 7 Mar 2025 16:05:02 +0900 Subject: [PATCH 11/11] Update swift-json-schema dependency and add tests --- Package.resolved | 8 +- Package.swift | 2 +- Tests/OllamaKitTests/ModelTests.swift | 188 ++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 Tests/OllamaKitTests/ModelTests.swift diff --git a/Package.resolved b/Package.resolved index b933a40..4e67108 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "01431723e770c237a52728e57f3f88f4b5a006d0bcafdaf43fb4b69479b0f188", + "originHash" : "1b286c8c0a077892b2e712977f467e5f49595367ac433daf2de7e17cc43f93a6", "pins" : [ { "identity" : "swift-docc-plugin", @@ -22,10 +22,10 @@ { "identity" : "swift-json-schema", "kind" : "remoteSourceControl", - "location" : "https://github.com/kevinhermawan/swift-json-schema.git", + "location" : "https://github.com/1amageek/swift-json-schema.git", "state" : { - "revision" : "4a9284e44d8ef2bfc863734e9ff535909ae6b27a", - "version" : "2.0.1" + "branch" : "main", + "revision" : "ef7c71dcae944c18792a2164501394d111501004" } } ], diff --git a/Package.swift b/Package.swift index 2b109af..9a019ce 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin.git", .upToNextMajor(from: "1.3.0")), - .package(url: "https://github.com/kevinhermawan/swift-json-schema.git", .upToNextMajor(from: "2.0.1")) + .package(url: "https://github.com/1amageek/swift-json-schema.git", branch: "main") ], targets: [ .target( diff --git a/Tests/OllamaKitTests/ModelTests.swift b/Tests/OllamaKitTests/ModelTests.swift new file mode 100644 index 0000000..7f67f6c --- /dev/null +++ b/Tests/OllamaKitTests/ModelTests.swift @@ -0,0 +1,188 @@ +// +// ModelTests.swift +// OllamaKit +// +// Created by Norikazu Muramoto on 2025/01/22. +// + + +import Testing +import Foundation +import OllamaKit + +@Test("Basic chat functionality for all models") +func testAllModelsBasicChat() async throws { + let models = ["deepseek-r1:8b", "phi4", "llama3.2"] + let ollamaKit = OllamaKit() + + for model in models { + print("Testing model: \(model)") + + // Basic chat test + let chatData = OKChatRequestData( + model: model, + messages: [.user("What is 2+2?")] + ) + + var response = "" + for try await chatResponse in ollamaKit.chat(data: chatData) { + if let content = chatResponse.message?.content { + response += content + } + } + + #expect(!response.isEmpty, "Model \(model) should return a response") + #expect(response.contains("4"), "Model \(model) should correctly answer 2+2=4") + } +} + +@Test("Tool calling functionality for all models") +func testAllModelsToolCalling() async throws { + let models = ["deepseek-r1:8b", "phi4", "llama3.2"] + let ollamaKit = OllamaKit() + + // Weather function definition + let weatherFunction = OKFunction( + name: "get_current_weather", + description: "Get the current weather in a given location", + parameters: .object( + description: "Parameters for the weather function", + properties: [ + "location": .string(description: "The city and state, e.g. San Francisco, CA"), + "unit": .string(description: "The temperature unit to use: 'celsius' or 'fahrenheit'") + ], + required: ["location", "unit"] + ) + ) + + for model in models { + print("Testing model: \(model) with tool calling") + + let chatData = OKChatRequestData( + model: model, + messages: [ + .system("You are a helpful assistant that uses the provided weather function when asked about weather."), + .user("What's the weather like in Tokyo?") + ], + tools: [.function(weatherFunction)] + ) + + var hasToolCall = false + var functionName: String? + + for try await chatResponse in ollamaKit.chat(data: chatData) { + if let toolCalls = chatResponse.message?.toolCalls { + hasToolCall = true + functionName = toolCalls.first?.function?.name + } + } + + #expect(hasToolCall, "Model \(model) should attempt to use the weather tool") + #expect(functionName == "get_current_weather", "Model \(model) should call the correct function") + } +} + +@Test("Response format validation for all models") +func testAllModelsResponseFormat() async throws { + let models = ["deepseek-r1:8b", "phi4", "llama3.2"] + let ollamaKit = OllamaKit() + + for model in models { + print("Testing model: \(model) response format") + + let chatData = OKChatRequestData( + model: model, + messages: [ + .system("You should respond in complete sentences."), + .user("List three colors.") + ] + ) + + var response = "" + var responseComplete = false + + for try await chatResponse in ollamaKit.chat(data: chatData) { + if let content = chatResponse.message?.content { + response += content + } + if chatResponse.done { + responseComplete = true + } + } + + #expect(responseComplete, "Model \(model) should complete its response") + #expect(response.contains("."), "Model \(model) should respond in complete sentences") + #expect(response.components(separatedBy: .whitespaces).count > 5, "Model \(model) should provide a substantial response") + } +} + +@Test("Error handling for all models") +func testAllModelsErrorHandling() async throws { + let models = ["deepseek-r1:8b", "phi4", "llama3.2"] + let ollamaKit = OllamaKit() + + for model in models { + print("Testing model: \(model) error handling") + + // Test with empty message + do { + let chatData = OKChatRequestData( + model: model, + messages: [] + ) + + var receivedResponse = false + for try await _ in ollamaKit.chat(data: chatData) { + receivedResponse = true + } + #expect(!receivedResponse, "Model \(model) should not process empty messages") + } catch { + // Error is expected + } + + // Test with invalid model name + do { + let chatData = OKChatRequestData( + model: model + "_invalid", + messages: [.user("Test")] + ) + + var receivedResponse = false + for try await _ in ollamaKit.chat(data: chatData) { + receivedResponse = true + } + #expect(!receivedResponse, "Invalid model name should not return response") + } catch { + // Error is expected + } + } +} + +@Test("Context handling for all models") +func testAllModelsContextHandling() async throws { + let models = ["deepseek-r1:8b", "phi4", "llama3.2"] + let ollamaKit = OllamaKit() + + for model in models { + print("Testing model: \(model) context handling") + + let chatData = OKChatRequestData( + model: model, + messages: [ + .system("You are a helpful assistant."), + .user("My name is Alice."), + .assistant("Nice to meet you, Alice!"), + .user("What's my name?") + ] + ) + + var response = "" + for try await chatResponse in ollamaKit.chat(data: chatData) { + if let content = chatResponse.message?.content { + response += content + } + } + + #expect(response.contains("Alice"), "Model \(model) should remember context") + } +}