From 561c5f95931afef91b6d88fcb7d781c0b4d5e340 Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Fri, 8 Dec 2023 14:56:59 +0100 Subject: [PATCH 01/15] chore: updated and applied pre-commit --- .gitignore | 88 +---------------------------------------- .pre-commit-config.yaml | 60 ++++++++++++++-------------- .swift-version | 1 + .swiftlint.yaml | 3 ++ README.md | 1 + Taskfile.yaml | 2 +- 6 files changed, 38 insertions(+), 117 deletions(-) create mode 100644 .swift-version create mode 100644 .swiftlint.yaml diff --git a/.gitignore b/.gitignore index 330d167..b46fb9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,90 +1,6 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -build/ -DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 - -## Obj-C/Swift specific -*.hmap - -## App packaging *.ipa *.dSYM.zip *.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -# *.xcodeproj -# -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -# .swiftpm - .build/ - -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# Pods/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# Accio dependency management -Dependencies/ -.accio/ - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ +node_modules/ +xcuserdata/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66861c8..2eee6c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,32 +1,32 @@ default_stages: [commit] repos: - - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook - rev: "v9.10.0" - hooks: - - id: commitlint - stages: [commit-msg] - additional_dependencies: - - "@commitlint/cli" - - "@commitlint/config-conventional" - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: "v4.5.0" - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - exclude: "package" - - id: check-yaml - - id: check-added-large-files - - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v3.1.0" - hooks: - - id: prettier - exclude: "package" - - repo: github.com/nicklockwood/SwiftFormat - rev: "" - hooks: - - id: swiftformat - - repo: github.com/realm/SwiftLint - rev: "" - hooks: - - id: swiftlint - entry: swiftlint --fix --strict + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: 'v9.10.0' + hooks: + - id: commitlint + stages: [commit-msg] + additional_dependencies: + - '@commitlint/cli' + - '@commitlint/config-conventional' + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 'v4.5.0' + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + exclude: 'package' + - id: check-yaml + - id: check-added-large-files + - repo: https://github.com/pre-commit/mirrors-prettier + rev: 'v4.0.0-alpha.3-1' + hooks: + - id: prettier + exclude: 'package' + - repo: https://github.com/nicklockwood/SwiftFormat + rev: '0.52.11' + hooks: + - id: swiftformat + - repo: https://github.com/realm/SwiftLint + rev: '0.54.0' + hooks: + - id: swiftlint + entry: swiftlint --config .swiftlint.yaml --fix --strict diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..95ee81a --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +5.9 diff --git a/.swiftlint.yaml b/.swiftlint.yaml new file mode 100644 index 0000000..f41c584 --- /dev/null +++ b/.swiftlint.yaml @@ -0,0 +1,3 @@ +disabled_rules: + - comma + - trailing_comma diff --git a/README.md b/README.md index 141921d..55f07cd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # sdk-ios + The iOS SDK for BaseMind.AI diff --git a/Taskfile.yaml b/Taskfile.yaml index d7f34ff..cb27a4c 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -1,4 +1,4 @@ -version: "3" +version: '3' tasks: setup: From b390dabd15f9b8687725162dbbe77a29372c1863 Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Fri, 8 Dec 2023 14:57:31 +0100 Subject: [PATCH 02/15] feat: initial implementation --- Package.swift | 10 +- Sources/BaseMindClient/BaseMindClient.swift | 47 ++- Sources/BaseMindGateway/gateway.grpc.swift | 413 -------------------- Sources/BaseMindGateway/gateway.pb.swift | 292 -------------- Sources/GatewayGRPC/gateway.grpc.swift | 411 +++++++++++++++++++ Sources/GatewayGRPC/gateway.pb.swift | 295 ++++++++++++++ 6 files changed, 755 insertions(+), 713 deletions(-) delete mode 100644 Sources/BaseMindGateway/gateway.grpc.swift delete mode 100644 Sources/BaseMindGateway/gateway.pb.swift create mode 100644 Sources/GatewayGRPC/gateway.grpc.swift create mode 100644 Sources/GatewayGRPC/gateway.pb.swift diff --git a/Package.swift b/Package.swift index e271e37..548031b 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let BaseMindGateway: Target = .target( .product(name: "GRPC", package: "grpc-swift"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "SwiftProtobuf", package: "swift-protobuf") + .product(name: "SwiftProtobuf", package: "swift-protobuf"), ] ) @@ -19,18 +19,18 @@ let package = Package( .macCatalyst(.v13), .macOS(.v10_15), .watchOS(.v6), - .tvOS(.v13) + .tvOS(.v13), ], products: [ .library( name: "BaseMindClient", targets: ["BaseMindClient"] - ) + ), ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.6.0"), - .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.15.0") + .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.15.0"), ], targets: [ BaseMindGateway, @@ -41,6 +41,6 @@ let package = Package( .testTarget( name: "BaseMindClientTests", dependencies: ["BaseMindClient"] - ) + ), ] ) diff --git a/Sources/BaseMindClient/BaseMindClient.swift b/Sources/BaseMindClient/BaseMindClient.swift index 6ab3b61..4f157e0 100644 --- a/Sources/BaseMindClient/BaseMindClient.swift +++ b/Sources/BaseMindClient/BaseMindClient.swift @@ -1,9 +1,50 @@ import BaseMindGateway import Foundation +import GRPC + +enum BaseMindError: Error { + case connectionFailure +} + +let DEFAULT_API_GATEWAY_ADDRESS = "gateway.basemind.ai" +let DEFAULT_API_GATEWAY_PORT = 443 + +public struct Options { + var host: String = DEFAULT_API_GATEWAY_ADDRESS + var port: Int = DEFAULT_API_GATEWAY_PORT + var debug: Bool = false + var promptConfigId: String? + var logger: Logger = .init( + subsystem: Bundle.main.bundleIdentifier!, + category: "BaseMindClient" + ) +} public class BaseMindClient { - private var apiToken: String = "" - private var promptConfigId: String? + private let apiToken: String = "" + private let promptConfigId: String? + + init(apiToken: String, options: Options = Options()) throws { + self.apiToken = apiToken + + if options.debug { + options.logger.debug("initializing client") + } + + let eventLoop = MultiThreadedEventLoopGroup(numberOfThreads: 1) + + do { + let channel = try GRPCChannelPool.with( + target: .host(options.host, port: options.port), + transportSecurity: .tls, + eventLoopGroup: eventLoopGroup + ) + } catch { + if options.debug { + options.logger.debug("Failed to connect to BaseMind server on \(options.host):\(options.port). Error: \(error).") + } - func init() {} + throw BaseMindError.connectionFailure + } + } } diff --git a/Sources/BaseMindGateway/gateway.grpc.swift b/Sources/BaseMindGateway/gateway.grpc.swift deleted file mode 100644 index 9990154..0000000 --- a/Sources/BaseMindGateway/gateway.grpc.swift +++ /dev/null @@ -1,413 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: gateway/v1/gateway.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// The API Gateway service definition. -/// -/// Usage: instantiate `Gateway_V1_APIGatewayServiceClient`, then call methods of this protocol to make API calls. -internal protocol Gateway_V1_APIGatewayServiceClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { get } - - func requestPrompt( - _ request: Gateway_V1_PromptRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func requestStreamingPrompt( - _ request: Gateway_V1_PromptRequest, - callOptions: CallOptions?, - handler: @escaping (Gateway_V1_StreamingPromptResponse) -> Void - ) -> ServerStreamingCall -} - -extension Gateway_V1_APIGatewayServiceClientProtocol { - internal var serviceName: String { - return "gateway.v1.APIGatewayService" - } - - /// Request a regular LLM prompt - /// - /// - Parameters: - /// - request: Request to send to RequestPrompt. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func requestPrompt( - _ request: Gateway_V1_PromptRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestPrompt.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRequestPromptInterceptors() ?? [] - ) - } - - /// Request a streaming LLM prompt - /// - /// - Parameters: - /// - request: Request to send to RequestStreamingPrompt. - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. - internal func requestStreamingPrompt( - _ request: Gateway_V1_PromptRequest, - callOptions: CallOptions? = nil, - handler: @escaping (Gateway_V1_StreamingPromptResponse) -> Void - ) -> ServerStreamingCall { - return self.makeServerStreamingCall( - path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestStreamingPrompt.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRequestStreamingPromptInterceptors() ?? [], - handler: handler - ) - } -} - -@available(*, deprecated) -extension Gateway_V1_APIGatewayServiceClient: @unchecked Sendable {} - -@available(*, deprecated, renamed: "Gateway_V1_APIGatewayServiceNIOClient") -internal final class Gateway_V1_APIGatewayServiceClient: Gateway_V1_APIGatewayServiceClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - internal var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the gateway.v1.APIGatewayService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -internal struct Gateway_V1_APIGatewayServiceNIOClient: Gateway_V1_APIGatewayServiceClientProtocol { - internal var channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? - - /// Creates a client for the gateway.v1.APIGatewayService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// The API Gateway service definition. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Gateway_V1_APIGatewayServiceAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { get } - - func makeRequestPromptCall( - _ request: Gateway_V1_PromptRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeRequestStreamingPromptCall( - _ request: Gateway_V1_PromptRequest, - callOptions: CallOptions? - ) -> GRPCAsyncServerStreamingCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Gateway_V1_APIGatewayServiceAsyncClientProtocol { - internal static var serviceDescriptor: GRPCServiceDescriptor { - return Gateway_V1_APIGatewayServiceClientMetadata.serviceDescriptor - } - - internal var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { - return nil - } - - internal func makeRequestPromptCall( - _ request: Gateway_V1_PromptRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestPrompt.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRequestPromptInterceptors() ?? [] - ) - } - - internal func makeRequestStreamingPromptCall( - _ request: Gateway_V1_PromptRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncServerStreamingCall { - return self.makeAsyncServerStreamingCall( - path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestStreamingPrompt.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRequestStreamingPromptInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Gateway_V1_APIGatewayServiceAsyncClientProtocol { - internal func requestPrompt( - _ request: Gateway_V1_PromptRequest, - callOptions: CallOptions? = nil - ) async throws -> Gateway_V1_PromptResponse { - return try await self.performAsyncUnaryCall( - path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestPrompt.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRequestPromptInterceptors() ?? [] - ) - } - - internal func requestStreamingPrompt( - _ request: Gateway_V1_PromptRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream { - return self.performAsyncServerStreamingCall( - path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestStreamingPrompt.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRequestStreamingPromptInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal struct Gateway_V1_APIGatewayServiceAsyncClient: Gateway_V1_APIGatewayServiceAsyncClientProtocol { - internal var channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? - - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -internal protocol Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when invoking 'requestPrompt'. - func makeRequestPromptInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'requestStreamingPrompt'. - func makeRequestStreamingPromptInterceptors() -> [ClientInterceptor] -} - -internal enum Gateway_V1_APIGatewayServiceClientMetadata { - internal static let serviceDescriptor = GRPCServiceDescriptor( - name: "APIGatewayService", - fullName: "gateway.v1.APIGatewayService", - methods: [ - Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestPrompt, - Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestStreamingPrompt, - ] - ) - - internal enum Methods { - internal static let requestPrompt = GRPCMethodDescriptor( - name: "RequestPrompt", - path: "/gateway.v1.APIGatewayService/RequestPrompt", - type: GRPCCallType.unary - ) - - internal static let requestStreamingPrompt = GRPCMethodDescriptor( - name: "RequestStreamingPrompt", - path: "/gateway.v1.APIGatewayService/RequestStreamingPrompt", - type: GRPCCallType.serverStreaming - ) - } -} - -/// The API Gateway service definition. -/// -/// To build a server, implement a class that conforms to this protocol. -internal protocol Gateway_V1_APIGatewayServiceProvider: CallHandlerProvider { - var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { get } - - /// Request a regular LLM prompt - func requestPrompt(request: Gateway_V1_PromptRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Request a streaming LLM prompt - func requestStreamingPrompt(request: Gateway_V1_PromptRequest, context: StreamingResponseCallContext) -> EventLoopFuture -} - -extension Gateway_V1_APIGatewayServiceProvider { - internal var serviceName: Substring { - return Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "RequestPrompt": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRequestPromptInterceptors() ?? [], - userFunction: self.requestPrompt(request:context:) - ) - - case "RequestStreamingPrompt": - return ServerStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRequestStreamingPromptInterceptors() ?? [], - userFunction: self.requestStreamingPrompt(request:context:) - ) - - default: - return nil - } - } -} - -/// The API Gateway service definition. -/// -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Gateway_V1_APIGatewayServiceAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { get } - - /// Request a regular LLM prompt - func requestPrompt( - request: Gateway_V1_PromptRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Gateway_V1_PromptResponse - - /// Request a streaming LLM prompt - func requestStreamingPrompt( - request: Gateway_V1_PromptRequest, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Gateway_V1_APIGatewayServiceAsyncProvider { - internal static var serviceDescriptor: GRPCServiceDescriptor { - return Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor - } - - internal var serviceName: Substring { - return Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor.fullName[...] - } - - internal var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { - return nil - } - - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "RequestPrompt": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRequestPromptInterceptors() ?? [], - wrapping: { try await self.requestPrompt(request: $0, context: $1) } - ) - - case "RequestStreamingPrompt": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRequestStreamingPromptInterceptors() ?? [], - wrapping: { try await self.requestStreamingPrompt(request: $0, responseStream: $1, context: $2) } - ) - - default: - return nil - } - } -} - -internal protocol Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when handling 'requestPrompt'. - /// Defaults to calling `self.makeInterceptors()`. - func makeRequestPromptInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'requestStreamingPrompt'. - /// Defaults to calling `self.makeInterceptors()`. - func makeRequestStreamingPromptInterceptors() -> [ServerInterceptor] -} - -internal enum Gateway_V1_APIGatewayServiceServerMetadata { - internal static let serviceDescriptor = GRPCServiceDescriptor( - name: "APIGatewayService", - fullName: "gateway.v1.APIGatewayService", - methods: [ - Gateway_V1_APIGatewayServiceServerMetadata.Methods.requestPrompt, - Gateway_V1_APIGatewayServiceServerMetadata.Methods.requestStreamingPrompt, - ] - ) - - internal enum Methods { - internal static let requestPrompt = GRPCMethodDescriptor( - name: "RequestPrompt", - path: "/gateway.v1.APIGatewayService/RequestPrompt", - type: GRPCCallType.unary - ) - - internal static let requestStreamingPrompt = GRPCMethodDescriptor( - name: "RequestStreamingPrompt", - path: "/gateway.v1.APIGatewayService/RequestStreamingPrompt", - type: GRPCCallType.serverStreaming - ) - } -} diff --git a/Sources/BaseMindGateway/gateway.pb.swift b/Sources/BaseMindGateway/gateway.pb.swift deleted file mode 100644 index 163e714..0000000 --- a/Sources/BaseMindGateway/gateway.pb.swift +++ /dev/null @@ -1,292 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: gateway/v1/gateway.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// A request for a prompt - sending user input to the server. -struct Gateway_V1_PromptRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The User prompt variables - /// This is a hash-map of variables that should have the same keys as those contained by the PromptConfigResponse - var templateVariables: Dictionary = [:] - - /// Optional Identifier designating the prompt config ID to use. If not set, the default prompt config will be used. - var promptConfigID: String { - get {return _promptConfigID ?? String()} - set {_promptConfigID = newValue} - } - /// Returns true if `promptConfigID` has been explicitly set. - var hasPromptConfigID: Bool {return self._promptConfigID != nil} - /// Clears the value of `promptConfigID`. Subsequent reads from it will return its default value. - mutating func clearPromptConfigID() {self._promptConfigID = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _promptConfigID: String? = nil -} - -/// A Prompt Response Message -struct Gateway_V1_PromptResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Prompt Content - var content: String = String() - - /// Number of tokens used for the prompt request - var requestTokens: UInt32 = 0 - - /// Number of tokens used for the prompt response - var responseTokens: UInt32 = 0 - - /// Request duration - var requestDuration: UInt32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// An Streaming Prompt Response Message -struct Gateway_V1_StreamingPromptResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Prompt Content - var content: String = String() - - /// Finish reason, given when the stream ends - var finishReason: String { - get {return _finishReason ?? String()} - set {_finishReason = newValue} - } - /// Returns true if `finishReason` has been explicitly set. - var hasFinishReason: Bool {return self._finishReason != nil} - /// Clears the value of `finishReason`. Subsequent reads from it will return its default value. - mutating func clearFinishReason() {self._finishReason = nil} - - /// Number of tokens used for the prompt request, given when the stream ends - var requestTokens: UInt32 { - get {return _requestTokens ?? 0} - set {_requestTokens = newValue} - } - /// Returns true if `requestTokens` has been explicitly set. - var hasRequestTokens: Bool {return self._requestTokens != nil} - /// Clears the value of `requestTokens`. Subsequent reads from it will return its default value. - mutating func clearRequestTokens() {self._requestTokens = nil} - - /// Number of tokens used for the prompt response, given when the stream ends - var responseTokens: UInt32 { - get {return _responseTokens ?? 0} - set {_responseTokens = newValue} - } - /// Returns true if `responseTokens` has been explicitly set. - var hasResponseTokens: Bool {return self._responseTokens != nil} - /// Clears the value of `responseTokens`. Subsequent reads from it will return its default value. - mutating func clearResponseTokens() {self._responseTokens = nil} - - /// Stream duration, given when the stream ends - var streamDuration: UInt32 { - get {return _streamDuration ?? 0} - set {_streamDuration = newValue} - } - /// Returns true if `streamDuration` has been explicitly set. - var hasStreamDuration: Bool {return self._streamDuration != nil} - /// Clears the value of `streamDuration`. Subsequent reads from it will return its default value. - mutating func clearStreamDuration() {self._streamDuration = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _finishReason: String? = nil - fileprivate var _requestTokens: UInt32? = nil - fileprivate var _responseTokens: UInt32? = nil - fileprivate var _streamDuration: UInt32? = nil -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension Gateway_V1_PromptRequest: @unchecked Sendable {} -extension Gateway_V1_PromptResponse: @unchecked Sendable {} -extension Gateway_V1_StreamingPromptResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "gateway.v1" - -extension Gateway_V1_PromptRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".PromptRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "template_variables"), - 2: .standard(proto: "prompt_config_id"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.templateVariables) }() - case 2: try { try decoder.decodeSingularStringField(value: &self._promptConfigID) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.templateVariables.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.templateVariables, fieldNumber: 1) - } - try { if let v = self._promptConfigID { - try visitor.visitSingularStringField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Gateway_V1_PromptRequest, rhs: Gateway_V1_PromptRequest) -> Bool { - if lhs.templateVariables != rhs.templateVariables {return false} - if lhs._promptConfigID != rhs._promptConfigID {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Gateway_V1_PromptResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".PromptResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "content"), - 2: .standard(proto: "request_tokens"), - 3: .standard(proto: "response_tokens"), - 4: .standard(proto: "request_duration"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.content) }() - case 2: try { try decoder.decodeSingularUInt32Field(value: &self.requestTokens) }() - case 3: try { try decoder.decodeSingularUInt32Field(value: &self.responseTokens) }() - case 4: try { try decoder.decodeSingularUInt32Field(value: &self.requestDuration) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.content.isEmpty { - try visitor.visitSingularStringField(value: self.content, fieldNumber: 1) - } - if self.requestTokens != 0 { - try visitor.visitSingularUInt32Field(value: self.requestTokens, fieldNumber: 2) - } - if self.responseTokens != 0 { - try visitor.visitSingularUInt32Field(value: self.responseTokens, fieldNumber: 3) - } - if self.requestDuration != 0 { - try visitor.visitSingularUInt32Field(value: self.requestDuration, fieldNumber: 4) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Gateway_V1_PromptResponse, rhs: Gateway_V1_PromptResponse) -> Bool { - if lhs.content != rhs.content {return false} - if lhs.requestTokens != rhs.requestTokens {return false} - if lhs.responseTokens != rhs.responseTokens {return false} - if lhs.requestDuration != rhs.requestDuration {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Gateway_V1_StreamingPromptResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".StreamingPromptResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "content"), - 2: .standard(proto: "finish_reason"), - 3: .standard(proto: "request_tokens"), - 4: .standard(proto: "response_tokens"), - 5: .standard(proto: "stream_duration"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.content) }() - case 2: try { try decoder.decodeSingularStringField(value: &self._finishReason) }() - case 3: try { try decoder.decodeSingularUInt32Field(value: &self._requestTokens) }() - case 4: try { try decoder.decodeSingularUInt32Field(value: &self._responseTokens) }() - case 5: try { try decoder.decodeSingularUInt32Field(value: &self._streamDuration) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.content.isEmpty { - try visitor.visitSingularStringField(value: self.content, fieldNumber: 1) - } - try { if let v = self._finishReason { - try visitor.visitSingularStringField(value: v, fieldNumber: 2) - } }() - try { if let v = self._requestTokens { - try visitor.visitSingularUInt32Field(value: v, fieldNumber: 3) - } }() - try { if let v = self._responseTokens { - try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) - } }() - try { if let v = self._streamDuration { - try visitor.visitSingularUInt32Field(value: v, fieldNumber: 5) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Gateway_V1_StreamingPromptResponse, rhs: Gateway_V1_StreamingPromptResponse) -> Bool { - if lhs.content != rhs.content {return false} - if lhs._finishReason != rhs._finishReason {return false} - if lhs._requestTokens != rhs._requestTokens {return false} - if lhs._responseTokens != rhs._responseTokens {return false} - if lhs._streamDuration != rhs._streamDuration {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/GatewayGRPC/gateway.grpc.swift b/Sources/GatewayGRPC/gateway.grpc.swift new file mode 100644 index 0000000..f024c9a --- /dev/null +++ b/Sources/GatewayGRPC/gateway.grpc.swift @@ -0,0 +1,411 @@ +// +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the protocol buffer compiler. +// Source: gateway/v1/gateway.proto +// +import GRPC +import NIO +import NIOConcurrencyHelpers +import SwiftProtobuf + +/// The API Gateway service definition. +/// +/// Usage: instantiate `Gateway_V1_APIGatewayServiceClient`, then call methods of this protocol to make API calls. +protocol Gateway_V1_APIGatewayServiceClientProtocol: GRPCClient { + var serviceName: String { get } + var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { get } + + func requestPrompt( + _ request: Gateway_V1_PromptRequest, + callOptions: CallOptions? + ) -> UnaryCall + + func requestStreamingPrompt( + _ request: Gateway_V1_PromptRequest, + callOptions: CallOptions?, + handler: @escaping (Gateway_V1_StreamingPromptResponse) -> Void + ) -> ServerStreamingCall +} + +extension Gateway_V1_APIGatewayServiceClientProtocol { + var serviceName: String { + "gateway.v1.APIGatewayService" + } + + /// Request a regular LLM prompt + /// + /// - Parameters: + /// - request: Request to send to RequestPrompt. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + func requestPrompt( + _ request: Gateway_V1_PromptRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + makeUnaryCall( + path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestPrompt.path, + request: request, + callOptions: callOptions ?? defaultCallOptions, + interceptors: interceptors?.makeRequestPromptInterceptors() ?? [] + ) + } + + /// Request a streaming LLM prompt + /// + /// - Parameters: + /// - request: Request to send to RequestStreamingPrompt. + /// - callOptions: Call options. + /// - handler: A closure called when each response is received from the server. + /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. + func requestStreamingPrompt( + _ request: Gateway_V1_PromptRequest, + callOptions: CallOptions? = nil, + handler: @escaping (Gateway_V1_StreamingPromptResponse) -> Void + ) -> ServerStreamingCall { + makeServerStreamingCall( + path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestStreamingPrompt.path, + request: request, + callOptions: callOptions ?? defaultCallOptions, + interceptors: interceptors?.makeRequestStreamingPromptInterceptors() ?? [], + handler: handler + ) + } +} + +@available(*, deprecated) +extension Gateway_V1_APIGatewayServiceClient: @unchecked Sendable {} + +@available(*, deprecated, renamed: "Gateway_V1_APIGatewayServiceNIOClient") +final class Gateway_V1_APIGatewayServiceClient: Gateway_V1_APIGatewayServiceClientProtocol { + private let lock = Lock() + private var _defaultCallOptions: CallOptions + private var _interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? + let channel: GRPCChannel + var defaultCallOptions: CallOptions { + get { lock.withLock { self._defaultCallOptions } } + set { lock.withLockVoid { self._defaultCallOptions = newValue } } + } + + var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { + get { lock.withLock { self._interceptors } } + set { lock.withLockVoid { self._interceptors = newValue } } + } + + /// Creates a client for the gateway.v1.APIGatewayService service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + _defaultCallOptions = defaultCallOptions + _interceptors = interceptors + } +} + +struct Gateway_V1_APIGatewayServiceNIOClient: Gateway_V1_APIGatewayServiceClientProtocol { + var channel: GRPCChannel + var defaultCallOptions: CallOptions + var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? + + /// Creates a client for the gateway.v1.APIGatewayService service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +/// The API Gateway service definition. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +protocol Gateway_V1_APIGatewayServiceAsyncClientProtocol: GRPCClient { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { get } + + func makeRequestPromptCall( + _ request: Gateway_V1_PromptRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + + func makeRequestStreamingPromptCall( + _ request: Gateway_V1_PromptRequest, + callOptions: CallOptions? + ) -> GRPCAsyncServerStreamingCall +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Gateway_V1_APIGatewayServiceAsyncClientProtocol { + static var serviceDescriptor: GRPCServiceDescriptor { + Gateway_V1_APIGatewayServiceClientMetadata.serviceDescriptor + } + + var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { + nil + } + + func makeRequestPromptCall( + _ request: Gateway_V1_PromptRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + makeAsyncUnaryCall( + path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestPrompt.path, + request: request, + callOptions: callOptions ?? defaultCallOptions, + interceptors: interceptors?.makeRequestPromptInterceptors() ?? [] + ) + } + + func makeRequestStreamingPromptCall( + _ request: Gateway_V1_PromptRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncServerStreamingCall { + makeAsyncServerStreamingCall( + path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestStreamingPrompt.path, + request: request, + callOptions: callOptions ?? defaultCallOptions, + interceptors: interceptors?.makeRequestStreamingPromptInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Gateway_V1_APIGatewayServiceAsyncClientProtocol { + func requestPrompt( + _ request: Gateway_V1_PromptRequest, + callOptions: CallOptions? = nil + ) async throws -> Gateway_V1_PromptResponse { + try await performAsyncUnaryCall( + path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestPrompt.path, + request: request, + callOptions: callOptions ?? defaultCallOptions, + interceptors: interceptors?.makeRequestPromptInterceptors() ?? [] + ) + } + + func requestStreamingPrompt( + _ request: Gateway_V1_PromptRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncResponseStream { + performAsyncServerStreamingCall( + path: Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestStreamingPrompt.path, + request: request, + callOptions: callOptions ?? defaultCallOptions, + interceptors: interceptors?.makeRequestStreamingPromptInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +struct Gateway_V1_APIGatewayServiceAsyncClient: Gateway_V1_APIGatewayServiceAsyncClientProtocol { + var channel: GRPCChannel + var defaultCallOptions: CallOptions + var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? + + init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +protocol Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol: Sendable { + /// - Returns: Interceptors to use when invoking 'requestPrompt'. + func makeRequestPromptInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'requestStreamingPrompt'. + func makeRequestStreamingPromptInterceptors() -> [ClientInterceptor] +} + +enum Gateway_V1_APIGatewayServiceClientMetadata { + static let serviceDescriptor = GRPCServiceDescriptor( + name: "APIGatewayService", + fullName: "gateway.v1.APIGatewayService", + methods: [ + Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestPrompt, + Gateway_V1_APIGatewayServiceClientMetadata.Methods.requestStreamingPrompt, + ] + ) + + enum Methods { + static let requestPrompt = GRPCMethodDescriptor( + name: "RequestPrompt", + path: "/gateway.v1.APIGatewayService/RequestPrompt", + type: GRPCCallType.unary + ) + + static let requestStreamingPrompt = GRPCMethodDescriptor( + name: "RequestStreamingPrompt", + path: "/gateway.v1.APIGatewayService/RequestStreamingPrompt", + type: GRPCCallType.serverStreaming + ) + } +} + +/// The API Gateway service definition. +/// +/// To build a server, implement a class that conforms to this protocol. +protocol Gateway_V1_APIGatewayServiceProvider: CallHandlerProvider { + var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { get } + + /// Request a regular LLM prompt + func requestPrompt(request: Gateway_V1_PromptRequest, context: StatusOnlyCallContext) -> EventLoopFuture + + /// Request a streaming LLM prompt + func requestStreamingPrompt(request: Gateway_V1_PromptRequest, context: StreamingResponseCallContext) -> EventLoopFuture +} + +extension Gateway_V1_APIGatewayServiceProvider { + var serviceName: Substring { + Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor.fullName[...] + } + + /// Determines, calls and returns the appropriate request handler, depending on the request's method. + /// Returns nil for methods not handled by this service. + func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "RequestPrompt": + UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: interceptors?.makeRequestPromptInterceptors() ?? [], + userFunction: requestPrompt(request:context:) + ) + + case "RequestStreamingPrompt": + ServerStreamingServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: interceptors?.makeRequestStreamingPromptInterceptors() ?? [], + userFunction: requestStreamingPrompt(request:context:) + ) + + default: + nil + } + } +} + +/// The API Gateway service definition. +/// +/// To implement a server, implement an object which conforms to this protocol. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +protocol Gateway_V1_APIGatewayServiceAsyncProvider: CallHandlerProvider, Sendable { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { get } + + /// Request a regular LLM prompt + func requestPrompt( + request: Gateway_V1_PromptRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Gateway_V1_PromptResponse + + /// Request a streaming LLM prompt + func requestStreamingPrompt( + request: Gateway_V1_PromptRequest, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Gateway_V1_APIGatewayServiceAsyncProvider { + static var serviceDescriptor: GRPCServiceDescriptor { + Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor + } + + var serviceName: Substring { + Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor.fullName[...] + } + + var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { + nil + } + + func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "RequestPrompt": + GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: interceptors?.makeRequestPromptInterceptors() ?? [], + wrapping: { try await self.requestPrompt(request: $0, context: $1) } + ) + + case "RequestStreamingPrompt": + GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: interceptors?.makeRequestStreamingPromptInterceptors() ?? [], + wrapping: { try await self.requestStreamingPrompt(request: $0, responseStream: $1, context: $2) } + ) + + default: + nil + } + } +} + +protocol Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol: Sendable { + /// - Returns: Interceptors to use when handling 'requestPrompt'. + /// Defaults to calling `self.makeInterceptors()`. + func makeRequestPromptInterceptors() -> [ServerInterceptor] + + /// - Returns: Interceptors to use when handling 'requestStreamingPrompt'. + /// Defaults to calling `self.makeInterceptors()`. + func makeRequestStreamingPromptInterceptors() -> [ServerInterceptor] +} + +enum Gateway_V1_APIGatewayServiceServerMetadata { + static let serviceDescriptor = GRPCServiceDescriptor( + name: "APIGatewayService", + fullName: "gateway.v1.APIGatewayService", + methods: [ + Gateway_V1_APIGatewayServiceServerMetadata.Methods.requestPrompt, + Gateway_V1_APIGatewayServiceServerMetadata.Methods.requestStreamingPrompt, + ] + ) + + enum Methods { + static let requestPrompt = GRPCMethodDescriptor( + name: "RequestPrompt", + path: "/gateway.v1.APIGatewayService/RequestPrompt", + type: GRPCCallType.unary + ) + + static let requestStreamingPrompt = GRPCMethodDescriptor( + name: "RequestStreamingPrompt", + path: "/gateway.v1.APIGatewayService/RequestStreamingPrompt", + type: GRPCCallType.serverStreaming + ) + } +} diff --git a/Sources/GatewayGRPC/gateway.pb.swift b/Sources/GatewayGRPC/gateway.pb.swift new file mode 100644 index 0000000..b326213 --- /dev/null +++ b/Sources/GatewayGRPC/gateway.pb.swift @@ -0,0 +1,295 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: gateway/v1/gateway.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +private struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// A request for a prompt - sending user input to the server. +struct Gateway_V1_PromptRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The User prompt variables + /// This is a hash-map of variables that should have the same keys as those contained by the PromptConfigResponse + var templateVariables: [String: String] = [:] + + /// Optional Identifier designating the prompt config ID to use. If not set, the default prompt config will be used. + var promptConfigID: String { + get { _promptConfigID ?? String() } + set { _promptConfigID = newValue } + } + + /// Returns true if `promptConfigID` has been explicitly set. + var hasPromptConfigID: Bool { _promptConfigID != nil } + /// Clears the value of `promptConfigID`. Subsequent reads from it will return its default value. + mutating func clearPromptConfigID() { _promptConfigID = nil } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _promptConfigID: String? +} + +/// A Prompt Response Message +struct Gateway_V1_PromptResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Prompt Content + var content: String = .init() + + /// Number of tokens used for the prompt request + var requestTokens: UInt32 = 0 + + /// Number of tokens used for the prompt response + var responseTokens: UInt32 = 0 + + /// Request duration + var requestDuration: UInt32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() +} + +/// An Streaming Prompt Response Message +struct Gateway_V1_StreamingPromptResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Prompt Content + var content: String = .init() + + /// Finish reason, given when the stream ends + var finishReason: String { + get { _finishReason ?? String() } + set { _finishReason = newValue } + } + + /// Returns true if `finishReason` has been explicitly set. + var hasFinishReason: Bool { _finishReason != nil } + /// Clears the value of `finishReason`. Subsequent reads from it will return its default value. + mutating func clearFinishReason() { _finishReason = nil } + + /// Number of tokens used for the prompt request, given when the stream ends + var requestTokens: UInt32 { + get { _requestTokens ?? 0 } + set { _requestTokens = newValue } + } + + /// Returns true if `requestTokens` has been explicitly set. + var hasRequestTokens: Bool { _requestTokens != nil } + /// Clears the value of `requestTokens`. Subsequent reads from it will return its default value. + mutating func clearRequestTokens() { _requestTokens = nil } + + /// Number of tokens used for the prompt response, given when the stream ends + var responseTokens: UInt32 { + get { _responseTokens ?? 0 } + set { _responseTokens = newValue } + } + + /// Returns true if `responseTokens` has been explicitly set. + var hasResponseTokens: Bool { _responseTokens != nil } + /// Clears the value of `responseTokens`. Subsequent reads from it will return its default value. + mutating func clearResponseTokens() { _responseTokens = nil } + + /// Stream duration, given when the stream ends + var streamDuration: UInt32 { + get { _streamDuration ?? 0 } + set { _streamDuration = newValue } + } + + /// Returns true if `streamDuration` has been explicitly set. + var hasStreamDuration: Bool { _streamDuration != nil } + /// Clears the value of `streamDuration`. Subsequent reads from it will return its default value. + mutating func clearStreamDuration() { _streamDuration = nil } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _finishReason: String? + fileprivate var _requestTokens: UInt32? + fileprivate var _responseTokens: UInt32? + fileprivate var _streamDuration: UInt32? +} + +#if swift(>=5.5) && canImport(_Concurrency) + extension Gateway_V1_PromptRequest: @unchecked Sendable {} + extension Gateway_V1_PromptResponse: @unchecked Sendable {} + extension Gateway_V1_StreamingPromptResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +private let _protobuf_package = "gateway.v1" + +extension Gateway_V1_PromptRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PromptRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "template_variables"), + 2: .standard(proto: "prompt_config_id"), + ] + + mutating func decodeMessage(decoder: inout some SwiftProtobuf.Decoder) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &templateVariables) + case 2: try decoder.decodeSingularStringField(value: &_promptConfigID) + default: break + } + } + } + + func traverse(visitor: inout some SwiftProtobuf.Visitor) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !templateVariables.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: templateVariables, fieldNumber: 1) + } + try { if let v = self._promptConfigID { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func == (lhs: Gateway_V1_PromptRequest, rhs: Gateway_V1_PromptRequest) -> Bool { + if lhs.templateVariables != rhs.templateVariables { return false } + if lhs._promptConfigID != rhs._promptConfigID { return false } + if lhs.unknownFields != rhs.unknownFields { return false } + return true + } +} + +extension Gateway_V1_PromptResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PromptResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "content"), + 2: .standard(proto: "request_tokens"), + 3: .standard(proto: "response_tokens"), + 4: .standard(proto: "request_duration"), + ] + + mutating func decodeMessage(decoder: inout some SwiftProtobuf.Decoder) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &content) + case 2: try decoder.decodeSingularUInt32Field(value: &requestTokens) + case 3: try decoder.decodeSingularUInt32Field(value: &responseTokens) + case 4: try decoder.decodeSingularUInt32Field(value: &requestDuration) + default: break + } + } + } + + func traverse(visitor: inout some SwiftProtobuf.Visitor) throws { + if !content.isEmpty { + try visitor.visitSingularStringField(value: content, fieldNumber: 1) + } + if requestTokens != 0 { + try visitor.visitSingularUInt32Field(value: requestTokens, fieldNumber: 2) + } + if responseTokens != 0 { + try visitor.visitSingularUInt32Field(value: responseTokens, fieldNumber: 3) + } + if requestDuration != 0 { + try visitor.visitSingularUInt32Field(value: requestDuration, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func == (lhs: Gateway_V1_PromptResponse, rhs: Gateway_V1_PromptResponse) -> Bool { + if lhs.content != rhs.content { return false } + if lhs.requestTokens != rhs.requestTokens { return false } + if lhs.responseTokens != rhs.responseTokens { return false } + if lhs.requestDuration != rhs.requestDuration { return false } + if lhs.unknownFields != rhs.unknownFields { return false } + return true + } +} + +extension Gateway_V1_StreamingPromptResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".StreamingPromptResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "content"), + 2: .standard(proto: "finish_reason"), + 3: .standard(proto: "request_tokens"), + 4: .standard(proto: "response_tokens"), + 5: .standard(proto: "stream_duration"), + ] + + mutating func decodeMessage(decoder: inout some SwiftProtobuf.Decoder) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &content) + case 2: try decoder.decodeSingularStringField(value: &_finishReason) + case 3: try decoder.decodeSingularUInt32Field(value: &_requestTokens) + case 4: try decoder.decodeSingularUInt32Field(value: &_responseTokens) + case 5: try decoder.decodeSingularUInt32Field(value: &_streamDuration) + default: break + } + } + } + + func traverse(visitor: inout some SwiftProtobuf.Visitor) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !content.isEmpty { + try visitor.visitSingularStringField(value: content, fieldNumber: 1) + } + try { if let v = self._finishReason { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } }() + try { if let v = self._requestTokens { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 3) + } }() + try { if let v = self._responseTokens { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } }() + try { if let v = self._streamDuration { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 5) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func == (lhs: Gateway_V1_StreamingPromptResponse, rhs: Gateway_V1_StreamingPromptResponse) -> Bool { + if lhs.content != rhs.content { return false } + if lhs._finishReason != rhs._finishReason { return false } + if lhs._requestTokens != rhs._requestTokens { return false } + if lhs._responseTokens != rhs._responseTokens { return false } + if lhs._streamDuration != rhs._streamDuration { return false } + if lhs.unknownFields != rhs.unknownFields { return false } + return true + } +} From 45a6a365f6caf2fc822b4eae841945f43503a186 Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Fri, 8 Dec 2023 16:47:24 +0100 Subject: [PATCH 03/15] chore: updated proto build and genrated code --- .gitignore | 8 +- Package.resolved | 4 +- Sources/BaseMindClient/BaseMindClient.swift | 52 +++-- .../gateway.grpc.swift | 203 +++--------------- .../gateway.pb.swift | 102 ++++----- Taskfile.yaml | 13 ++ 6 files changed, 138 insertions(+), 244 deletions(-) rename Sources/{GatewayGRPC => BaseMindGateway}/gateway.grpc.swift (54%) rename Sources/{GatewayGRPC => BaseMindGateway}/gateway.pb.swift (77%) diff --git a/.gitignore b/.gitignore index b46fb9f..d65e442 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -*.ipa -*.dSYM.zip -*.dSYM .build/ +.DS_Store +*.dSYM +*.dSYM.zip +*.ipa node_modules/ xcuserdata/ +tmp/ diff --git a/Package.resolved b/Package.resolved index 989545a..1cfe203 100644 --- a/Package.resolved +++ b/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types", "state" : { - "revision" : "4c542094de9b8316809a28ef51c434ee6ce6ee43", - "version" : "1.0.1" + "revision" : "1827dc94bdab2eb5f2fc804e9b0cb43574282566", + "version" : "1.0.2" } }, { diff --git a/Sources/BaseMindClient/BaseMindClient.swift b/Sources/BaseMindClient/BaseMindClient.swift index 4f157e0..cc3ac25 100644 --- a/Sources/BaseMindClient/BaseMindClient.swift +++ b/Sources/BaseMindClient/BaseMindClient.swift @@ -1,8 +1,10 @@ import BaseMindGateway import Foundation import GRPC +import Logging enum BaseMindError: Error { + case missingToken case connectionFailure } @@ -10,41 +12,65 @@ let DEFAULT_API_GATEWAY_ADDRESS = "gateway.basemind.ai" let DEFAULT_API_GATEWAY_PORT = 443 public struct Options { + /* The gRPC server address. */ var host: String = DEFAULT_API_GATEWAY_ADDRESS + /* The gRPC server port. */ var port: Int = DEFAULT_API_GATEWAY_PORT + /* A flag dictating whether debug messages will be logged. */ var debug: Bool = false + /* The ID of the prompt configuration to use. + + This value is optional. If not provided, the default prompt configuration will be used. + */ var promptConfigId: String? - var logger: Logger = .init( - subsystem: Bundle.main.bundleIdentifier!, - category: "BaseMindClient" - ) + /* The logger instance to use. + + Note: messages are logged only when 'debug' is set to true. + */ + var logger: Logger = .init(label: "BaseMindClient") } public class BaseMindClient { - private let apiToken: String = "" - private let promptConfigId: String? + private let client: BaseMindGateway.Gateway_V1_APIGatewayServiceAsyncClient + private let apiToken: String + private let options: Options init(apiToken: String, options: Options = Options()) throws { + if apiToken == "" { + throw BaseMindError.missingToken + } + self.apiToken = apiToken + self.options = options - if options.debug { - options.logger.debug("initializing client") + if self.options.debug { + self.options.logger.debug("initializing client") } - let eventLoop = MultiThreadedEventLoopGroup(numberOfThreads: 1) - + let eventLoopGroup = PlatformSupport.makeEventLoopGroup(loopCount: 1) do { let channel = try GRPCChannelPool.with( target: .host(options.host, port: options.port), - transportSecurity: .tls, + transportSecurity: .plaintext, eventLoopGroup: eventLoopGroup ) + client = BaseMindGateway.Gateway_V1_APIGatewayServiceAsyncClient(channel: channel) + + if self.options.debug { + self.options.logger.debug("Successfully connected to BaseMind.AI server on \(options.host):\(options.port).") + } } catch { - if options.debug { - options.logger.debug("Failed to connect to BaseMind server on \(options.host):\(options.port). Error: \(error).") + if self.options.debug { + self.options.logger.debug("Failed to connect to the BaseMind.AI server on \(options.host):\(options.port). Error: \(error).") } throw BaseMindError.connectionFailure } } + + func requestPrompt(templateVariables _: [String: String]? = nil) async -> BaseMindGateway.Gateway_V1_PromptResponse { + do {} + } + + func requestStream(templateVariables _: [String: String]? = nil) async -> BaseMindGateway.Gateway_V1_StreamingPromptResponse {} } diff --git a/Sources/GatewayGRPC/gateway.grpc.swift b/Sources/BaseMindGateway/gateway.grpc.swift similarity index 54% rename from Sources/GatewayGRPC/gateway.grpc.swift rename to Sources/BaseMindGateway/gateway.grpc.swift index f024c9a..14595ed 100644 --- a/Sources/GatewayGRPC/gateway.grpc.swift +++ b/Sources/BaseMindGateway/gateway.grpc.swift @@ -3,7 +3,7 @@ // swift-format-ignore-file // // Generated by the protocol buffer compiler. -// Source: gateway/v1/gateway.proto +// Source: proto/gateway/v1/gateway.proto // import GRPC import NIO @@ -13,7 +13,7 @@ import SwiftProtobuf /// The API Gateway service definition. /// /// Usage: instantiate `Gateway_V1_APIGatewayServiceClient`, then call methods of this protocol to make API calls. -protocol Gateway_V1_APIGatewayServiceClientProtocol: GRPCClient { +public protocol Gateway_V1_APIGatewayServiceClientProtocol: GRPCClient { var serviceName: String { get } var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { get } @@ -29,7 +29,7 @@ protocol Gateway_V1_APIGatewayServiceClientProtocol: GRPCClient { ) -> ServerStreamingCall } -extension Gateway_V1_APIGatewayServiceClientProtocol { +public extension Gateway_V1_APIGatewayServiceClientProtocol { var serviceName: String { "gateway.v1.APIGatewayService" } @@ -78,17 +78,17 @@ extension Gateway_V1_APIGatewayServiceClientProtocol { extension Gateway_V1_APIGatewayServiceClient: @unchecked Sendable {} @available(*, deprecated, renamed: "Gateway_V1_APIGatewayServiceNIOClient") -final class Gateway_V1_APIGatewayServiceClient: Gateway_V1_APIGatewayServiceClientProtocol { +public final class Gateway_V1_APIGatewayServiceClient: Gateway_V1_APIGatewayServiceClientProtocol { private let lock = Lock() private var _defaultCallOptions: CallOptions private var _interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? - let channel: GRPCChannel - var defaultCallOptions: CallOptions { + public let channel: GRPCChannel + public var defaultCallOptions: CallOptions { get { lock.withLock { self._defaultCallOptions } } set { lock.withLockVoid { self._defaultCallOptions = newValue } } } - var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { + public var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { get { lock.withLock { self._interceptors } } set { lock.withLockVoid { self._interceptors = newValue } } } @@ -99,7 +99,7 @@ final class Gateway_V1_APIGatewayServiceClient: Gateway_V1_APIGatewayServiceClie /// - channel: `GRPCChannel` to the service host. /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. /// - interceptors: A factory providing interceptors for each RPC. - init( + public init( channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions(), interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? = nil @@ -110,10 +110,10 @@ final class Gateway_V1_APIGatewayServiceClient: Gateway_V1_APIGatewayServiceClie } } -struct Gateway_V1_APIGatewayServiceNIOClient: Gateway_V1_APIGatewayServiceClientProtocol { - var channel: GRPCChannel - var defaultCallOptions: CallOptions - var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? +public struct Gateway_V1_APIGatewayServiceNIOClient: Gateway_V1_APIGatewayServiceClientProtocol { + public var channel: GRPCChannel + public var defaultCallOptions: CallOptions + public var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? /// Creates a client for the gateway.v1.APIGatewayService service. /// @@ -121,7 +121,7 @@ struct Gateway_V1_APIGatewayServiceNIOClient: Gateway_V1_APIGatewayServiceClient /// - channel: `GRPCChannel` to the service host. /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. /// - interceptors: A factory providing interceptors for each RPC. - init( + public init( channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions(), interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? = nil @@ -134,7 +134,7 @@ struct Gateway_V1_APIGatewayServiceNIOClient: Gateway_V1_APIGatewayServiceClient /// The API Gateway service definition. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -protocol Gateway_V1_APIGatewayServiceAsyncClientProtocol: GRPCClient { +public protocol Gateway_V1_APIGatewayServiceAsyncClientProtocol: GRPCClient { static var serviceDescriptor: GRPCServiceDescriptor { get } var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? { get } @@ -150,7 +150,7 @@ protocol Gateway_V1_APIGatewayServiceAsyncClientProtocol: GRPCClient { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Gateway_V1_APIGatewayServiceAsyncClientProtocol { +public extension Gateway_V1_APIGatewayServiceAsyncClientProtocol { static var serviceDescriptor: GRPCServiceDescriptor { Gateway_V1_APIGatewayServiceClientMetadata.serviceDescriptor } @@ -185,7 +185,7 @@ extension Gateway_V1_APIGatewayServiceAsyncClientProtocol { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Gateway_V1_APIGatewayServiceAsyncClientProtocol { +public extension Gateway_V1_APIGatewayServiceAsyncClientProtocol { func requestPrompt( _ request: Gateway_V1_PromptRequest, callOptions: CallOptions? = nil @@ -212,12 +212,12 @@ extension Gateway_V1_APIGatewayServiceAsyncClientProtocol { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct Gateway_V1_APIGatewayServiceAsyncClient: Gateway_V1_APIGatewayServiceAsyncClientProtocol { - var channel: GRPCChannel - var defaultCallOptions: CallOptions - var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? +public struct Gateway_V1_APIGatewayServiceAsyncClient: Gateway_V1_APIGatewayServiceAsyncClientProtocol { + public var channel: GRPCChannel + public var defaultCallOptions: CallOptions + public var interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? - init( + public init( channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions(), interceptors: Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol? = nil @@ -228,7 +228,7 @@ struct Gateway_V1_APIGatewayServiceAsyncClient: Gateway_V1_APIGatewayServiceAsyn } } -protocol Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol: Sendable { +public protocol Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when invoking 'requestPrompt'. func makeRequestPromptInterceptors() -> [ClientInterceptor] @@ -236,8 +236,8 @@ protocol Gateway_V1_APIGatewayServiceClientInterceptorFactoryProtocol: Sendable func makeRequestStreamingPromptInterceptors() -> [ClientInterceptor] } -enum Gateway_V1_APIGatewayServiceClientMetadata { - static let serviceDescriptor = GRPCServiceDescriptor( +public enum Gateway_V1_APIGatewayServiceClientMetadata { + public static let serviceDescriptor = GRPCServiceDescriptor( name: "APIGatewayService", fullName: "gateway.v1.APIGatewayService", methods: [ @@ -246,163 +246,14 @@ enum Gateway_V1_APIGatewayServiceClientMetadata { ] ) - enum Methods { - static let requestPrompt = GRPCMethodDescriptor( - name: "RequestPrompt", - path: "/gateway.v1.APIGatewayService/RequestPrompt", - type: GRPCCallType.unary - ) - - static let requestStreamingPrompt = GRPCMethodDescriptor( - name: "RequestStreamingPrompt", - path: "/gateway.v1.APIGatewayService/RequestStreamingPrompt", - type: GRPCCallType.serverStreaming - ) - } -} - -/// The API Gateway service definition. -/// -/// To build a server, implement a class that conforms to this protocol. -protocol Gateway_V1_APIGatewayServiceProvider: CallHandlerProvider { - var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { get } - - /// Request a regular LLM prompt - func requestPrompt(request: Gateway_V1_PromptRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Request a streaming LLM prompt - func requestStreamingPrompt(request: Gateway_V1_PromptRequest, context: StreamingResponseCallContext) -> EventLoopFuture -} - -extension Gateway_V1_APIGatewayServiceProvider { - var serviceName: Substring { - Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "RequestPrompt": - UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: interceptors?.makeRequestPromptInterceptors() ?? [], - userFunction: requestPrompt(request:context:) - ) - - case "RequestStreamingPrompt": - ServerStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: interceptors?.makeRequestStreamingPromptInterceptors() ?? [], - userFunction: requestStreamingPrompt(request:context:) - ) - - default: - nil - } - } -} - -/// The API Gateway service definition. -/// -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -protocol Gateway_V1_APIGatewayServiceAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { get } - - /// Request a regular LLM prompt - func requestPrompt( - request: Gateway_V1_PromptRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Gateway_V1_PromptResponse - - /// Request a streaming LLM prompt - func requestStreamingPrompt( - request: Gateway_V1_PromptRequest, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Gateway_V1_APIGatewayServiceAsyncProvider { - static var serviceDescriptor: GRPCServiceDescriptor { - Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor - } - - var serviceName: Substring { - Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor.fullName[...] - } - - var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { - nil - } - - func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "RequestPrompt": - GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: interceptors?.makeRequestPromptInterceptors() ?? [], - wrapping: { try await self.requestPrompt(request: $0, context: $1) } - ) - - case "RequestStreamingPrompt": - GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: interceptors?.makeRequestStreamingPromptInterceptors() ?? [], - wrapping: { try await self.requestStreamingPrompt(request: $0, responseStream: $1, context: $2) } - ) - - default: - nil - } - } -} - -protocol Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol: Sendable { - /// - Returns: Interceptors to use when handling 'requestPrompt'. - /// Defaults to calling `self.makeInterceptors()`. - func makeRequestPromptInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'requestStreamingPrompt'. - /// Defaults to calling `self.makeInterceptors()`. - func makeRequestStreamingPromptInterceptors() -> [ServerInterceptor] -} - -enum Gateway_V1_APIGatewayServiceServerMetadata { - static let serviceDescriptor = GRPCServiceDescriptor( - name: "APIGatewayService", - fullName: "gateway.v1.APIGatewayService", - methods: [ - Gateway_V1_APIGatewayServiceServerMetadata.Methods.requestPrompt, - Gateway_V1_APIGatewayServiceServerMetadata.Methods.requestStreamingPrompt, - ] - ) - - enum Methods { - static let requestPrompt = GRPCMethodDescriptor( + public enum Methods { + public static let requestPrompt = GRPCMethodDescriptor( name: "RequestPrompt", path: "/gateway.v1.APIGatewayService/RequestPrompt", type: GRPCCallType.unary ) - static let requestStreamingPrompt = GRPCMethodDescriptor( + public static let requestStreamingPrompt = GRPCMethodDescriptor( name: "RequestStreamingPrompt", path: "/gateway.v1.APIGatewayService/RequestStreamingPrompt", type: GRPCCallType.serverStreaming diff --git a/Sources/GatewayGRPC/gateway.pb.swift b/Sources/BaseMindGateway/gateway.pb.swift similarity index 77% rename from Sources/GatewayGRPC/gateway.pb.swift rename to Sources/BaseMindGateway/gateway.pb.swift index b326213..5e112de 100644 --- a/Sources/GatewayGRPC/gateway.pb.swift +++ b/Sources/BaseMindGateway/gateway.pb.swift @@ -2,7 +2,7 @@ // swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: gateway/v1/gateway.proto +// Source: proto/gateway/v1/gateway.proto // // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ @@ -21,115 +21,117 @@ private struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVer } /// A request for a prompt - sending user input to the server. -struct Gateway_V1_PromptRequest { +public struct Gateway_V1_PromptRequest { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// The User prompt variables /// This is a hash-map of variables that should have the same keys as those contained by the PromptConfigResponse - var templateVariables: [String: String] = [:] + public var templateVariables: [String: String] = [:] /// Optional Identifier designating the prompt config ID to use. If not set, the default prompt config will be used. - var promptConfigID: String { + public var promptConfigID: String { get { _promptConfigID ?? String() } set { _promptConfigID = newValue } } /// Returns true if `promptConfigID` has been explicitly set. - var hasPromptConfigID: Bool { _promptConfigID != nil } + public var hasPromptConfigID: Bool { _promptConfigID != nil } /// Clears the value of `promptConfigID`. Subsequent reads from it will return its default value. - mutating func clearPromptConfigID() { _promptConfigID = nil } + public mutating func clearPromptConfigID() { _promptConfigID = nil } - var unknownFields = SwiftProtobuf.UnknownStorage() + public var unknownFields = SwiftProtobuf.UnknownStorage() - init() {} + public init() {} - fileprivate var _promptConfigID: String? + private var _promptConfigID: String? } /// A Prompt Response Message -struct Gateway_V1_PromptResponse { +public struct Gateway_V1_PromptResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// Prompt Content - var content: String = .init() + public var content: String = .init() /// Number of tokens used for the prompt request - var requestTokens: UInt32 = 0 + public var requestTokens: UInt32 = 0 /// Number of tokens used for the prompt response - var responseTokens: UInt32 = 0 + public var responseTokens: UInt32 = 0 /// Request duration - var requestDuration: UInt32 = 0 + public var requestDuration: UInt32 = 0 - var unknownFields = SwiftProtobuf.UnknownStorage() + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} } /// An Streaming Prompt Response Message -struct Gateway_V1_StreamingPromptResponse { +public struct Gateway_V1_StreamingPromptResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// Prompt Content - var content: String = .init() + public var content: String = .init() /// Finish reason, given when the stream ends - var finishReason: String { + public var finishReason: String { get { _finishReason ?? String() } set { _finishReason = newValue } } /// Returns true if `finishReason` has been explicitly set. - var hasFinishReason: Bool { _finishReason != nil } + public var hasFinishReason: Bool { _finishReason != nil } /// Clears the value of `finishReason`. Subsequent reads from it will return its default value. - mutating func clearFinishReason() { _finishReason = nil } + public mutating func clearFinishReason() { _finishReason = nil } /// Number of tokens used for the prompt request, given when the stream ends - var requestTokens: UInt32 { + public var requestTokens: UInt32 { get { _requestTokens ?? 0 } set { _requestTokens = newValue } } /// Returns true if `requestTokens` has been explicitly set. - var hasRequestTokens: Bool { _requestTokens != nil } + public var hasRequestTokens: Bool { _requestTokens != nil } /// Clears the value of `requestTokens`. Subsequent reads from it will return its default value. - mutating func clearRequestTokens() { _requestTokens = nil } + public mutating func clearRequestTokens() { _requestTokens = nil } /// Number of tokens used for the prompt response, given when the stream ends - var responseTokens: UInt32 { + public var responseTokens: UInt32 { get { _responseTokens ?? 0 } set { _responseTokens = newValue } } /// Returns true if `responseTokens` has been explicitly set. - var hasResponseTokens: Bool { _responseTokens != nil } + public var hasResponseTokens: Bool { _responseTokens != nil } /// Clears the value of `responseTokens`. Subsequent reads from it will return its default value. - mutating func clearResponseTokens() { _responseTokens = nil } + public mutating func clearResponseTokens() { _responseTokens = nil } /// Stream duration, given when the stream ends - var streamDuration: UInt32 { + public var streamDuration: UInt32 { get { _streamDuration ?? 0 } set { _streamDuration = newValue } } /// Returns true if `streamDuration` has been explicitly set. - var hasStreamDuration: Bool { _streamDuration != nil } + public var hasStreamDuration: Bool { _streamDuration != nil } /// Clears the value of `streamDuration`. Subsequent reads from it will return its default value. - mutating func clearStreamDuration() { _streamDuration = nil } + public mutating func clearStreamDuration() { _streamDuration = nil } - var unknownFields = SwiftProtobuf.UnknownStorage() + public var unknownFields = SwiftProtobuf.UnknownStorage() - init() {} + public init() {} - fileprivate var _finishReason: String? - fileprivate var _requestTokens: UInt32? - fileprivate var _responseTokens: UInt32? - fileprivate var _streamDuration: UInt32? + private var _finishReason: String? + private var _requestTokens: UInt32? + private var _responseTokens: UInt32? + private var _streamDuration: UInt32? } #if swift(>=5.5) && canImport(_Concurrency) @@ -143,13 +145,13 @@ struct Gateway_V1_StreamingPromptResponse { private let _protobuf_package = "gateway.v1" extension Gateway_V1_PromptRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".PromptRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + public static let protoMessageName: String = _protobuf_package + ".PromptRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "template_variables"), 2: .standard(proto: "prompt_config_id"), ] - mutating func decodeMessage(decoder: inout some SwiftProtobuf.Decoder) throws { + public mutating func decodeMessage(decoder: inout some SwiftProtobuf.Decoder) throws { while let fieldNumber = try decoder.nextFieldNumber() { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every case branch when no optimizations are @@ -162,7 +164,7 @@ extension Gateway_V1_PromptRequest: SwiftProtobuf.Message, SwiftProtobuf._Messag } } - func traverse(visitor: inout some SwiftProtobuf.Visitor) throws { + public func traverse(visitor: inout some SwiftProtobuf.Visitor) throws { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and @@ -176,7 +178,7 @@ extension Gateway_V1_PromptRequest: SwiftProtobuf.Message, SwiftProtobuf._Messag try unknownFields.traverse(visitor: &visitor) } - static func == (lhs: Gateway_V1_PromptRequest, rhs: Gateway_V1_PromptRequest) -> Bool { + public static func == (lhs: Gateway_V1_PromptRequest, rhs: Gateway_V1_PromptRequest) -> Bool { if lhs.templateVariables != rhs.templateVariables { return false } if lhs._promptConfigID != rhs._promptConfigID { return false } if lhs.unknownFields != rhs.unknownFields { return false } @@ -185,15 +187,15 @@ extension Gateway_V1_PromptRequest: SwiftProtobuf.Message, SwiftProtobuf._Messag } extension Gateway_V1_PromptResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".PromptResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + public static let protoMessageName: String = _protobuf_package + ".PromptResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "content"), 2: .standard(proto: "request_tokens"), 3: .standard(proto: "response_tokens"), 4: .standard(proto: "request_duration"), ] - mutating func decodeMessage(decoder: inout some SwiftProtobuf.Decoder) throws { + public mutating func decodeMessage(decoder: inout some SwiftProtobuf.Decoder) throws { while let fieldNumber = try decoder.nextFieldNumber() { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every case branch when no optimizations are @@ -208,7 +210,7 @@ extension Gateway_V1_PromptResponse: SwiftProtobuf.Message, SwiftProtobuf._Messa } } - func traverse(visitor: inout some SwiftProtobuf.Visitor) throws { + public func traverse(visitor: inout some SwiftProtobuf.Visitor) throws { if !content.isEmpty { try visitor.visitSingularStringField(value: content, fieldNumber: 1) } @@ -224,7 +226,7 @@ extension Gateway_V1_PromptResponse: SwiftProtobuf.Message, SwiftProtobuf._Messa try unknownFields.traverse(visitor: &visitor) } - static func == (lhs: Gateway_V1_PromptResponse, rhs: Gateway_V1_PromptResponse) -> Bool { + public static func == (lhs: Gateway_V1_PromptResponse, rhs: Gateway_V1_PromptResponse) -> Bool { if lhs.content != rhs.content { return false } if lhs.requestTokens != rhs.requestTokens { return false } if lhs.responseTokens != rhs.responseTokens { return false } @@ -235,8 +237,8 @@ extension Gateway_V1_PromptResponse: SwiftProtobuf.Message, SwiftProtobuf._Messa } extension Gateway_V1_StreamingPromptResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".StreamingPromptResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + public static let protoMessageName: String = _protobuf_package + ".StreamingPromptResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "content"), 2: .standard(proto: "finish_reason"), 3: .standard(proto: "request_tokens"), @@ -244,7 +246,7 @@ extension Gateway_V1_StreamingPromptResponse: SwiftProtobuf.Message, SwiftProtob 5: .standard(proto: "stream_duration"), ] - mutating func decodeMessage(decoder: inout some SwiftProtobuf.Decoder) throws { + public mutating func decodeMessage(decoder: inout some SwiftProtobuf.Decoder) throws { while let fieldNumber = try decoder.nextFieldNumber() { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every case branch when no optimizations are @@ -260,7 +262,7 @@ extension Gateway_V1_StreamingPromptResponse: SwiftProtobuf.Message, SwiftProtob } } - func traverse(visitor: inout some SwiftProtobuf.Visitor) throws { + public func traverse(visitor: inout some SwiftProtobuf.Visitor) throws { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and @@ -283,7 +285,7 @@ extension Gateway_V1_StreamingPromptResponse: SwiftProtobuf.Message, SwiftProtob try unknownFields.traverse(visitor: &visitor) } - static func == (lhs: Gateway_V1_StreamingPromptResponse, rhs: Gateway_V1_StreamingPromptResponse) -> Bool { + public static func == (lhs: Gateway_V1_StreamingPromptResponse, rhs: Gateway_V1_StreamingPromptResponse) -> Bool { if lhs.content != rhs.content { return false } if lhs._finishReason != rhs._finishReason { return false } if lhs._requestTokens != rhs._requestTokens { return false } diff --git a/Taskfile.yaml b/Taskfile.yaml index cb27a4c..0a16ee6 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -4,6 +4,9 @@ tasks: setup: desc: Setup the project dependencies cmds: + - brew update && brew upgrade + - command -v protoc &> /dev/null || brew install protobuf + - brew install swift-protobuf grpc-swift - command -v pre-commit &> /dev/null || brew install pre-commit - pre-commit install && pre-commit install --hook-type commit-msg && pre-commit install-hooks update: @@ -15,3 +18,13 @@ tasks: desc: Lint the project cmds: - pre-commit run --show-diff-on-failure --color=always --all-files + generate: + desc: Generates swift code from the proto files + cmds: + - rm -rf tmp + - mkdir -p tmp + - protoc --swift_opt=Visibility=public --swift_out=tmp proto/gateway/v1/gateway.proto + - protoc --grpc-swift_opt=Client=true,Server=false,Visibility=Public --grpc-swift_out=tmp proto/gateway/v1/gateway.proto + - rm -rf Sources/BaseMindGateway + - mv tmp/proto/gateway/v1 Sources/BaseMindGateway + - rm -rf tmp From cbdd728645601a3e79d014a4ea0918ffa3dea107 Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Sat, 9 Dec 2023 15:14:17 +0100 Subject: [PATCH 04/15] feat: finish client implementation --- Sources/BaseMindClient/BaseMindClient.swift | 124 +++++++++++++++++--- 1 file changed, 108 insertions(+), 16 deletions(-) diff --git a/Sources/BaseMindClient/BaseMindClient.swift b/Sources/BaseMindClient/BaseMindClient.swift index cc3ac25..43dfde5 100644 --- a/Sources/BaseMindClient/BaseMindClient.swift +++ b/Sources/BaseMindClient/BaseMindClient.swift @@ -4,7 +4,13 @@ import GRPC import Logging enum BaseMindError: Error { + /// generic error thrown whenever there is an error communicating with the server + case serverError + /// thrown when the server responds to a request with the gRPC 'invalid argument' status + case invalidArgument + /// thrown when the token passed to the SDK is empty case missingToken + /// thrown when the connection to the server fails case connectionFailure } @@ -12,26 +18,28 @@ let DEFAULT_API_GATEWAY_ADDRESS = "gateway.basemind.ai" let DEFAULT_API_GATEWAY_PORT = 443 public struct Options { - /* The gRPC server address. */ + /// The gRPC server address. var host: String = DEFAULT_API_GATEWAY_ADDRESS - /* The gRPC server port. */ + /// The gRPC server port. var port: Int = DEFAULT_API_GATEWAY_PORT - /* A flag dictating whether debug messages will be logged. */ + /// A flag dictating whether debug messages will be logged. var debug: Bool = false - /* The ID of the prompt configuration to use. - - This value is optional. If not provided, the default prompt configuration will be used. - */ + /// The ID of the prompt configuration to use. + /// + /// Note: This value is optional. If not provided, the default prompt configuration will be used. var promptConfigId: String? - /* The logger instance to use. - - Note: messages are logged only when 'debug' is set to true. - */ + /// The logger instance to use. + /// + /// Note: messages are logged only when 'debug' is set to true. var logger: Logger = .init(label: "BaseMindClient") } +/// The BaseMindClient +/// +/// Initialize the client by passing the initializer the api key that you created on the BaseMind dashboard. +/// You can also pass an options object. public class BaseMindClient { - private let client: BaseMindGateway.Gateway_V1_APIGatewayServiceAsyncClient + private let client: Gateway_V1_APIGatewayServiceAsyncClient private let apiToken: String private let options: Options @@ -54,7 +62,7 @@ public class BaseMindClient { transportSecurity: .plaintext, eventLoopGroup: eventLoopGroup ) - client = BaseMindGateway.Gateway_V1_APIGatewayServiceAsyncClient(channel: channel) + client = Gateway_V1_APIGatewayServiceAsyncClient(channel: channel) if self.options.debug { self.options.logger.debug("Successfully connected to BaseMind.AI server on \(options.host):\(options.port).") @@ -68,9 +76,93 @@ public class BaseMindClient { } } - func requestPrompt(templateVariables _: [String: String]? = nil) async -> BaseMindGateway.Gateway_V1_PromptResponse { - do {} + private func createCallOptions() -> CallOptions { + var options = CallOptions() + + options.customMetadata.add( + name: "authorization", + value: "Bearer \(apiToken)" + ) + + return options } - func requestStream(templateVariables _: [String: String]? = nil) async -> BaseMindGateway.Gateway_V1_StreamingPromptResponse {} + private func createRequest(_ templateVariables: [String: String]? = nil) -> Gateway_V1_PromptRequest { + var request = Gateway_V1_PromptRequest() + + if let variables = templateVariables { + request.templateVariables = variables + } + + if let configId = options.promptConfigId { + request.promptConfigID = configId + } + + return request + } + + /// Request an LLM Prompt. + /// - Parameter templateVariables: A dictionary of key/value strings. This dictionary should supply the values required for any template variables defined in the BaseMind dashboard. + /// - Returns: A prompt response object. + public func requestPrompt(_ templateVariables: [String: String]? = nil) async throws -> Gateway_V1_PromptResponse { + do { + if options.debug { + options.logger.debug("requesting prompt") + } + + let request = createRequest(templateVariables) + return try await client.requestPrompt(request, callOptions: createCallOptions()) + } catch { + if options.debug { + options.logger.debug("error requesting prompt \(error)") + } + + if let status = error as? GRPCStatus { + throw status.code == GRPCStatus.Code.invalidArgument ? BaseMindError.invalidArgument : BaseMindError.serverError + } + throw BaseMindError.serverError + } + } + + public func requestStream(_ templateVariables: [String: String]? = nil) async throws -> ThrowingRequestStream { + if options.debug { + options.logger.debug("requesting streaming prompt") + } + + let request = createRequest(templateVariables) + let stream = client.requestStreamingPrompt(request, callOptions: createCallOptions()) + + return ThrowingRequestStream(stream: stream, + logError: { + (error: Error) in + if self.options.debug { + self.options.logger.debug("erroring streaming prompt \(error)") + } + + }) + } +} + +public struct ThrowingRequestStream: AsyncIteratorProtocol { + private var iterator: GRPCAsyncResponseStream.AsyncIterator + private let logError: (_ error: Error) -> Void + + init(stream: GRPCAsyncResponseStream, logError: @escaping (_ error: Error) -> Void) { + iterator = stream.makeAsyncIterator() + self.logError = logError + } + + public mutating func next() async throws -> Element? { + if Task.isCancelled { throw GRPCStatus(code: .cancelled) } + do { + return try await iterator.next() + } catch { + logError(error) + + if let status = error as? GRPCStatus { + throw status.code == GRPCStatus.Code.invalidArgument ? BaseMindError.invalidArgument : BaseMindError.serverError + } + throw BaseMindError.serverError + } + } } From fea1af3377519b0deb607d30edc84522721cc74d Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Sun, 10 Dec 2023 17:56:08 +0100 Subject: [PATCH 05/15] chore: created async test setup --- .pre-commit-config.yaml | 5 - .swiftlint.yaml | 3 - Package.swift | 2 +- Sources/BaseMindClient/BaseMindClient.swift | 76 ++++---- Sources/BaseMindGateway/gateway.grpc.swift | 149 +++++++++++++++ Taskfile.yaml | 2 +- .../BaseMindClientTests.swift | 176 +++++++++++++++++- 7 files changed, 363 insertions(+), 50 deletions(-) delete mode 100644 .swiftlint.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2eee6c2..7141e43 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,8 +25,3 @@ repos: rev: '0.52.11' hooks: - id: swiftformat - - repo: https://github.com/realm/SwiftLint - rev: '0.54.0' - hooks: - - id: swiftlint - entry: swiftlint --config .swiftlint.yaml --fix --strict diff --git a/.swiftlint.yaml b/.swiftlint.yaml deleted file mode 100644 index f41c584..0000000 --- a/.swiftlint.yaml +++ /dev/null @@ -1,3 +0,0 @@ -disabled_rules: - - comma - - trailing_comma diff --git a/Package.swift b/Package.swift index 548031b..b79bb68 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( platforms: [ .iOS(.v13), .macCatalyst(.v13), - .macOS(.v10_15), + .macOS(.v12), .watchOS(.v6), .tvOS(.v13), ], diff --git a/Sources/BaseMindClient/BaseMindClient.swift b/Sources/BaseMindClient/BaseMindClient.swift index 43dfde5..86e1e00 100644 --- a/Sources/BaseMindClient/BaseMindClient.swift +++ b/Sources/BaseMindClient/BaseMindClient.swift @@ -1,7 +1,8 @@ import BaseMindGateway import Foundation import GRPC -import Logging +import NIOCore +import OSLog enum BaseMindError: Error { /// generic error thrown whenever there is an error communicating with the server @@ -10,28 +11,30 @@ enum BaseMindError: Error { case invalidArgument /// thrown when the token passed to the SDK is empty case missingToken - /// thrown when the connection to the server fails - case connectionFailure } -let DEFAULT_API_GATEWAY_ADDRESS = "gateway.basemind.ai" -let DEFAULT_API_GATEWAY_PORT = 443 +public let DEFAULT_API_GATEWAY_ADDRESS = "gateway.basemind.ai" +public let DEFAULT_API_GATEWAY_PORT = 443 +public let DEFAULT_LOGGER = Logger(subsystem: "BaseMindClient", category: "client logs") + +public struct ClientOptions { + public init( + ) {} -public struct Options { /// The gRPC server address. - var host: String = DEFAULT_API_GATEWAY_ADDRESS + public var host: String = DEFAULT_API_GATEWAY_ADDRESS /// The gRPC server port. - var port: Int = DEFAULT_API_GATEWAY_PORT + public var port: Int = DEFAULT_API_GATEWAY_PORT /// A flag dictating whether debug messages will be logged. - var debug: Bool = false + public var debug: Bool = false /// The ID of the prompt configuration to use. /// /// Note: This value is optional. If not provided, the default prompt configuration will be used. - var promptConfigId: String? + public var promptConfigId: String? /// The logger instance to use. /// /// Note: messages are logged only when 'debug' is set to true. - var logger: Logger = .init(label: "BaseMindClient") + public var logger: Logger = DEFAULT_LOGGER } /// The BaseMindClient @@ -40,15 +43,15 @@ public struct Options { /// You can also pass an options object. public class BaseMindClient { private let client: Gateway_V1_APIGatewayServiceAsyncClient - private let apiToken: String - private let options: Options + private let apiKey: String + private let options: ClientOptions - init(apiToken: String, options: Options = Options()) throws { - if apiToken == "" { + public init(apiKey: String, options: ClientOptions = ClientOptions()) throws { + if apiKey == "" { throw BaseMindError.missingToken } - self.apiToken = apiToken + self.apiKey = apiKey self.options = options if self.options.debug { @@ -56,23 +59,12 @@ public class BaseMindClient { } let eventLoopGroup = PlatformSupport.makeEventLoopGroup(loopCount: 1) - do { - let channel = try GRPCChannelPool.with( - target: .host(options.host, port: options.port), - transportSecurity: .plaintext, - eventLoopGroup: eventLoopGroup - ) - client = Gateway_V1_APIGatewayServiceAsyncClient(channel: channel) - - if self.options.debug { - self.options.logger.debug("Successfully connected to BaseMind.AI server on \(options.host):\(options.port).") - } - } catch { - if self.options.debug { - self.options.logger.debug("Failed to connect to the BaseMind.AI server on \(options.host):\(options.port). Error: \(error).") - } + let builder = ClientConnection.insecure(group: eventLoopGroup) // ClientConnection.usingPlatformAppropriateTLS(for: eventLoopGroup) + let connection = builder.connect(host: options.host, port: options.port) + client = Gateway_V1_APIGatewayServiceAsyncClient(channel: connection) - throw BaseMindError.connectionFailure + if self.options.debug { + self.options.logger.debug("connected to BaseMind.AI server on \(options.host):\(options.port).") } } @@ -81,7 +73,7 @@ public class BaseMindClient { options.customMetadata.add( name: "authorization", - value: "Bearer \(apiToken)" + value: "Bearer \(apiKey)" ) return options @@ -101,8 +93,17 @@ public class BaseMindClient { return request } + /// Close the client, and any connections associated with it. Any ongoing RPCs may fail. + /// + /// - Returns: Returns a future which will be resolved when shutdown has completed. + public func close() -> EventLoopFuture { + client.channel.close() + } + /// Request an LLM Prompt. + /// /// - Parameter templateVariables: A dictionary of key/value strings. This dictionary should supply the values required for any template variables defined in the BaseMind dashboard. + /// /// - Returns: A prompt response object. public func requestPrompt(_ templateVariables: [String: String]? = nil) async throws -> Gateway_V1_PromptResponse { do { @@ -124,6 +125,12 @@ public class BaseMindClient { } } + /// Request an LLM Streaming Prompt + /// + /// - Parameter templateVariables: A dictionary of key/value strings. This dictionary should supply the values required for any template variables defined in the BaseMind dashboard. + /// + /// - Returns: A stream conforming to the AsyncIterator protocol. Iterate the stream to access each chunk of the result in the order it is + /// being transmitted by the server. public func requestStream(_ templateVariables: [String: String]? = nil) async throws -> ThrowingRequestStream { if options.debug { options.logger.debug("requesting streaming prompt") @@ -143,6 +150,9 @@ public class BaseMindClient { } } +/// An AsyncIterator of StreamingPromptResponse elements. +/// +/// Throws BaseMindError. public struct ThrowingRequestStream: AsyncIteratorProtocol { private var iterator: GRPCAsyncResponseStream.AsyncIterator private let logError: (_ error: Error) -> Void diff --git a/Sources/BaseMindGateway/gateway.grpc.swift b/Sources/BaseMindGateway/gateway.grpc.swift index 14595ed..a5cefd8 100644 --- a/Sources/BaseMindGateway/gateway.grpc.swift +++ b/Sources/BaseMindGateway/gateway.grpc.swift @@ -260,3 +260,152 @@ public enum Gateway_V1_APIGatewayServiceClientMetadata { ) } } + +/// The API Gateway service definition. +/// +/// To build a server, implement a class that conforms to this protocol. +public protocol Gateway_V1_APIGatewayServiceProvider: CallHandlerProvider { + var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { get } + + /// Request a regular LLM prompt + func requestPrompt(request: Gateway_V1_PromptRequest, context: StatusOnlyCallContext) -> EventLoopFuture + + /// Request a streaming LLM prompt + func requestStreamingPrompt(request: Gateway_V1_PromptRequest, context: StreamingResponseCallContext) -> EventLoopFuture +} + +public extension Gateway_V1_APIGatewayServiceProvider { + var serviceName: Substring { + Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor.fullName[...] + } + + /// Determines, calls and returns the appropriate request handler, depending on the request's method. + /// Returns nil for methods not handled by this service. + func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "RequestPrompt": + UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: interceptors?.makeRequestPromptInterceptors() ?? [], + userFunction: requestPrompt(request:context:) + ) + + case "RequestStreamingPrompt": + ServerStreamingServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: interceptors?.makeRequestStreamingPromptInterceptors() ?? [], + userFunction: requestStreamingPrompt(request:context:) + ) + + default: + nil + } + } +} + +/// The API Gateway service definition. +/// +/// To implement a server, implement an object which conforms to this protocol. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public protocol Gateway_V1_APIGatewayServiceAsyncProvider: CallHandlerProvider, Sendable { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { get } + + /// Request a regular LLM prompt + func requestPrompt( + request: Gateway_V1_PromptRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Gateway_V1_PromptResponse + + /// Request a streaming LLM prompt + func requestStreamingPrompt( + request: Gateway_V1_PromptRequest, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public extension Gateway_V1_APIGatewayServiceAsyncProvider { + static var serviceDescriptor: GRPCServiceDescriptor { + Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor + } + + var serviceName: Substring { + Gateway_V1_APIGatewayServiceServerMetadata.serviceDescriptor.fullName[...] + } + + var interceptors: Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol? { + nil + } + + func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "RequestPrompt": + GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: interceptors?.makeRequestPromptInterceptors() ?? [], + wrapping: { try await self.requestPrompt(request: $0, context: $1) } + ) + + case "RequestStreamingPrompt": + GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: interceptors?.makeRequestStreamingPromptInterceptors() ?? [], + wrapping: { try await self.requestStreamingPrompt(request: $0, responseStream: $1, context: $2) } + ) + + default: + nil + } + } +} + +public protocol Gateway_V1_APIGatewayServiceServerInterceptorFactoryProtocol: Sendable { + /// - Returns: Interceptors to use when handling 'requestPrompt'. + /// Defaults to calling `self.makeInterceptors()`. + func makeRequestPromptInterceptors() -> [ServerInterceptor] + + /// - Returns: Interceptors to use when handling 'requestStreamingPrompt'. + /// Defaults to calling `self.makeInterceptors()`. + func makeRequestStreamingPromptInterceptors() -> [ServerInterceptor] +} + +public enum Gateway_V1_APIGatewayServiceServerMetadata { + public static let serviceDescriptor = GRPCServiceDescriptor( + name: "APIGatewayService", + fullName: "gateway.v1.APIGatewayService", + methods: [ + Gateway_V1_APIGatewayServiceServerMetadata.Methods.requestPrompt, + Gateway_V1_APIGatewayServiceServerMetadata.Methods.requestStreamingPrompt, + ] + ) + + public enum Methods { + public static let requestPrompt = GRPCMethodDescriptor( + name: "RequestPrompt", + path: "/gateway.v1.APIGatewayService/RequestPrompt", + type: GRPCCallType.unary + ) + + public static let requestStreamingPrompt = GRPCMethodDescriptor( + name: "RequestStreamingPrompt", + path: "/gateway.v1.APIGatewayService/RequestStreamingPrompt", + type: GRPCCallType.serverStreaming + ) + } +} diff --git a/Taskfile.yaml b/Taskfile.yaml index 0a16ee6..e9e2813 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -24,7 +24,7 @@ tasks: - rm -rf tmp - mkdir -p tmp - protoc --swift_opt=Visibility=public --swift_out=tmp proto/gateway/v1/gateway.proto - - protoc --grpc-swift_opt=Client=true,Server=false,Visibility=Public --grpc-swift_out=tmp proto/gateway/v1/gateway.proto + - protoc --grpc-swift_opt=Visibility=Public --grpc-swift_out=tmp proto/gateway/v1/gateway.proto - rm -rf Sources/BaseMindGateway - mv tmp/proto/gateway/v1 Sources/BaseMindGateway - rm -rf tmp diff --git a/Tests/BaseMindClientTests/BaseMindClientTests.swift b/Tests/BaseMindClientTests/BaseMindClientTests.swift index 0bc845a..20970fd 100644 --- a/Tests/BaseMindClientTests/BaseMindClientTests.swift +++ b/Tests/BaseMindClientTests/BaseMindClientTests.swift @@ -1,12 +1,174 @@ -@testable import apiClient +import BaseMindClient +import BaseMindGateway +import struct Foundation.Date +import GRPC +import NIOConcurrencyHelpers +import NIOCore +import NIOPosix +import NIOSSL +import SwiftProtobuf import XCTest -final class apiClientTests: XCTestCase { - func testExample() throws { - // XCTest Documentation - // https://developer.apple.com/documentation/xctest +private let serverCert = """ +-----BEGIN CERTIFICATE----- +MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe +Fw0yMzA3MDUxMDI4NDRaFw0yNDA3MDQxMDI4NDRaMBQxEjAQBgNVBAMMCWxvY2Fs +aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTZdzuWLHyxPM/F +sviBBXpSzl2MxJxDkmir8DSdXO5E1sHCAymTaxy9bOdi1XUZbRTyKfTv3x6sdRdT +0Gs2WjhL0yFT9IEVrGZADt3GIYoHYZU56Yn/nLglGQZqIeo33wyPEIAkbWL6X4RG +1Hc6nJQxhw1aaVtsYNAoWjAVzP773TZgyRcsGliqHtYpD0q0b+EfmPkb0GM1yvBa +j88dWWFFlG00aZFQatSkIrPbkXG0Mu4/1UxYDEuxOYrIFkFMfR8V8h6ZQ2x3H6cS +cTJ2TpIlw3rO6E0J/HYaVhmvJpevIPQhvH/Q+vM1bkvaIkckLchW7VgU4P+ZzHEw +r/xMcqMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAGpBsuzx72mOBa9o7m1eNh2cY +H6MrNi1b6vTaA3SOH68RDxg2qx6UrKxI34/No7FaOzRrfs9vUaKXHwwBnDxMskH5 +iTmVAGegumDQE3Bd11j+v1tKxXWS/bvWH7tfK6taoex76ktR3L8qO+Hp8n4YKuSb +qJScIhMPIg7fWPonLvcszGFPdBIxU3YkAZJZFeom/s1WhWCYXsJZSYOXv4YRlaU5 +ozeV3v9icDptaxNY7n4U6C32eykMjowJJ9dcOD+ib3PF88S+utmZnSEGYu+5bnXy +6MGWZcYH1wQ0RpNC+YzjQcGsKwHfaoBS4WFEK2fJdRfX4owZOu6HO1zhyoLpqw== +-----END CERTIFICATE----- +""" - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods +private let serverKey = """ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEApNl3O5YsfLE8z8Wy+IEFelLOXYzEnEOSaKvwNJ1c7kTWwcID +KZNrHL1s52LVdRltFPIp9O/fHqx1F1PQazZaOEvTIVP0gRWsZkAO3cYhigdhlTnp +if+cuCUZBmoh6jffDI8QgCRtYvpfhEbUdzqclDGHDVppW2xg0ChaMBXM/vvdNmDJ +FywaWKoe1ikPSrRv4R+Y+RvQYzXK8FqPzx1ZYUWUbTRpkVBq1KQis9uRcbQy7j/V +TFgMS7E5isgWQUx9HxXyHplDbHcfpxJxMnZOkiXDes7oTQn8dhpWGa8ml68g9CG8 +f9D68zVuS9oiRyQtyFbtWBTg/5nMcTCv/ExyowIDAQABAoIBAAq5FpdqqlQmF0WQ +n5aoldmiH0hYisV7Y7+pR4O0pMHe+nU6EIiYzUPeUoIunKH0WHMfWXlUTRgqsacl +zY3byDyXOhGV63amGUPBcPYeGDppRoC1dqqCVQhpaVpQdwpMPhcMC0+6jt78WFA7 +Z0CmMF83ZYiJ1AadYyLHLS6pjF8dmkj/Rd6yeLIVkKr4xHxou7au/6WKorop5XLM +fEyWC2iotha2dkXw3i324n0qrbR2v/EYLnAn75uA9FF/pJWe6iPc6H5tfBSnzmO6 +fkZ2rCrDt4ANabg6WMmRdrZXFHSR/JlPPyh4T4iJGenkLltKZG+wWSm2nVXE0DYt +JQdmhiECgYEAz3EclGIrk63Hp/2mAHAOIOUGh6+Tk4JA+ibHSzziVZZqJsGQ9jcK +eOn6TX5674+aNzo2ROnHCT6u2tCQEl5/lrB8YpYh3F6aaNqPvFwqRhDViOw2l8KL +Ic40x19o5ur3Ss914htwTxiEzQVB/n+5zhE7W4N/RaDIT2hedWR19PECgYEAy3AF +CiHa6P+pbhskoSETtxbWkhDENpXat2dlFRDrNN9T2NZNVmIxCjAE52arduCxaLTP +hazyq4d7FZ4OkxfbJY9D2HnBS6mF0RHB0gZXZ7iB/uEr0KcTex5saqX9TF3YA5Wj +PNVtOM37IIaLJ1qOmfXf4yL3EVlI30eNwfoMkNMCgYAv0VAYOET5Rs7GP6b7ZNks +5f5KWsO29giKYVQBWOiHeCPCCU6kIu3sD2teX7Bw9nZDEs0dt5Hk5Kkj0X3UbioV +D1us0hS+GqSXVQJbFhe8jPbcGC9BblvqEAGEj867pCAbA5WV6GNMKEe8huC+jKzE +/p3jK320DCsAevuDLgQu0QKBgHvE60v+zPB0muAiI2bkeNorSuAS001iXm62uQjY +AkFondqOhv7HPo60KEegbzEkAstxNdBeKEWzZ27/el6DZRC02NIbQT6HJKLN6t2c +fhDccDphRAbtnyyIle1Mj46miYWkxGt+bbThnKdtM7v9nESPEmdeHnKvn2Y4YkZh +msOBAoGAaarkv8JjjmIgjRZrJ7r4dkzZwZa/msm+/NHr3nlXK227ExMeFRPmzYls +zIofM+DoEk1sDXRfnv+8EU8Dn1DYSq6M6W8xrm7Ulpzj0kXE4f9TD+MUwSNCQ6Gg +zLRkHQBKblIa0lEvlulLtJT2UN9AnCmvTH2R11wD87DWjFDZKD8= +-----END RSA PRIVATE KEY----- +""" + +private let caCert = """ +-----BEGIN CERTIFICATE----- +MIICoDCCAYgCCQCgCA1/0dKfFjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz +b21lLWNhMB4XDTIzMDcwNTEwMjg0NFoXDTI0MDcwNDEwMjg0NFowEjEQMA4GA1UE +AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALTi2aJy +Vw3E0OQwNIm9GZOG4E/Rc0atKoJes9yWaMrMPGwoenLEc2JNIvJSdBGZHKO7HKAG +OnffpqVIXtRBIU7l8HEhX97Q+knI6wPz8O7JaGVf6KznLa2eFO6xGM1pogO7m+/M +0mw8LSftn2IEiJk9v00qj+WgfwJJqL/TUZRoT5M2+u99uiaW7bnI1+1vawo5i7A1 +zfN6SBud5K/BaEYcAjxX1JMWCJLWSuOFZArWX7Je2MP+LqZkjh8kQO+d8ZZaLSIs +ujd6x6/r365Sl24l4auNfWy/5V1Ctfxl4avupAm7CpmEFpswe/ucNHkD0drUCzvt +hBeR3coLXWgbQs0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJm1Yntrrl6WxPbsA +s1DrI9YHdQjUNkouX0PtGp4yKrP7hwTclIhHjlGaQRJ2p1I7hllCMCPDa2YZa714 +XhtvEmpWOeLXMFolpKEn83kccvkQviZ3yd2lKH64jDX1/g2Rf6dXhDZMKrMAkEdx +X3JwZwPxwb8VDtac7TkVgOcQFHRzdX2g6pQXz3eNsjckGNJgzzl/ln6DrHHDbruI +M7bfnc2ZCBcHUCLWts8LnX2ekUq9KOxMe4e3sD27sKPizklNfGH4Rdg4LByhkx3S +GGR3ziWyixfcs4BNhA5mbsvb8vpPdtOh1oFt+TtPxlQ2FQOnSHk6wF285XggYYgv +p8pG5Q== +-----END CERTIFICATE----- +""" + +let certificate = try! NIOSSLCertificate(bytes: .init(serverCert.utf8), format: .pem) +let key = try! NIOSSLPrivateKey( + bytes: .init(serverKey.utf8), + format: .pem +) +let caCertificate = try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .pem) + +final class MockGatewayServer: Gateway_V1_APIGatewayServiceAsyncProvider { + public var exc: Error? + public var authHeader: String? + public var templateVariables: [String: String]? + public var promptConfigId: String? + public var request: Gateway_V1_PromptRequest? + + init() {} + + func requestPrompt(request: BaseMindGateway.Gateway_V1_PromptRequest, context _: GRPC.GRPCAsyncServerCallContext) async throws -> BaseMindGateway.Gateway_V1_PromptResponse { + self.request = request + + if let exc { + throw exc + } + + var response = BaseMindGateway.Gateway_V1_PromptResponse() + response.content = "abc" + + return response + } + + func requestStreamingPrompt(request: BaseMindGateway.Gateway_V1_PromptRequest, responseStream _: GRPC.GRPCAsyncResponseStreamWriter, context _: GRPC.GRPCAsyncServerCallContext) async throws { + self.request = request + + if let exc { + throw exc + } + } +} + +/// Client test suite +/// +/// See: https://github.com/grpc/grpc-swift/blob/main/Tests/GRPCTests for examples +final class BaseMindClientTests: XCTestCase { + let token = "abcJeronimo" + + var server: Server! + var provider: MockGatewayServer! + var group: EventLoopGroup! + + override func setUp() { + super.setUp() + + group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + provider = MockGatewayServer() + } + + override func tearDown() async throws { + try await server.close().get() + server = nil + + try await group.shutdownGracefully() + group = nil + + provider = nil + + try await super.tearDown() + } + + private func startServer() throws { + server = try Server.insecure(group: group) + .withServiceProviders([provider]) + .bind(host: "127.0.0.1", port: 0) + .wait() + } + + func makeClient() throws -> BaseMindClient { + var options = ClientOptions() + options.host = "127.0.0.1" + options.port = server.channel.localAddress!.port! + + return try BaseMindClient(apiKey: token, options: options) + } + + func testThrowsWhenTokenIsEmpty() throws { + XCTAssertThrowsError(try BaseMindClient(apiKey: "")) + } + + func testReturnsReponse() async throws { + try startServer() + + let client = try makeClient() + let response = try await client.requestPrompt() + XCTAssertEqual(response.content, "abc") } } From 43653a4138e70751bf65ccdfdad4c5c0689092cf Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Mon, 11 Dec 2023 10:11:53 +0100 Subject: [PATCH 06/15] chore: add tests --- Sources/BaseMindClient/BaseMindClient.swift | 59 ++++- .../BaseMindClientTests.swift | 234 +++++++++++++++++- 2 files changed, 270 insertions(+), 23 deletions(-) diff --git a/Sources/BaseMindClient/BaseMindClient.swift b/Sources/BaseMindClient/BaseMindClient.swift index 86e1e00..9844e80 100644 --- a/Sources/BaseMindClient/BaseMindClient.swift +++ b/Sources/BaseMindClient/BaseMindClient.swift @@ -2,9 +2,10 @@ import BaseMindGateway import Foundation import GRPC import NIOCore +import NIOSSL import OSLog -enum BaseMindError: Error { +public enum BaseMindError: Error { /// generic error thrown whenever there is an error communicating with the server case serverError /// thrown when the server responds to a request with the gRPC 'invalid argument' status @@ -19,7 +20,28 @@ public let DEFAULT_LOGGER = Logger(subsystem: "BaseMindClient", category: "clien public struct ClientOptions { public init( - ) {} + host: String? = nil, + port: Int? = nil, + debug: Bool? = nil, + promptConfigId: String? = nil, + logger: Logger? = nil + ) { + if let host { + self.host = host + } + if let port { + self.port = port + } + if let debug { + self.debug = debug + } + if let promptConfigId { + self.promptConfigId = promptConfigId + } + if let logger { + self.logger = logger + } + } /// The gRPC server address. public var host: String = DEFAULT_API_GATEWAY_ADDRESS @@ -59,8 +81,12 @@ public class BaseMindClient { } let eventLoopGroup = PlatformSupport.makeEventLoopGroup(loopCount: 1) - let builder = ClientConnection.insecure(group: eventLoopGroup) // ClientConnection.usingPlatformAppropriateTLS(for: eventLoopGroup) - let connection = builder.connect(host: options.host, port: options.port) + let connection = ClientConnection + .usingTLSBackedByNIOSSL(on: eventLoopGroup) + .withTLS(trustRoots: .certificates([])) + .withTLS(certificateVerification: .none) + .connect(host: options.host, port: options.port) + client = Gateway_V1_APIGatewayServiceAsyncClient(channel: connection) if self.options.debug { @@ -139,25 +165,28 @@ public class BaseMindClient { let request = createRequest(templateVariables) let stream = client.requestStreamingPrompt(request, callOptions: createCallOptions()) - return ThrowingRequestStream(stream: stream, - logError: { - (error: Error) in - if self.options.debug { - self.options.logger.debug("erroring streaming prompt \(error)") - } + let logError = { + (error: Error) in + if self.options.debug { + self.options.logger.debug("erroring streaming prompt \(error)") + } + } - }) + return ThrowingRequestStream(stream: stream, logError: logError) } } /// An AsyncIterator of StreamingPromptResponse elements. /// /// Throws BaseMindError. -public struct ThrowingRequestStream: AsyncIteratorProtocol { +public struct ThrowingRequestStream: AsyncSequence, AsyncIteratorProtocol { + public typealias AsyncIterator = ThrowingRequestStream + public typealias Element = Value + private var iterator: GRPCAsyncResponseStream.AsyncIterator private let logError: (_ error: Error) -> Void - init(stream: GRPCAsyncResponseStream, logError: @escaping (_ error: Error) -> Void) { + init(stream: GRPCAsyncResponseStream, logError: @escaping (_ error: Error) -> Void) { iterator = stream.makeAsyncIterator() self.logError = logError } @@ -175,4 +204,8 @@ public struct ThrowingRequestStream: AsyncIteratorProtocol { throw BaseMindError.serverError } } + + public func makeAsyncIterator() -> AsyncIterator { + self + } } diff --git a/Tests/BaseMindClientTests/BaseMindClientTests.swift b/Tests/BaseMindClientTests/BaseMindClientTests.swift index 20970fd..c801f0c 100644 --- a/Tests/BaseMindClientTests/BaseMindClientTests.swift +++ b/Tests/BaseMindClientTests/BaseMindClientTests.swift @@ -6,6 +6,7 @@ import NIOConcurrencyHelpers import NIOCore import NIOPosix import NIOSSL +import OSLog import SwiftProtobuf import XCTest @@ -87,9 +88,6 @@ let caCertificate = try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .p final class MockGatewayServer: Gateway_V1_APIGatewayServiceAsyncProvider { public var exc: Error? - public var authHeader: String? - public var templateVariables: [String: String]? - public var promptConfigId: String? public var request: Gateway_V1_PromptRequest? init() {} @@ -107,15 +105,30 @@ final class MockGatewayServer: Gateway_V1_APIGatewayServiceAsyncProvider { return response } - func requestStreamingPrompt(request: BaseMindGateway.Gateway_V1_PromptRequest, responseStream _: GRPC.GRPCAsyncResponseStreamWriter, context _: GRPC.GRPCAsyncServerCallContext) async throws { + func requestStreamingPrompt(request: BaseMindGateway.Gateway_V1_PromptRequest, responseStream: GRPC.GRPCAsyncResponseStreamWriter, context _: GRPC.GRPCAsyncServerCallContext) async throws { self.request = request if let exc { throw exc } + + for el in ["1", "2", "3"] { + var response = Gateway_V1_StreamingPromptResponse() + response.content = el + + if el == "3" { + response.finishReason = "done" + } + + try await responseStream.send(response) + } } } +enum TestingError: Error { + case unknown +} + /// Client test suite /// /// See: https://github.com/grpc/grpc-swift/blob/main/Tests/GRPCTests for examples @@ -129,12 +142,12 @@ final class BaseMindClientTests: XCTestCase { override func setUp() { super.setUp() - group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + group = PlatformSupport.makeEventLoopGroup(loopCount: 1) provider = MockGatewayServer() } override func tearDown() async throws { - try await server.close().get() + try closeServer() server = nil try await group.shutdownGracefully() @@ -145,8 +158,19 @@ final class BaseMindClientTests: XCTestCase { try await super.tearDown() } + private func closeServer() throws { + if let srv = server { + try? srv.close().wait() + } + } + private func startServer() throws { - server = try Server.insecure(group: group) + server = try Server + .usingTLSBackedByNIOSSL( + on: group, + certificateChain: [certificate], + privateKey: key + ) .withServiceProviders([provider]) .bind(host: "127.0.0.1", port: 0) .wait() @@ -154,21 +178,211 @@ final class BaseMindClientTests: XCTestCase { func makeClient() throws -> BaseMindClient { var options = ClientOptions() + options.debug = true options.host = "127.0.0.1" options.port = server.channel.localAddress!.port! + options.promptConfigId = "123abc" return try BaseMindClient(apiKey: token, options: options) } + // MARK: initializer tests + func testThrowsWhenTokenIsEmpty() throws { - XCTAssertThrowsError(try BaseMindClient(apiKey: "")) + do { + try BaseMindClient(apiKey: "") + XCTFail("should throw error") + } catch { + if let err = error as? BaseMindError { + XCTAssertEqual(err, BaseMindError.missingToken) + } else { + XCTFail("failed to match error") + } + } + } + + func testDoesNotThrowForNonEmptyKey() { + let apiKey = "apiKey" + + do { + let client = try BaseMindClient(apiKey: apiKey) + + XCTAssertNotNil(client) + } catch { + XCTFail("failed to initialize client with valid API key: \(error)") + } } - func testReturnsReponse() async throws { + func testClientOptions() { + let apiKey = "apiKey" + let options = ClientOptions(host: "custom_host", port: 1234, debug: true, promptConfigId: "abc123", logger: Logger(subsystem: "test", category: "test")) + + do { + let client = try BaseMindClient(apiKey: apiKey, options: options) + + XCTAssertNotNil(client) + } catch { + XCTFail("Failed to initialize client with valid API key and custom options: \(error)") + } + } + + // MARK: request prompt tests + + func testRequestPromptSuccessScenario() async throws { try startServer() let client = try makeClient() - let response = try await client.requestPrompt() + let response = try await client.requestPrompt(["key": "value"]) + XCTAssertEqual(response.content, "abc") + XCTAssertEqual(provider.request?.templateVariables, ["key": "value"]) + XCTAssertEqual(provider.request?.promptConfigID, "123abc") + } + + func testRequestPromptInvalidArgumentErrorScenario() async throws { + try startServer() + + provider.exc = GRPCStatus(code: GRPCStatus.Code.invalidArgument, message: "invalid key") + + let client = try makeClient() + + do { + _ = try await client.requestPrompt() + XCTFail("should throw error") + } catch { + if let err = error as? BaseMindError { + XCTAssertEqual(err, BaseMindError.invalidArgument) + } else { + XCTFail("failed to match error") + } + } + } + + func testRequestPromptServerErrorScenario() async throws { + try startServer() + + provider.exc = GRPCStatus(code: GRPCStatus.Code.internalError, message: "oops") + + let client = try makeClient() + + do { + _ = try await client.requestPrompt() + XCTFail("should throw error") + } catch { + if let err = error as? BaseMindError { + XCTAssertEqual(err, BaseMindError.serverError) + } else { + XCTFail("failed to match error") + } + } + } + + func testRequestUnknownErrorScenario() async throws { + try startServer() + + provider.exc = TestingError.unknown + + let client = try makeClient() + + do { + _ = try await client.requestPrompt() + XCTFail("should throw error") + } catch { + if let err = error as? BaseMindError { + XCTAssertEqual(err, BaseMindError.serverError) + } else { + XCTFail("failed to match error") + } + } + } + + // MARK: request stream tests + + func testRequestStreamSuccessScenario() async throws { + try startServer() + + let client = try makeClient() + let stream = try await client.requestStream(["key": "value"]) + var responses: [String] = [] + var finishReason = "" + for try await response in stream { + responses.append(response.content) + if response.finishReason != "" { + finishReason = response.finishReason + } + } + XCTAssertEqual(responses, ["1", "2", "3"]) + XCTAssertEqual(finishReason, "done") + XCTAssertEqual(provider.request?.templateVariables, ["key": "value"]) + XCTAssertEqual(provider.request?.promptConfigID, "123abc") + } + + func testRequestStreamInvalidArgumentErrorScenario() async throws { + try startServer() + + let client = try makeClient() + let stream = try await client.requestStream() + + provider.exc = GRPCStatus(code: GRPCStatus.Code.invalidArgument, message: "invalid key") + + do { + do { + for try await _ in stream { + XCTFail("should throw error") + } + } catch { + if let err = error as? BaseMindError { + XCTAssertEqual(err, BaseMindError.invalidArgument) + } else { + XCTFail("failed to match error") + } + } + } + } + + func testRequestStreamServerErrorScenario() async throws { + try startServer() + + let client = try makeClient() + let stream = try await client.requestStream() + + provider.exc = GRPCStatus(code: GRPCStatus.Code.internalError, message: "oops") + + do { + do { + for try await _ in stream { + XCTFail("should throw error") + } + } catch { + if let err = error as? BaseMindError { + XCTAssertEqual(err, BaseMindError.serverError) + } else { + XCTFail("failed to match error") + } + } + } + } + + func testRequestStreamUnknownErrorScenario() async throws { + try startServer() + + let client = try makeClient() + let stream = try await client.requestStream() + + provider.exc = TestingError.unknown + + do { + do { + for try await _ in stream { + XCTFail("should throw error") + } + } catch { + if let err = error as? BaseMindError { + XCTAssertEqual(err, BaseMindError.serverError) + } else { + XCTFail("failed to match error") + } + } + } } } From 35fa00f3845867dbb0c04ba7580b86dcd2cf049d Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Mon, 11 Dec 2023 12:28:01 +0100 Subject: [PATCH 07/15] chore: updated workflows --- .github/workflows/codeql.yaml | 36 +++++++++++++++++++++++++++++++++ .github/workflows/test.yaml | 34 +++++++++++++++++++++++++++++++ .github/workflows/validate.yaml | 4 +++- .pre-commit-config.yaml | 2 +- Package.resolved | 4 ++-- 5 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/codeql.yaml create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 0000000..516e46c --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,36 @@ +name: 'CodeQL' + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: '16 20 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Swift + uses: swift-actions/setup-swift@v1 + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: swift + - name: Build + run: swift build + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: '/language:swift' diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..399cd41 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,34 @@ +name: 'Test' +on: + push: + branches: + - main + pull_request: + branches: + - main +env: + DEEPSOURCE_DSN: ${{secrets.DEEPSOURCE_DSN}} +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + ref: ${{ github.event.pull_request.head.sha }} + - name: Setup Swift + uses: swift-actions/setup-swift@v1 + - name: Download the DeepSource CLI + run: curl https://deepsource.io/cli | sh + - name: Run tests + run: swift test --enable-code-coverage + - name: Create Coverage Report + uses: maxep/spm-lcov-action@0.3.0 + with: + output-file: ./coverage/lcov.info + - name: Upload Coverage Report + run: ./bin/deepsource report --analyzer test-coverage --key swift --value-file ./coverage/lcov.info diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 8e6d3df..5441314 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup Swift uses: swift-actions/setup-swift@v1 - name: Setup Node @@ -21,7 +23,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - name: Install Task uses: arduino/setup-task@v1 - name: Install Pre-Commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7141e43..f77f92a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: check-yaml - id: check-added-large-files - repo: https://github.com/pre-commit/mirrors-prettier - rev: 'v4.0.0-alpha.3-1' + rev: 'v4.0.0-alpha.4' hooks: - id: prettier exclude: 'package' diff --git a/Package.resolved b/Package.resolved index 1cfe203..6560e67 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/grpc/grpc-swift.git", "state" : { - "revision" : "663a85221ecf93e4b8fb1f0fdd34d4b27ae78665", - "version" : "1.20.0" + "revision" : "6ade19f0b57f5fc436dfecfced83f3c84d1095b9", + "version" : "1.21.0" } }, { From ddce0cd4a11c0865f9a42d567999090e83abaf61 Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Mon, 11 Dec 2023 12:38:40 +0100 Subject: [PATCH 08/15] chore: updated the readme --- README.md | 84 +++++++++++++++++++- Taskfile.yaml | 1 + package.xcworkspace/contents.xcworkspacedata | 7 -- 3 files changed, 83 insertions(+), 9 deletions(-) delete mode 100644 package.xcworkspace/contents.xcworkspacedata diff --git a/README.md b/README.md index 55f07cd..a4d5e45 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,83 @@ -# sdk-ios +# BaseMind.AI Swift (iOS/MacOS) SDK -The iOS SDK for BaseMind.AI +This repository hosts the BaseMind.AI Swift SDK. The SDK is a gRPC client library for connecting with the BaseMind.AI platform. + +It supports iOS >= 13 and MacOS >= 12. + +## Local Development + +Repository Structure: + +```text +root # repository root, holding all tooling configurations +├─── .github # GitHub CI/CD and other configurations +├─── .swiftpm # Swift Package Manager Workspace +├─── proto/gateway # Git submodule that includes the protobuf schema +├─── Tests # Tests +├─── Sources/BaseMindGateway # Protobuf and gRPC stubs generated from the proto file +└─── Sources/BaseMindClient # The SDK source code +``` + +### Installation + +1. Clone to repository to your local machine including the submodules. + +```shell +git clone --recurse-submodules https://github.com/basemind-ai/sdk-ios.git +``` + +2. Install [TaskFile](https://taskfile.dev/) and the following prerequisites: + + - Python >= 3.11 + - Swift >= 5.9 + - XCode (optional but recommended) + +3. If you're using MacOS - execute the setup task with: + +```shell +task setup +``` + +This will setup [pre-commit](https://pre-commit.com/) and the protobuf dependencies. + +If you're using a linux or windows machine, you will need to setup the components manually. + +### Linting + +To lint the project, execute the lint command: + +```shell +task lint +``` + +### Updating Dependencies + +To update the dependencies, execute the update-dependencies command: + +```shell +task update +``` + +This will update the swift dependencies and the the pre-commit hooks. + +### Executing Tests + +```shell +swift test +``` + +### Generating Protobuf and gRPC Stubs + +```shell +task generate +``` + +This command will generate new protobuf and grpc stubs, replacing those in `Sources/BaseMindGateway`. + +## Contribution + +The SDK is open source. + +Pull requests are welcome - as long as they address an issue or substantially improve the SDK or the test app. + +Note: Tests are mandatory for the SDK - untested code will not be merged. diff --git a/Taskfile.yaml b/Taskfile.yaml index e9e2813..13d8482 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -11,6 +11,7 @@ tasks: - pre-commit install && pre-commit install --hook-type commit-msg && pre-commit install-hooks update: cmds: + - command -v protoc &> /dev/null && brew update && brew upgrade - pre-commit autoupdate - swift package update - swift outdated diff --git a/package.xcworkspace/contents.xcworkspacedata b/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - From b356f9298de38949e6d7b23625c65cd2346badc6 Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Mon, 11 Dec 2023 13:32:44 +0100 Subject: [PATCH 09/15] chore: updated readme --- README.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4d5e45..6d7590e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,120 @@ This repository hosts the BaseMind.AI Swift SDK. The SDK is a gRPC client librar It supports iOS >= 13 and MacOS >= 12. +## Installation + +Add the sdk in your `Package.swift` file dependencies: + +```swift + dependencies: [ + .package(url: "https://github.com/basemind-ai/sdk-ios.git", from: "0.1.0"), + ] +``` + +Then add the dependency to a target: + +```swift + targets: [ + .target( + name: "MyApp", + dependencies: ["BaseMindClient"] + ), + ] +``` + +## Usage + +Before using the client you have to initialize it. The init function requires an `apiKey` that you can create using the BaseMind platform (visit https://basemind.ai): + +```swift +import BaseMindClient + +let client = BaseMindClient(apiKey: "") +``` + +Once the client is initialized, you can use it to interact with the AI model(s) you configured in the BaseMind dashboard. + +### Prompt Request/Response + +You can request an LLM prompt using the `requestPrompt` method, which expects a dictionary of string key/value pairs - correlating with any template variables defined in the dashboard (if any): + +```swift +import BaseMindClient + +let client = BaseMindClient(apiKey: "") + +func handlePromptRequest(userInput: String) async throws -> String { + let templateVariables = ["userInput": userInput] + + let response = try client.requestPrompt(templateVariables) + + return response.content +} +``` + +### Prompt Streaming + +You can also stream a prompt response using the `requestStream` method: + +```swift +import BaseMindClient + +let client = BaseMindClient(apiKey: "") + +func handlePromptStream(userInput: String) async throws -> [String] { + let templateVariables = ["userInput": userInput] + + let stream = try client.requestStream(templateVariables) + + var chunks: [String] = [] + + for try await response in stream { + chunks.append(response.content) + } + + return chunks +} +``` + +Similarly to the `requestPrompt` method, `requestStream` expects a dictionary of strings (if any template variables are defined in the dashboard). + +It returns a data container that is both an AsyncSequence and an AsyncIterator. You can therefore loop and iterate through the results as fits your use case. + +### Error Handling + +All errors thrown by the client are instances of `BaseMindError`. Errors are thrown in the following cases: + +1. The api key is empty (`BaseMindError.missingToken`). +2. A server side or connectivity error occured (`BaseMindError.serverError`) +3. A required template variable was not provided in the dictionary of the request (`BaseMindError.invalidArgument`) + +### Options + +You can pass an options struct to the client: + +```swift +import BaseMindClient +import OSLog + +let options = ClientOptions( + debug: true, + host: "127.0.0.1", + logger: Logger(subsystem: "my-sub-system", category: "client"), + port: 443, + promptConfigId: "c5f5d1fd-d25d-4ba2-b103-8c85f48a679d" +) + +let client = BaseMindClient(apiKey: "", options: options) +``` + +- `debug`: if set to true (default false) the client will log debug messages. +- `host`: the host of the BaseMind Gateway server to use. +- `logger`: an OSLog.Logger instance. If provided it will override the default logger used. +- `port`: the server port. +- `promptConfigId`: the ID of the prompt config to use. If given this value will be used by the server. If not provided, the default prompt configuration will be used. + +**Note**: you can have multiple client instances with different `promptConfigId` values set. This allows you to use multiple model configurations within a single application. + ## Local Development Repository Structure: @@ -18,7 +132,7 @@ root # repository root, holding all tooling configura └─── Sources/BaseMindClient # The SDK source code ``` -### Installation +### Repository Setup 1. Clone to repository to your local machine including the submodules. From 0d1e43be796f9f26a498a4d20637182e355d3b3d Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Mon, 11 Dec 2023 13:50:03 +0100 Subject: [PATCH 10/15] chore: add discord badge --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 6d7590e..3fa36d3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # BaseMind.AI Swift (iOS/MacOS) SDK +
+ +[![Discord](https://img.shields.io/discord/1153195687459160197)](https://discord.gg/ZSV2CQ86yg) + +
+ This repository hosts the BaseMind.AI Swift SDK. The SDK is a gRPC client library for connecting with the BaseMind.AI platform. It supports iOS >= 13 and MacOS >= 12. From 5343d76871793c84aebb3b46988648de6efa296e Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Mon, 11 Dec 2023 13:56:16 +0100 Subject: [PATCH 11/15] chore: exclude generated code from deepsource analysis --- .deepsource.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.deepsource.toml b/.deepsource.toml index 51ffe88..3c3113d 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,6 +1,6 @@ version = 1 -exclude_patterns = ["package/**", "example-app/**"] +exclude_patterns = ["Sources/BaseMindGateway/**"] test_patterns = ["**/Tests/**"] From efe1e57032db8bb1829c85f44f2983e3c363e732 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 12:58:15 +0000 Subject: [PATCH 12/15] refactor: autofix issues in 2 files Resolved issues in the following files with DeepSource Autofix: 1. Sources/BaseMindClient/BaseMindClient.swift 2. Tests/BaseMindClientTests/BaseMindClientTests.swift --- Sources/BaseMindClient/BaseMindClient.swift | 2 +- Tests/BaseMindClientTests/BaseMindClientTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/BaseMindClient/BaseMindClient.swift b/Sources/BaseMindClient/BaseMindClient.swift index 9844e80..dadc9b4 100644 --- a/Sources/BaseMindClient/BaseMindClient.swift +++ b/Sources/BaseMindClient/BaseMindClient.swift @@ -69,7 +69,7 @@ public class BaseMindClient { private let options: ClientOptions public init(apiKey: String, options: ClientOptions = ClientOptions()) throws { - if apiKey == "" { + if apiKey.isEmpty { throw BaseMindError.missingToken } diff --git a/Tests/BaseMindClientTests/BaseMindClientTests.swift b/Tests/BaseMindClientTests/BaseMindClientTests.swift index c801f0c..2dc7946 100644 --- a/Tests/BaseMindClientTests/BaseMindClientTests.swift +++ b/Tests/BaseMindClientTests/BaseMindClientTests.swift @@ -307,7 +307,7 @@ final class BaseMindClientTests: XCTestCase { var finishReason = "" for try await response in stream { responses.append(response.content) - if response.finishReason != "" { + if !response.finishReason.isEmpty { finishReason = response.finishReason } } From 1f4db76551348bfd116c0cc75dd7f7101c898af0 Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Mon, 11 Dec 2023 14:03:08 +0100 Subject: [PATCH 13/15] chore: switch to using macos runners --- .github/workflows/codeql.yaml | 2 +- .github/workflows/test.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 516e46c..53e6947 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -13,7 +13,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: macos-latest timeout-minutes: 360 permissions: actions: read diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 399cd41..9174846 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ env: DEEPSOURCE_DSN: ${{secrets.DEEPSOURCE_DSN}} jobs: test: - runs-on: ubuntu-latest + runs-on: macos-latest permissions: contents: read pull-requests: write From 399d8684535d44ce68c86ec765419f4f2ba5c856 Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Mon, 11 Dec 2023 16:23:16 +0100 Subject: [PATCH 14/15] chore: address coverage issues --- .deepsource.toml | 2 +- .../xcshareddata/xcschemes/BaseMind.xcscheme | 92 +++++++++++++++++++ Package.swift | 2 - Sources/BaseMindClient/BaseMindClient.swift | 14 ++- .../BaseMindClientTests.swift | 85 ++++++++++++----- 5 files changed, 164 insertions(+), 31 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/BaseMind.xcscheme diff --git a/.deepsource.toml b/.deepsource.toml index 3c3113d..f2f3cf5 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -2,7 +2,7 @@ version = 1 exclude_patterns = ["Sources/BaseMindGateway/**"] -test_patterns = ["**/Tests/**"] +test_patterns = ["Tests/**"] [[analyzers]] name = "test-coverage" diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/BaseMind.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/BaseMind.xcscheme new file mode 100644 index 0000000..0d1b0de --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/BaseMind.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index b79bb68..653d085 100644 --- a/Package.swift +++ b/Package.swift @@ -18,8 +18,6 @@ let package = Package( .iOS(.v13), .macCatalyst(.v13), .macOS(.v12), - .watchOS(.v6), - .tvOS(.v13), ], products: [ .library( diff --git a/Sources/BaseMindClient/BaseMindClient.swift b/Sources/BaseMindClient/BaseMindClient.swift index dadc9b4..d786098 100644 --- a/Sources/BaseMindClient/BaseMindClient.swift +++ b/Sources/BaseMindClient/BaseMindClient.swift @@ -12,6 +12,8 @@ public enum BaseMindError: Error { case invalidArgument /// thrown when the token passed to the SDK is empty case missingToken + /// thrown when a stream is cancelled + case cancelled } public let DEFAULT_API_GATEWAY_ADDRESS = "gateway.basemind.ai" @@ -145,7 +147,9 @@ public class BaseMindClient { } if let status = error as? GRPCStatus { - throw status.code == GRPCStatus.Code.invalidArgument ? BaseMindError.invalidArgument : BaseMindError.serverError + if status.code == GRPCStatus.Code.invalidArgument { + throw BaseMindError.invalidArgument + } } throw BaseMindError.serverError } @@ -167,6 +171,7 @@ public class BaseMindClient { let logError = { (error: Error) in + if self.options.debug { self.options.logger.debug("erroring streaming prompt \(error)") } @@ -192,15 +197,18 @@ public struct ThrowingRequestStream: AsyncSequence, AsyncIterat } public mutating func next() async throws -> Element? { - if Task.isCancelled { throw GRPCStatus(code: .cancelled) } + if Task.isCancelled { throw BaseMindError.cancelled } do { return try await iterator.next() } catch { logError(error) if let status = error as? GRPCStatus { - throw status.code == GRPCStatus.Code.invalidArgument ? BaseMindError.invalidArgument : BaseMindError.serverError + if status.code == GRPCStatus.Code.invalidArgument { + throw BaseMindError.invalidArgument + } } + throw BaseMindError.serverError } } diff --git a/Tests/BaseMindClientTests/BaseMindClientTests.swift b/Tests/BaseMindClientTests/BaseMindClientTests.swift index 2dc7946..6a42cb4 100644 --- a/Tests/BaseMindClientTests/BaseMindClientTests.swift +++ b/Tests/BaseMindClientTests/BaseMindClientTests.swift @@ -136,6 +136,7 @@ final class BaseMindClientTests: XCTestCase { let token = "abcJeronimo" var server: Server! + var client: BaseMindClient! var provider: MockGatewayServer! var group: EventLoopGroup! @@ -150,6 +151,9 @@ final class BaseMindClientTests: XCTestCase { try closeServer() server = nil + try closeClient() + client = nil + try await group.shutdownGracefully() group = nil @@ -159,8 +163,14 @@ final class BaseMindClientTests: XCTestCase { } private func closeServer() throws { - if let srv = server { - try? srv.close().wait() + if let server { + try? server.close().wait() + } + } + + private func closeClient() throws { + if let client { + try? client.close().wait() } } @@ -182,15 +192,15 @@ final class BaseMindClientTests: XCTestCase { options.host = "127.0.0.1" options.port = server.channel.localAddress!.port! options.promptConfigId = "123abc" - - return try BaseMindClient(apiKey: token, options: options) + client = try BaseMindClient(apiKey: token, options: options) + return client } // MARK: initializer tests func testThrowsWhenTokenIsEmpty() throws { do { - try BaseMindClient(apiKey: "") + _ = try BaseMindClient(apiKey: "") XCTFail("should throw error") } catch { if let err = error as? BaseMindError { @@ -305,12 +315,14 @@ final class BaseMindClientTests: XCTestCase { let stream = try await client.requestStream(["key": "value"]) var responses: [String] = [] var finishReason = "" + for try await response in stream { responses.append(response.content) if !response.finishReason.isEmpty { finishReason = response.finishReason } } + XCTAssertEqual(responses, ["1", "2", "3"]) XCTAssertEqual(finishReason, "done") XCTAssertEqual(provider.request?.templateVariables, ["key": "value"]) @@ -326,16 +338,14 @@ final class BaseMindClientTests: XCTestCase { provider.exc = GRPCStatus(code: GRPCStatus.Code.invalidArgument, message: "invalid key") do { - do { - for try await _ in stream { - XCTFail("should throw error") - } - } catch { - if let err = error as? BaseMindError { - XCTAssertEqual(err, BaseMindError.invalidArgument) - } else { - XCTFail("failed to match error") - } + for try await _ in stream { + XCTFail("should throw error") + } + } catch { + if let err = error as? BaseMindError { + XCTAssertEqual(err, BaseMindError.invalidArgument) + } else { + XCTFail("failed to match error") } } } @@ -349,16 +359,14 @@ final class BaseMindClientTests: XCTestCase { provider.exc = GRPCStatus(code: GRPCStatus.Code.internalError, message: "oops") do { - do { - for try await _ in stream { - XCTFail("should throw error") - } - } catch { - if let err = error as? BaseMindError { - XCTAssertEqual(err, BaseMindError.serverError) - } else { - XCTFail("failed to match error") - } + for try await _ in stream { + XCTFail("should throw error") + } + } catch { + if let err = error as? BaseMindError { + XCTAssertEqual(err, BaseMindError.serverError) + } else { + XCTFail("failed to match error") } } } @@ -385,4 +393,31 @@ final class BaseMindClientTests: XCTestCase { } } } + + func testRequestStreamCancellationScenario() async throws { + try startServer() + + let client = try makeClient() + let stream = try await client.requestStream() + + let task = Task { () -> [String] in + var responses: [String] = [] + + for try await response in stream { + responses.append(response.content) + } + + return responses + } + + do { + task.cancel() + } catch { + if let err = error as? BaseMindError { + XCTAssertEqual(err, BaseMindError.cancelled) + } else { + XCTFail("failed to match error") + } + } + } } From c24032ee380a141d5a3ae1711b1cebfa5cabb53a Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Mon, 11 Dec 2023 16:29:18 +0100 Subject: [PATCH 15/15] chore: added missing documentation --- Sources/BaseMindClient/BaseMindClient.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/BaseMindClient/BaseMindClient.swift b/Sources/BaseMindClient/BaseMindClient.swift index d786098..1560409 100644 --- a/Sources/BaseMindClient/BaseMindClient.swift +++ b/Sources/BaseMindClient/BaseMindClient.swift @@ -5,6 +5,7 @@ import NIOCore import NIOSSL import OSLog +/// Error types thrown by the BaseMindClient public enum BaseMindError: Error { /// generic error thrown whenever there is an error communicating with the server case serverError @@ -20,6 +21,13 @@ public let DEFAULT_API_GATEWAY_ADDRESS = "gateway.basemind.ai" public let DEFAULT_API_GATEWAY_PORT = 443 public let DEFAULT_LOGGER = Logger(subsystem: "BaseMindClient", category: "client logs") +/// Client Options container. +/// +/// Example usage: +/// +/// let options = ClientOptions(debug: true) +/// let client = BaseMindClient(apiKey: "", options: options) +/// public struct ClientOptions { public init( host: String? = nil,