From 0d77f3f838f0362287cbf0e0d2eb029078f9cfa8 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Wed, 9 Oct 2024 16:22:09 +0900 Subject: [PATCH 01/26] add entities for message batches api --- .../Entity/Batch/BatchRequestCounts.swift | 14 +++++++ .../Entity/Batch/BatchResult.swift | 14 +++++++ .../Entity/Batch/BatchType.swift | 10 +++++ .../Entity/Batch/ProcessingStatus.swift | 13 +++++++ .../AnthropicSwiftSDK/MessageBatches.swift | 18 +++++++++ .../Network/BatchListResponse.swift | 18 +++++++++ .../Network/BatchRequest.swift | 23 +++++++++++ .../Network/BatchResponse.swift | 38 +++++++++++++++++++ .../Network/MessagesRequest.swift | 1 + 9 files changed, 149 insertions(+) create mode 100644 Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift create mode 100644 Sources/AnthropicSwiftSDK/Entity/Batch/BatchResult.swift create mode 100644 Sources/AnthropicSwiftSDK/Entity/Batch/BatchType.swift create mode 100644 Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift create mode 100644 Sources/AnthropicSwiftSDK/MessageBatches.swift create mode 100644 Sources/AnthropicSwiftSDK/Network/BatchListResponse.swift create mode 100644 Sources/AnthropicSwiftSDK/Network/BatchRequest.swift create mode 100644 Sources/AnthropicSwiftSDK/Network/BatchResponse.swift diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift new file mode 100644 index 0000000..e69c9d4 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift @@ -0,0 +1,14 @@ +// +// BatchRequestCounts.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/09. +// + +struct BatchRequestCounts: Decodable { + let processing: Int + let succeeeded: Int + let errored: Int + let canceled: Int + let expired: Int +} diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResult.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResult.swift new file mode 100644 index 0000000..2f1cfa6 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResult.swift @@ -0,0 +1,14 @@ +// +// BatchResult.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/09. +// + +/// https://docs.anthropic.com/en/docs/build-with-claude/message-batches#retrieving-batch-results +enum BatchResult { + case succeeded // include the message result as jsonl + case errored(Error) + case cancelled + case expired +} diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchType.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchType.swift new file mode 100644 index 0000000..337b601 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchType.swift @@ -0,0 +1,10 @@ +// +// BatchType.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/09. +// + +enum BatchType: String, RawRepresentable, Decodable { + case message = "message_batch" +} diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift new file mode 100644 index 0000000..6995734 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift @@ -0,0 +1,13 @@ +// +// ProcessingStatus.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/09. +// + +/// https://docs.anthropic.com/en/api/retrieving-message-batches +enum ProcessingStatus: String, RawRepresentable, Decodable { + case inProgress = "in_progress" + case canceling + case ended +} diff --git a/Sources/AnthropicSwiftSDK/MessageBatches.swift b/Sources/AnthropicSwiftSDK/MessageBatches.swift new file mode 100644 index 0000000..da60cd3 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/MessageBatches.swift @@ -0,0 +1,18 @@ +// +// Batches.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/09. +// + +public struct MessageBatches { + public func createBatches() {} + + public func retrieve(batchId: String) {} + + public func results(of batchId: String) {} + + public func list(beforeId: String? = nil, afterId: String? = nil, limit: Int = 20) {} + + public func cancel(batchId: String) {} +} diff --git a/Sources/AnthropicSwiftSDK/Network/BatchListResponse.swift b/Sources/AnthropicSwiftSDK/Network/BatchListResponse.swift new file mode 100644 index 0000000..011e8cc --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/BatchListResponse.swift @@ -0,0 +1,18 @@ +// +// BatchListResponse.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/09. +// + +/// https://docs.anthropic.com/en/api/listing-message-batches +struct BatchListResponse { + /// List of `BatchResponse` object. + let data: [BatchResponse] + /// Indicates if there are more results in the requested page direction. + let hasMore: Bool + /// First ID in the `data` list. Can be used as the `before_id` for the previous page. + let firstId: String? + /// Last ID in the `data` list. Can be used as the `after_id` for the next page. + let lastId: String? +} diff --git a/Sources/AnthropicSwiftSDK/Network/BatchRequest.swift b/Sources/AnthropicSwiftSDK/Network/BatchRequest.swift new file mode 100644 index 0000000..5cc3535 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/BatchRequest.swift @@ -0,0 +1,23 @@ +// +// BatchRequest.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/09. +// + +public struct Batch: Encodable { + /// Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order. + /// + /// Must be unique for each request within the Message Batch. + let customId: String + /// Messages API creation parameters for the individual request. + /// + /// See the [Messages API reference](https://docs.anthropic.com/en/api/messages) for full documentation on available parameters. + let params: MessagesRequest +} + +/// https://docs.anthropic.com/en/api/creating-message-batches +public struct BatchRequest: Encodable { + /// List of requests for prompt completion. Each is an individual request to create a Message. + let requests: [Batch] +} diff --git a/Sources/AnthropicSwiftSDK/Network/BatchResponse.swift b/Sources/AnthropicSwiftSDK/Network/BatchResponse.swift new file mode 100644 index 0000000..c33d155 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/BatchResponse.swift @@ -0,0 +1,38 @@ +// +// BatchResponse.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/09. +// + +/// https://docs.anthropic.com/en/api/creating-message-batches +struct BatchResponse: Decodable { + /// Unique object identifier. + /// + /// The format and length of IDs may change over time. + let id: String + /// Object type. + /// + /// For Message Batches, this is always "message_batch". + let type: BatchType + /// Processing status of the Message Batch. + let processingStatus: ProcessingStatus + /// Tallies requests within the Message Batch, categorized by their status. + /// + /// Requests start as processing and move to one of the other statuses only once processing of the entire batch ends. The sum of all values always matches the total number of requests in the batch. + let requestCounts: BatchRequestCounts + /// RFC 3339 datetime string representing the time at which processing for the Message Batch ended. Specified only once processing ends. + /// + /// Processing ends when every request in a Message Batch has either succeeded, errored, canceled, or expired. + let endedAt: String? + /// RFC 3339 datetime string representing the time at which the Message Batch was created. + let createdAt: String + /// RFC 3339 datetime string representing the time at which the Message Batch will expire and end processing, which is 24 hours after creation. + let expiresAt: String + /// RFC 3339 datetime string representing the time at which cancellation was initiated for the Message Batch. Specified only if cancellation was initiated. + let cancelInitiatedAt: String? + /// URL to a .jsonl file containing the results of the Message Batch requests. Specified only once processing ends. + /// + /// Results in the file are not guaranteed to be in the same order as requests. Use the custom_id field to match results to requests. + let resultsURL: String? +} diff --git a/Sources/AnthropicSwiftSDK/Network/MessagesRequest.swift b/Sources/AnthropicSwiftSDK/Network/MessagesRequest.swift index d26acc5..1f0751d 100644 --- a/Sources/AnthropicSwiftSDK/Network/MessagesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/MessagesRequest.swift @@ -82,6 +82,7 @@ public struct MessagesRequest: Encodable { } } +// TODO: remove this extension if it is not needed. extension MessagesRequest { public func encode(with appendingObject: [String: Any], without removingObjectKeys: [String] = []) throws -> Data { let encoded = try anthropicJSONEncoder.encode(self) From 0349e23a88554a44f66fdf80b8a4e659be73ecdd Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Thu, 17 Oct 2024 22:09:16 +0900 Subject: [PATCH 02/26] refactor APIClient with using `Request` protocol for request object --- .../MessagesRequest+Extensions.swift | 15 ++- Sources/AnthropicSwiftSDK/Messages.swift | 72 ++++++------ .../Network/AnthropicAPIClient.swift | 105 ++++++------------ .../{ => Request}/MessagesRequest.swift | 59 +++++----- .../Network/Request/Request.swift | 49 ++++++++ .../Network/AnthropicAPIClientTests.swift | 32 +++--- 6 files changed, 171 insertions(+), 161 deletions(-) rename Sources/AnthropicSwiftSDK/Network/{ => Request}/MessagesRequest.swift (73%) create mode 100644 Sources/AnthropicSwiftSDK/Network/Request/Request.swift diff --git a/Sources/AnthropicSwiftSDK-TestUtils/MessagesRequest+Extensions.swift b/Sources/AnthropicSwiftSDK-TestUtils/MessagesRequest+Extensions.swift index 8354c89..7744d4e 100644 --- a/Sources/AnthropicSwiftSDK-TestUtils/MessagesRequest+Extensions.swift +++ b/Sources/AnthropicSwiftSDK-TestUtils/MessagesRequest+Extensions.swift @@ -8,8 +8,17 @@ import Foundation import AnthropicSwiftSDK -extension MessagesRequest { - public static var nop: Self { - MessagesRequest(messages: [], maxTokens: 1024) +public struct NopRequest: Request { + public let method: HttpMethod + public let path: String + public let queries: [String : any CustomStringConvertible]? = nil + public let body: Never? = nil + + public init( + method: HttpMethod = .post, + path: String = "/v1/messages" + ) { + self.method = method + self.path = path } } diff --git a/Sources/AnthropicSwiftSDK/Messages.swift b/Sources/AnthropicSwiftSDK/Messages.swift index 50df9d5..0a0667f 100644 --- a/Sources/AnthropicSwiftSDK/Messages.swift +++ b/Sources/AnthropicSwiftSDK/Messages.swift @@ -99,28 +99,30 @@ public struct Messages { anthropicHeaderProvider: AnthropicHeaderProvider, authenticationHeaderProvider: AuthenticationHeaderProvider ) async throws -> MessagesResponse { - let client = AnthropicAPIClient( + let client = APIClient( + session: session, anthropicHeaderProvider: anthropicHeaderProvider, - authenticationHeaderProvider: authenticationHeaderProvider, - session: session + authenticationHeaderProvider: authenticationHeaderProvider ) - let requestBody = MessagesRequest( - model: model, - messages: messages, - system: system, - maxTokens: maxTokens, - metaData: metaData, - stopSequences: stopSequence, - stream: false, - temperature: temperature, - topP: topP, - topK: topK, - tools: toolContainer?.allTools, - toolChoice: toolChoice + let request = MessagesRequest( + body: .init( + model: model, + messages: messages, + system: system, + maxTokens: maxTokens, + metaData: metaData, + stopSequences: stopSequence, + stream: false, + temperature: temperature, + topP: topP, + topK: topK, + tools: toolContainer?.allTools, + toolChoice: toolChoice + ) ) - let (data, response) = try await client.send(requestBody: requestBody) + let (data, response) = try await client.send(request: request) guard let httpResponse = response as? HTTPURLResponse else { throw ClientError.cannotHandleURLResponse(response) @@ -300,28 +302,30 @@ public struct Messages { anthropicHeaderProvider: AnthropicHeaderProvider, authenticationHeaderProvider: AuthenticationHeaderProvider ) async throws -> AsyncThrowingStream { - let client = AnthropicAPIClient( + let client = APIClient( + session: session, anthropicHeaderProvider: anthropicHeaderProvider, - authenticationHeaderProvider: authenticationHeaderProvider, - session: session + authenticationHeaderProvider: authenticationHeaderProvider ) - let requestBody = MessagesRequest( - model: model, - messages: messages, - system: system, - maxTokens: maxTokens, - metaData: metaData, - stopSequences: stopSequence, - stream: true, - temperature: temperature, - topP: topP, - topK: topK, - tools: toolContainer?.allTools, - toolChoice: toolChoice + let request = MessagesRequest( + body: .init( + model: model, + messages: messages, + system: system, + maxTokens: maxTokens, + metaData: metaData, + stopSequences: stopSequence, + stream: true, + temperature: temperature, + topP: topP, + topK: topK, + tools: toolContainer?.allTools, + toolChoice: toolChoice + ) ) - let (data, response) = try await client.stream(requestBody: requestBody) + let (data, response) = try await client.stream(request: request) guard let httpResponse = response as? HTTPURLResponse else { throw ClientError.cannotHandleURLResponse(response) diff --git a/Sources/AnthropicSwiftSDK/Network/AnthropicAPIClient.swift b/Sources/AnthropicSwiftSDK/Network/AnthropicAPIClient.swift index 4037f97..646bca0 100644 --- a/Sources/AnthropicSwiftSDK/Network/AnthropicAPIClient.swift +++ b/Sources/AnthropicSwiftSDK/Network/AnthropicAPIClient.swift @@ -7,35 +7,23 @@ import Foundation -enum HttpMethod: String { - case post = "POST" -} - -enum APIType: String { - case messages -// case textCompletions - - var path: String { - switch self { - case .messages: - return "/v1/messages" - } - } - - var method: HttpMethod { - switch self { - case .messages: - return .post - } - } -} - -struct AnthropicAPIClient { - let endpoint = "https://api.anthropic.com" - let type: APIType +struct APIClient { + private let session: URLSession + let baseURL: URL let anthropicHeaderProvider: AnthropicHeaderProvider let authenticationHeaderProvider: AuthenticationHeaderProvider - private let session: URLSession + + init( + session: URLSession = .shared, + baseURL: URL = .init(string: "https://api.anthropic.com")!, // swiftlint:disable:this force_unwrapping + anthropicHeaderProvider: AnthropicHeaderProvider, + authenticationHeaderProvider: AuthenticationHeaderProvider + ) { + self.session = session + self.baseURL = baseURL + self.anthropicHeaderProvider = anthropicHeaderProvider + self.authenticationHeaderProvider = authenticationHeaderProvider + } private var headers: [String: String] { var headers: [String: String] = [:] @@ -51,65 +39,34 @@ struct AnthropicAPIClient { return headers } - private var requestURL: URL? { - guard var urlComponents = URLComponents(string: endpoint) else { - return nil - } - - urlComponents.path = type.path - - return urlComponents.url - } - - private var urlRequest: URLRequest { - guard let url = requestURL else { - fatalError("APIClient must have requestURL") - } - - return URLRequest(url: url) - } - - init( - type: APIType = .messages, - anthropicHeaderProvider: AnthropicHeaderProvider, - authenticationHeaderProvider: AuthenticationHeaderProvider, - session: URLSession - ) { - self.type = type - self.anthropicHeaderProvider = anthropicHeaderProvider - self.authenticationHeaderProvider = authenticationHeaderProvider - self.session = session - } - /// Send messages API request. This method receives HTTP response from API. /// - /// For more detail, see https://docs.anthropic.com/claude/reference/messages_post. - /// - Parameter requestBody: request body for api request + /// - Parameter request: request body for api request /// - Returns: response from API - func send(requestBody: MessagesRequest) async throws -> (Data, URLResponse) { - var request = urlRequest + func send(request: any Request) async throws -> (Data, URLResponse) { + var urlRequest = try request.toURLRequest(for: baseURL) headers.forEach { key, value in - request.setValue(value, forHTTPHeaderField: key) + urlRequest.setValue(value, forHTTPHeaderField: key) } - request.httpMethod = type.method.rawValue - request.httpBody = try anthropicJSONEncoder.encode(requestBody) - return try await session.data(for: request) + return try await session.data(for: urlRequest) } - /// Send messages API request. This method read the messages api response sequentially. + /// Send messages API request. This method read the API response sequentially. /// - /// For more detail, see https://docs.anthropic.com/claude/reference/messages-streaming. - /// - Parameter requestBody: request body for api request + /// - Parameter request: request body for api request /// - Returns: response chunk from API - func stream(requestBody: MessagesRequest) async throws -> (URLSession.AsyncBytes, URLResponse) { - var request = urlRequest + func stream(request: any Request) async throws -> (URLSession.AsyncBytes, URLResponse) { + var urlRequest = try request.toURLRequest(for: baseURL) headers.forEach { key, value in - request.setValue(value, forHTTPHeaderField: key) + urlRequest.setValue(value, forHTTPHeaderField: key) } - request.httpMethod = type.method.rawValue - request.httpBody = try anthropicJSONEncoder.encode(requestBody) - return try await session.bytes(for: request) + return try await session.bytes(for: urlRequest) } } + +public enum HttpMethod: String { + case post = "POST" + case get = "GET" +} diff --git a/Sources/AnthropicSwiftSDK/Network/MessagesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift similarity index 73% rename from Sources/AnthropicSwiftSDK/Network/MessagesRequest.swift rename to Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift index 1f0751d..ef9cdea 100644 --- a/Sources/AnthropicSwiftSDK/Network/MessagesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift @@ -8,52 +8,63 @@ import Foundation import FunctionCalling +struct MessagesRequest: Request { + typealias Body = MessagesRequestBody + + let method: HttpMethod = .post + let path: String = RequestType.messages.basePath + let queries: [String: CustomStringConvertible]? = nil + let body: Body? +} + +// MARK: Request Body + /// Request object for Messages API /// /// a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation. -public struct MessagesRequest: Encodable { +struct MessagesRequestBody: Encodable { /// The model that will complete your prompt. - public let model: Model + let model: Model /// Input messages. - public let messages: [Message] + let messages: [Message] /// System prompt. /// /// A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. - public let system: [SystemPrompt] + let system: [SystemPrompt] /// The maximum number of tokens to generate before stopping. /// /// Note that our models may stop before reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate. /// Different models have different maximum values for this parameter. - public let maxTokens: Int + let maxTokens: Int /// An object describing metadata about the request. - public let metaData: MetaData? + let metaData: MetaData? /// Custom text sequences that will cause the model to stop generating. - public let stopSequences: [String]? + let stopSequences: [String]? /// Whether to incrementally stream the response using server-sent events. /// /// see [streaming](https://docs.anthropic.com/claude/reference/messages-streaming) for more detail. - public let stream: Bool + let stream: Bool /// Amount of randomness injected into the response. /// /// Defaults to 1.0. Ranges from 0.0 to 1.0. Use temperature closer to 0.0 for analytical / multiple choice, and closer to 1.0 for creative and generative tasks. /// Note that even with temperature of 0.0, the results will not be fully deterministic. - public let temperature: Double? + let temperature: Double? /// Use nucleus sampling. /// /// In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both. /// Recommended for advanced use cases only. You usually only need to use temperature. - public let topP: Double? + let topP: Double? /// Only sample from the top K options for each subsequent token. /// /// Used to remove "long tail" low probability responses. /// Recommended for advanced use cases only. You usually only need to use temperature. - public let topK: Int? + let topK: Int? /// Definition of tools with names, descriptions, and input schemas in your API request. - public let tools: [Tool]? + let tools: [Tool]? /// Definition whether or not to force Claude to use the tool. ToolChoice should be set if tools are specified. - public let toolChoice: ToolChoice? + let toolChoice: ToolChoice? - public init( + init( model: Model = .claude_3_Opus, messages: [Message], system: [SystemPrompt] = [], @@ -81,23 +92,3 @@ public struct MessagesRequest: Encodable { self.toolChoice = tools == nil ? nil : toolChoice // ToolChoice should be set if tools are specified. } } - -// TODO: remove this extension if it is not needed. -extension MessagesRequest { - public func encode(with appendingObject: [String: Any], without removingObjectKeys: [String] = []) throws -> Data { - let encoded = try anthropicJSONEncoder.encode(self) - guard var dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: Any] else { - return encoded - } - - appendingObject.forEach { key, value in - dictionary[key] = value - } - - removingObjectKeys.forEach { key in - dictionary.removeValue(forKey: key) - } - - return try JSONSerialization.data(withJSONObject: dictionary, options: []) - } -} diff --git a/Sources/AnthropicSwiftSDK/Network/Request/Request.swift b/Sources/AnthropicSwiftSDK/Network/Request/Request.swift new file mode 100644 index 0000000..907a27e --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/Request/Request.swift @@ -0,0 +1,49 @@ +// +// Request.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/13. +// + +import Foundation + +enum RequestType { + case messages + case batches + + var basePath: String { + switch self { + case .messages: + return "/v1/messages" + case .batches: + return "/v1/messages/batches" + } + } +} + +public protocol Request { + associatedtype Body: Encodable + + var method: HttpMethod { get } + var path: String { get } + var queries: [String: CustomStringConvertible]? { get } + var body: Body? { get } +} + +extension Request { + func toURLRequest(for baseURL: URL) throws -> URLRequest { + let url = baseURL.appendingPathComponent(path) + + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + if let body { + request.httpBody = try anthropicJSONEncoder.encode(body) + } + + if let queries { + request.url?.append(queryItems: queries.map { .init(name: $0.key, value: $0.value.description) }) + } + + return request + } +} diff --git a/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift b/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift index 7889d81..0dd9654 100644 --- a/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift +++ b/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift @@ -20,10 +20,10 @@ final class AnthropicAPIClientTests: XCTestCase { } func testAPITypeProvidesCorrectMethodAndPathForSend() async throws { - let client = AnthropicAPIClient( + let client = APIClient( + session: session, anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), - authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: ""), - session: session + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: "") ) let expectation = XCTestExpectation(description: "APIType.message should `/v1/messages` path and `POST` method.") @@ -34,15 +34,15 @@ final class AnthropicAPIClientTests: XCTestCase { expectation.fulfill() }) - let _ = try await client.send(requestBody: .nop) + let _ = try await client.send(request: NopRequest()) await fulfillment(of: [expectation], timeout: 1.0) } func testAPITypeProvidesCorrectMethodAndPathForStream() async throws { - let client = AnthropicAPIClient( + let client = APIClient( + session: session, anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), - authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: ""), - session: session + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: "") ) let expectation = XCTestExpectation(description: "APIType.message should `/v1/messages` path and `POST` method.") @@ -53,15 +53,15 @@ final class AnthropicAPIClientTests: XCTestCase { expectation.fulfill() }) - let _ = try await client.stream(requestBody: .nop) + let _ = try await client.stream(request: NopRequest()) await fulfillment(of: [expectation], timeout: 1.0) } func testHeaderProviderProvidesCorrectHeadersForSend() async throws { - let client = AnthropicAPIClient( + let client = APIClient( + session: session, anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), - authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: "test-api-key"), - session: session + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: "test-api-key") ) let expectation = XCTestExpectation(description: "API request should have headers `x-api-key`, `anthropic-version`, `content-type` and `anthropic-beta`.") @@ -74,15 +74,15 @@ final class AnthropicAPIClientTests: XCTestCase { expectation.fulfill() }) - let _ = try await client.send(requestBody: .nop) + let _ = try await client.send(request: NopRequest()) await fulfillment(of: [expectation], timeout: 1.0) } func testHeaderProviderProvidesCorrectHeadersForStream() async throws { - let client = AnthropicAPIClient( + let client = APIClient( + session: session, anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), - authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: "test-api-key"), - session: session + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: "test-api-key") ) let expectation = XCTestExpectation(description: "API request should have headers `x-api-key`, `anthropic-version`, `content-type` and `anthropic-beta`.") @@ -95,7 +95,7 @@ final class AnthropicAPIClientTests: XCTestCase { expectation.fulfill() }) - let _ = try await client.stream(requestBody: .nop) + let _ = try await client.stream(request: NopRequest()) await fulfillment(of: [expectation], timeout: 1.0) } From e0da226ad14329ea0c7b160aefc3609fc2231bb5 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Thu, 17 Oct 2024 22:10:14 +0900 Subject: [PATCH 03/26] separate directory for response object --- .../Network/{ => Response}/BatchListResponse.swift | 0 .../AnthropicSwiftSDK/Network/{ => Response}/BatchResponse.swift | 0 .../Network/{ => Response}/MessagesResponse.swift | 0 .../Network/{ => Response}/StreamingResponse.swift | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Sources/AnthropicSwiftSDK/Network/{ => Response}/BatchListResponse.swift (100%) rename Sources/AnthropicSwiftSDK/Network/{ => Response}/BatchResponse.swift (100%) rename Sources/AnthropicSwiftSDK/Network/{ => Response}/MessagesResponse.swift (100%) rename Sources/AnthropicSwiftSDK/Network/{ => Response}/StreamingResponse.swift (100%) diff --git a/Sources/AnthropicSwiftSDK/Network/BatchListResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/BatchListResponse.swift similarity index 100% rename from Sources/AnthropicSwiftSDK/Network/BatchListResponse.swift rename to Sources/AnthropicSwiftSDK/Network/Response/BatchListResponse.swift diff --git a/Sources/AnthropicSwiftSDK/Network/BatchResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/BatchResponse.swift similarity index 100% rename from Sources/AnthropicSwiftSDK/Network/BatchResponse.swift rename to Sources/AnthropicSwiftSDK/Network/Response/BatchResponse.swift diff --git a/Sources/AnthropicSwiftSDK/Network/MessagesResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/MessagesResponse.swift similarity index 100% rename from Sources/AnthropicSwiftSDK/Network/MessagesResponse.swift rename to Sources/AnthropicSwiftSDK/Network/Response/MessagesResponse.swift diff --git a/Sources/AnthropicSwiftSDK/Network/StreamingResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/StreamingResponse.swift similarity index 100% rename from Sources/AnthropicSwiftSDK/Network/StreamingResponse.swift rename to Sources/AnthropicSwiftSDK/Network/Response/StreamingResponse.swift From f5f56098ca3b00bfe26124740b002aaa45cc220d Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Thu, 17 Oct 2024 22:12:08 +0900 Subject: [PATCH 04/26] update support macOS version to conform `Never` to `Encodable` --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index dc8403c..1dd197a 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "AnthropicSwiftSDK", platforms: [ .iOS(.v17), - .macOS(.v13) + .macOS(.v14) ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. From a9e888d90595129216dc21d84c0cc254285a53a9 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Thu, 17 Oct 2024 22:10:41 +0900 Subject: [PATCH 05/26] use `Request` protocol to build each request objects --- .../Request/CancelMessageBatchRequest.swift | 23 +++++++++++++++ .../Request/ListMessageBatchesRequest.swift | 13 +++++++++ .../MessageBatchesRequest.swift} | 28 +++++++++++++------ .../RetrieveMessageBatchResultsRequest.swift | 17 +++++++++++ .../RetrieveMessageBatchesRequest.swift | 17 +++++++++++ 5 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift create mode 100644 Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift rename Sources/AnthropicSwiftSDK/Network/{BatchRequest.swift => Request/MessageBatchesRequest.swift} (61%) create mode 100644 Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchResultsRequest.swift create mode 100644 Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchesRequest.swift diff --git a/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift new file mode 100644 index 0000000..429d9ca --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift @@ -0,0 +1,23 @@ +// +// CancelMessageBatchRequest.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/16. +// + +struct CancelMessageBatchRequest: Request { + let method: HttpMethod = .post + var path: String { + "\(RequestType.batches.basePath)/\(batchId)/cancel" + } + let queries: [String: CustomStringConvertible]? + let body: Never? = nil + + enum Parameter: String { + case beforeId + case afterId + case limit + } + + let batchId: String +} diff --git a/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift new file mode 100644 index 0000000..e7786b5 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift @@ -0,0 +1,13 @@ +// +// ListMessageBatchesRequest.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/16. +// + +struct ListMessageBatchesRequest: Request { + let method: HttpMethod = .get + let path: String = RequestType.batches.basePath + let queries: [String: CustomStringConvertible]? = nil + let body: Never? = nil +} diff --git a/Sources/AnthropicSwiftSDK/Network/BatchRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift similarity index 61% rename from Sources/AnthropicSwiftSDK/Network/BatchRequest.swift rename to Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift index 5cc3535..9993248 100644 --- a/Sources/AnthropicSwiftSDK/Network/BatchRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift @@ -1,10 +1,28 @@ // -// BatchRequest.swift +// MessageBatchesRequest.swift // AnthropicSwiftSDK // // Created by 伊藤史 on 2024/10/09. // +struct MessageBatchesRequest: Request { + typealias Body = MessageBatchesRequestBody + + let method: HttpMethod = .post + let path: String = RequestType.batches.basePath + let queries: [String: CustomStringConvertible]? = nil + let body: MessageBatchesRequestBody? +} + +// MARK: Request Body + +/// https://docs.anthropic.com/en/api/creating-message-batches +public struct MessageBatchesRequestBody: Encodable { + /// List of requests for prompt completion. Each is an individual request to create a Message. + let requests: [Batch] +} + +/// https://docs.anthropic.com/en/api/creating-message-batches public struct Batch: Encodable { /// Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order. /// @@ -13,11 +31,5 @@ public struct Batch: Encodable { /// Messages API creation parameters for the individual request. /// /// See the [Messages API reference](https://docs.anthropic.com/en/api/messages) for full documentation on available parameters. - let params: MessagesRequest -} - -/// https://docs.anthropic.com/en/api/creating-message-batches -public struct BatchRequest: Encodable { - /// List of requests for prompt completion. Each is an individual request to create a Message. - let requests: [Batch] + let params: MessagesRequestBody } diff --git a/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchResultsRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchResultsRequest.swift new file mode 100644 index 0000000..2361ac9 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchResultsRequest.swift @@ -0,0 +1,17 @@ +// +// RetrieveMessageBatchResultsRequest.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/16. +// + +struct RetrieveMessageBatchResultsRequest: Request { + let method: HttpMethod = .get + var path: String { + "\(RequestType.batches.basePath)/\(batchId)/results" + } + let queries: [String: CustomStringConvertible]? = nil + let body: Never? = nil + + let batchId: String +} diff --git a/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchesRequest.swift new file mode 100644 index 0000000..ca2896a --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchesRequest.swift @@ -0,0 +1,17 @@ +// +// RetrieveMessageBatchesRequest.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/16. +// + +struct RetrieveMessageBatchesRequest: Request { + let method: HttpMethod = .get + var path: String { + "\(RequestType.batches.basePath)/\(batchId)" + } + let queries: [String: CustomStringConvertible]? = nil + let body: Never? = nil + + let batchId: String +} From 8d37383f6ca31493bb48b4301e595bacc1302f9e Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 18 Oct 2024 10:58:43 +0900 Subject: [PATCH 06/26] move `HttpMethod` --- Sources/AnthropicSwiftSDK/Network/AnthropicAPIClient.swift | 5 ----- Sources/AnthropicSwiftSDK/Network/Request/Request.swift | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/AnthropicSwiftSDK/Network/AnthropicAPIClient.swift b/Sources/AnthropicSwiftSDK/Network/AnthropicAPIClient.swift index 646bca0..5cfcbcb 100644 --- a/Sources/AnthropicSwiftSDK/Network/AnthropicAPIClient.swift +++ b/Sources/AnthropicSwiftSDK/Network/AnthropicAPIClient.swift @@ -65,8 +65,3 @@ struct APIClient { return try await session.bytes(for: urlRequest) } } - -public enum HttpMethod: String { - case post = "POST" - case get = "GET" -} diff --git a/Sources/AnthropicSwiftSDK/Network/Request/Request.swift b/Sources/AnthropicSwiftSDK/Network/Request/Request.swift index 907a27e..2b4f831 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/Request.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/Request.swift @@ -7,6 +7,11 @@ import Foundation +public enum HttpMethod: String { + case post = "POST" + case get = "GET" +} + enum RequestType { case messages case batches From 18b60d663263e060484fbddb91d0710c0de4c536 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 18 Oct 2024 14:22:31 +0900 Subject: [PATCH 07/26] update beta version for batch api --- .../Network/HeaderProvider/AnthropicHeaderProvider.swift | 2 +- .../Network/AnthropicAPIClientTests.swift | 4 ++-- .../HeaderProvider/DefaultAnthropicHeaderProviderTests.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/AnthropicSwiftSDK/Network/HeaderProvider/AnthropicHeaderProvider.swift b/Sources/AnthropicSwiftSDK/Network/HeaderProvider/AnthropicHeaderProvider.swift index 543f235..a51e516 100644 --- a/Sources/AnthropicSwiftSDK/Network/HeaderProvider/AnthropicHeaderProvider.swift +++ b/Sources/AnthropicSwiftSDK/Network/HeaderProvider/AnthropicHeaderProvider.swift @@ -20,7 +20,7 @@ struct DefaultAnthropicHeaderProvider: AnthropicHeaderProvider { /// content type of response, now only support JSON let contentType = "application/json" - private let betaDescription = "prompt-caching-2024-07-31" + private let betaDescription = "message-batches-2024-09-24" func getAnthropicAPIHeaders() -> [String: String] { var headers: [String: String] = [ diff --git a/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift b/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift index 0dd9654..c4ea56e 100644 --- a/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift +++ b/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift @@ -69,7 +69,7 @@ final class AnthropicAPIClientTests: XCTestCase { XCTAssertEqual(headers!["x-api-key"], "test-api-key") XCTAssertEqual(headers!["anthropic-version"], "2023-06-01") XCTAssertEqual(headers!["Content-Type"], "application/json") - XCTAssertEqual(headers!["anthropic-beta"], "prompt-caching-2024-07-31") + XCTAssertEqual(headers!["anthropic-beta"], "message-batches-2024-09-24") expectation.fulfill() }) @@ -90,7 +90,7 @@ final class AnthropicAPIClientTests: XCTestCase { XCTAssertEqual(headers!["x-api-key"], "test-api-key") XCTAssertEqual(headers!["anthropic-version"], "2023-06-01") XCTAssertEqual(headers!["Content-Type"], "application/json") - XCTAssertEqual(headers!["anthropic-beta"], "prompt-caching-2024-07-31") + XCTAssertEqual(headers!["anthropic-beta"], "message-batches-2024-09-24") expectation.fulfill() }) diff --git a/Tests/AnthropicSwiftSDKTests/Network/HeaderProvider/DefaultAnthropicHeaderProviderTests.swift b/Tests/AnthropicSwiftSDKTests/Network/HeaderProvider/DefaultAnthropicHeaderProviderTests.swift index 8456c37..d682373 100644 --- a/Tests/AnthropicSwiftSDKTests/Network/HeaderProvider/DefaultAnthropicHeaderProviderTests.swift +++ b/Tests/AnthropicSwiftSDKTests/Network/HeaderProvider/DefaultAnthropicHeaderProviderTests.swift @@ -29,7 +29,7 @@ final class DefaultAnthropicHeaderProviderTests: XCTestCase { let provider = DefaultAnthropicHeaderProvider(useBeta: true) let headers = provider.getAnthropicAPIHeaders() - XCTAssertEqual(headers["anthropic-beta"], "prompt-caching-2024-07-31") + XCTAssertEqual(headers["anthropic-beta"], "message-batches-2024-09-24") } func testBetaHeaderShouldNotBeProvidedIfUseBeta() { From fa5f3a69ce8c9044304abac13f7cee0fcbac837f Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 18 Oct 2024 14:25:07 +0900 Subject: [PATCH 08/26] publish response entities --- .../Entity/Batch/BatchRequestCounts.swift | 2 +- ...atchResult.swift => BatchResultType.swift} | 6 +++--- .../Entity/Batch/BatchType.swift | 2 +- .../Entity/Batch/ProcessingStatus.swift | 2 +- .../Network/Response/BatchListResponse.swift | 10 +++++----- .../Network/Response/BatchResponse.swift | 20 +++++++++---------- .../Response/BatchResultResponse.swift | 17 ++++++++++++++++ 7 files changed, 38 insertions(+), 21 deletions(-) rename Sources/AnthropicSwiftSDK/Entity/Batch/{BatchResult.swift => BatchResultType.swift} (76%) create mode 100644 Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift index e69c9d4..0814875 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift @@ -5,7 +5,7 @@ // Created by 伊藤史 on 2024/10/09. // -struct BatchRequestCounts: Decodable { +public struct BatchRequestCounts: Decodable { let processing: Int let succeeeded: Int let errored: Int diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResult.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift similarity index 76% rename from Sources/AnthropicSwiftSDK/Entity/Batch/BatchResult.swift rename to Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift index 2f1cfa6..f101c68 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResult.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift @@ -1,14 +1,14 @@ // -// BatchResult.swift +// BatchResultType.swift // AnthropicSwiftSDK // // Created by 伊藤史 on 2024/10/09. // /// https://docs.anthropic.com/en/docs/build-with-claude/message-batches#retrieving-batch-results -enum BatchResult { +public enum BatchResultType: Decodable { case succeeded // include the message result as jsonl - case errored(Error) + case errored case cancelled case expired } diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchType.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchType.swift index 337b601..28c9cb7 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchType.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchType.swift @@ -5,6 +5,6 @@ // Created by 伊藤史 on 2024/10/09. // -enum BatchType: String, RawRepresentable, Decodable { +public enum BatchType: String, RawRepresentable, Decodable { case message = "message_batch" } diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift index 6995734..737ffe9 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift @@ -6,7 +6,7 @@ // /// https://docs.anthropic.com/en/api/retrieving-message-batches -enum ProcessingStatus: String, RawRepresentable, Decodable { +public enum ProcessingStatus: String, RawRepresentable, Decodable { case inProgress = "in_progress" case canceling case ended diff --git a/Sources/AnthropicSwiftSDK/Network/Response/BatchListResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/BatchListResponse.swift index 011e8cc..a73ca81 100644 --- a/Sources/AnthropicSwiftSDK/Network/Response/BatchListResponse.swift +++ b/Sources/AnthropicSwiftSDK/Network/Response/BatchListResponse.swift @@ -6,13 +6,13 @@ // /// https://docs.anthropic.com/en/api/listing-message-batches -struct BatchListResponse { +public struct BatchListResponse: Decodable { /// List of `BatchResponse` object. - let data: [BatchResponse] + public let data: [BatchResponse] /// Indicates if there are more results in the requested page direction. - let hasMore: Bool + public let hasMore: Bool /// First ID in the `data` list. Can be used as the `before_id` for the previous page. - let firstId: String? + public let firstId: String? /// Last ID in the `data` list. Can be used as the `after_id` for the next page. - let lastId: String? + public let lastId: String? } diff --git a/Sources/AnthropicSwiftSDK/Network/Response/BatchResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/BatchResponse.swift index c33d155..3527ad6 100644 --- a/Sources/AnthropicSwiftSDK/Network/Response/BatchResponse.swift +++ b/Sources/AnthropicSwiftSDK/Network/Response/BatchResponse.swift @@ -6,33 +6,33 @@ // /// https://docs.anthropic.com/en/api/creating-message-batches -struct BatchResponse: Decodable { +public struct BatchResponse: Decodable { /// Unique object identifier. /// /// The format and length of IDs may change over time. - let id: String + public let id: String /// Object type. /// /// For Message Batches, this is always "message_batch". - let type: BatchType + public let type: BatchType /// Processing status of the Message Batch. - let processingStatus: ProcessingStatus + public let processingStatus: ProcessingStatus /// Tallies requests within the Message Batch, categorized by their status. /// /// Requests start as processing and move to one of the other statuses only once processing of the entire batch ends. The sum of all values always matches the total number of requests in the batch. - let requestCounts: BatchRequestCounts + public let requestCounts: BatchRequestCounts /// RFC 3339 datetime string representing the time at which processing for the Message Batch ended. Specified only once processing ends. /// /// Processing ends when every request in a Message Batch has either succeeded, errored, canceled, or expired. - let endedAt: String? + public let endedAt: String? /// RFC 3339 datetime string representing the time at which the Message Batch was created. - let createdAt: String + public let createdAt: String /// RFC 3339 datetime string representing the time at which the Message Batch will expire and end processing, which is 24 hours after creation. - let expiresAt: String + public let expiresAt: String /// RFC 3339 datetime string representing the time at which cancellation was initiated for the Message Batch. Specified only if cancellation was initiated. - let cancelInitiatedAt: String? + public let cancelInitiatedAt: String? /// URL to a .jsonl file containing the results of the Message Batch requests. Specified only once processing ends. /// /// Results in the file are not guaranteed to be in the same order as requests. Use the custom_id field to match results to requests. - let resultsURL: String? + public let resultsURL: String? } diff --git a/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift new file mode 100644 index 0000000..a5d9cff --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift @@ -0,0 +1,17 @@ +// +// BatchResultResponse.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/18. +// + +public struct BatchResult: Decodable { + public let type: BatchResultType + public let message: MessagesResponse +} + +public struct BatchResultResponse: Decodable { + public let customId: String + public let result: BatchResult? + public let error: StreamingError? +} From 28b25e361ce3e9371aa518e016ac1beef0909723 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 18 Oct 2024 14:26:19 +0900 Subject: [PATCH 09/26] [should be merged] fix query handling --- .../Network/Request/CancelMessageBatchRequest.swift | 9 +-------- .../Network/Request/ListMessageBatchesRequest.swift | 8 +++++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift index 429d9ca..a2770dd 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift @@ -10,14 +10,7 @@ struct CancelMessageBatchRequest: Request { var path: String { "\(RequestType.batches.basePath)/\(batchId)/cancel" } - let queries: [String: CustomStringConvertible]? + let queries: [String: CustomStringConvertible]? = nil let body: Never? = nil - - enum Parameter: String { - case beforeId - case afterId - case limit - } - let batchId: String } diff --git a/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift index e7786b5..0488e1f 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift @@ -8,6 +8,12 @@ struct ListMessageBatchesRequest: Request { let method: HttpMethod = .get let path: String = RequestType.batches.basePath - let queries: [String: CustomStringConvertible]? = nil + let queries: [String: CustomStringConvertible]? let body: Never? = nil + + enum Parameter: String { + case beforeId = "before_id" + case afterId = "after_id" + case limit + } } From 96576305f867b94748966cd09df8727cc152e916 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 18 Oct 2024 14:27:33 +0900 Subject: [PATCH 10/26] make intertinate unneeded object --- .../Network/Request/MessageBatchesRequest.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift index 9993248..92950dc 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift @@ -17,13 +17,13 @@ struct MessageBatchesRequest: Request { // MARK: Request Body /// https://docs.anthropic.com/en/api/creating-message-batches -public struct MessageBatchesRequestBody: Encodable { +struct MessageBatchesRequestBody: Encodable { /// List of requests for prompt completion. Each is an individual request to create a Message. let requests: [Batch] } /// https://docs.anthropic.com/en/api/creating-message-batches -public struct Batch: Encodable { +struct Batch: Encodable { /// Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order. /// /// Must be unique for each request within the Message Batch. From 8acbee55ebff9fcb14760f801d0b3446ca132559 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 18 Oct 2024 14:28:13 +0900 Subject: [PATCH 11/26] add request entities --- .../Entity/Batch/BatchParameter.swift | 56 +++++++++++++++++++ .../Entity/Batch/MessageBatch.swift | 16 ++++++ .../Request/MessageBatchesRequest.swift | 9 +++ .../Network/Request/MessagesRequest.swift | 15 +++++ 4 files changed, 96 insertions(+) create mode 100644 Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift create mode 100644 Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift new file mode 100644 index 0000000..36d1fa5 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift @@ -0,0 +1,56 @@ +// +// BatchParameter.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/18. +// + +import FunctionCalling + +public struct BatchParameter { + public let messages: [Message] + public let model: Model + public let system: [SystemPrompt] + public let maxTokens: Int + public let metaData: MetaData? + public let stopSequence: [String]? + /// Whether the response should be handles as streaming or not, + /// + /// This parameter is always `false` + /// + /// - Note: Streaming is not supported for batch requests. + /// For more details, see https://docs.anthropic.com/en/docs/build-with-claude/message-batches#can-i-use-the-message-batches-api-with-other-api-features + public let stream: Bool = false + public let temperature: Double? + public let topP: Double? + public let topK: Int? + public let toolContainer: ToolContainer? + public let toolChoice: ToolChoice + + public init( + messages: [Message], + model: Model = .claude_3_Opus, + system: [SystemPrompt] = [], + maxTokens: Int, + metaData: MetaData? = nil, + stopSequence: [String]? = nil, + temperature: Double? = nil, + topP: Double? = nil, + topK: Int? = nil, + toolContainer: ToolContainer? = nil, + toolChoice: ToolChoice = .auto + ) { + self.messages = messages + self.model = model + self.system = system + self.maxTokens = maxTokens + self.metaData = metaData + self.stopSequence = stopSequence + self.temperature = temperature + self.topP = topP + self.topK = topK + self.toolContainer = toolContainer + self.toolChoice = toolChoice + } +} + diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift new file mode 100644 index 0000000..22fec77 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift @@ -0,0 +1,16 @@ +// +// MessageBatch.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/18. +// + +public struct MessageBatch { + public let customId: String + public let parameter: BatchParameter + + public init(customId: String, parameter: BatchParameter) { + self.customId = customId + self.parameter = parameter + } +} diff --git a/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift index 92950dc..e57f2bd 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift @@ -20,6 +20,10 @@ struct MessageBatchesRequest: Request { struct MessageBatchesRequestBody: Encodable { /// List of requests for prompt completion. Each is an individual request to create a Message. let requests: [Batch] + + init(from batches: [MessageBatch]) { + self.requests = batches.map { Batch(from: $0) } + } } /// https://docs.anthropic.com/en/api/creating-message-batches @@ -32,4 +36,9 @@ struct Batch: Encodable { /// /// See the [Messages API reference](https://docs.anthropic.com/en/api/messages) for full documentation on available parameters. let params: MessagesRequestBody + + init(from batch: MessageBatch) { + self.customId = batch.customId + self.params = MessagesRequestBody(from: batch.parameter) + } } diff --git a/Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift index ef9cdea..7aeb9d3 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift @@ -91,4 +91,19 @@ struct MessagesRequestBody: Encodable { self.tools = tools self.toolChoice = tools == nil ? nil : toolChoice // ToolChoice should be set if tools are specified. } + + init(from parameter: BatchParameter) { + self.model = parameter.model + self.messages = parameter.messages + self.system = parameter.system + self.maxTokens = parameter.maxTokens + self.metaData = parameter.metaData + self.stopSequences = parameter.stopSequence + self.stream = parameter.stream + self.temperature = parameter.temperature + self.topP = parameter.topP + self.topK = parameter.topK + self.tools = parameter.toolContainer?.allTools + self.toolChoice = parameter.toolContainer?.allTools == nil ? nil : parameter.toolChoice + } } From ecea79c8bc5c487053d089b1a03b13178bd8c4fe Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 18 Oct 2024 14:28:34 +0900 Subject: [PATCH 12/26] add API object and methods --- .../AnthropicSwiftSDK/MessageBatches.swift | 240 +++++++++++++++++- 1 file changed, 235 insertions(+), 5 deletions(-) diff --git a/Sources/AnthropicSwiftSDK/MessageBatches.swift b/Sources/AnthropicSwiftSDK/MessageBatches.swift index da60cd3..d560db5 100644 --- a/Sources/AnthropicSwiftSDK/MessageBatches.swift +++ b/Sources/AnthropicSwiftSDK/MessageBatches.swift @@ -5,14 +5,244 @@ // Created by 伊藤史 on 2024/10/09. // +import Foundation +import FunctionCalling + public struct MessageBatches { - public func createBatches() {} + private let apiKey: String + private let session: URLSession + + init(apiKey: String, session: URLSession) { + self.apiKey = apiKey + self.session = session + } + + public func createBatches(batches: [MessageBatch]) async throws -> BatchResponse { + try await createBatches( + batches: batches, + anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) + ) + } + + public func createBatches( + batches: [MessageBatch], + anthropicHeaderProvider: AnthropicHeaderProvider, + authenticationHeaderProvider: AuthenticationHeaderProvider + ) async throws -> BatchResponse { + let client = APIClient( + session: session, + anthropicHeaderProvider: anthropicHeaderProvider, + authenticationHeaderProvider: authenticationHeaderProvider + ) + + let request = MessageBatchesRequest(body: .init(from: batches)) + let (data, response) = try await client.send(request: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw ClientError.cannotHandleURLResponse(response) + } + + guard httpResponse.statusCode == 200 else { + throw AnthropicAPIError(fromHttpStatusCode: httpResponse.statusCode) + } + + return try anthropicJSONDecoder.decode(BatchResponse.self, from: data) + } + + public func retrieve(batchId: String) async throws -> BatchResponse { + try await retrieve( + batchId: batchId, + anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) + ) + } + + public func retrieve( + batchId: String, + anthropicHeaderProvider: AnthropicHeaderProvider, + authenticationHeaderProvider: AuthenticationHeaderProvider + ) async throws -> BatchResponse { + let client = APIClient( + session: session, + anthropicHeaderProvider: anthropicHeaderProvider, + authenticationHeaderProvider: authenticationHeaderProvider + ) + + let request = RetrieveMessageBatchesRequest(batchId: batchId) + let (data, response) = try await client.send(request: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw ClientError.cannotHandleURLResponse(response) + } + + guard httpResponse.statusCode == 200 else { + throw AnthropicAPIError(fromHttpStatusCode: httpResponse.statusCode) + } + + return try anthropicJSONDecoder.decode(BatchResponse.self, from: data) + } + + public func results(of batchId: String) async throws -> [BatchResultResponse] { + try await results( + of: batchId, + anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) + ) + } + + public func results( + of batchId: String, + anthropicHeaderProvider: AnthropicHeaderProvider, + authenticationHeaderProvider: AuthenticationHeaderProvider + ) async throws -> [BatchResultResponse] { + let client = APIClient( + session: session, + anthropicHeaderProvider: anthropicHeaderProvider, + authenticationHeaderProvider: authenticationHeaderProvider + ) + + let request = RetrieveMessageBatchResultsRequest(batchId: batchId) + let (data, response) = try await client.send(request: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw ClientError.cannotHandleURLResponse(response) + } + + guard httpResponse.statusCode == 200 else { + throw AnthropicAPIError(fromHttpStatusCode: httpResponse.statusCode) + } + + return try anthropicJSONDecoder.decode([BatchResultResponse].self, from: data) + } + + public func results(streamOf batchId: String) async throws -> AsyncThrowingStream { + try await results( + streamOf: batchId, + anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) + ) + } + + public func results( + streamOf batchId: String, + anthropicHeaderProvider: AnthropicHeaderProvider, + authenticationHeaderProvider: AuthenticationHeaderProvider + ) async throws -> AsyncThrowingStream { + let client = APIClient( + session: session, + anthropicHeaderProvider: anthropicHeaderProvider, + authenticationHeaderProvider: authenticationHeaderProvider + ) + + let request = RetrieveMessageBatchResultsRequest(batchId: batchId) + let (data, response) = try await client.stream(request: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw ClientError.cannotHandleURLResponse(response) + } + + guard httpResponse.statusCode == 200 else { + throw AnthropicAPIError(fromHttpStatusCode: httpResponse.statusCode) + } + + return AsyncThrowingStream.init { continuation in + let task = Task { + for try await line in data.lines { + guard let data = line.data(using: .utf8) else { + return + } + + continuation.yield(try anthropicJSONDecoder.decode(BatchResultResponse.self, from: data)) + } + continuation.finish() + } + continuation.onTermination = { @Sendable _ in + task.cancel() + } + } + } + + public func list(beforeId: String? = nil, afterId: String? = nil, limit: Int = 20) async throws -> BatchListResponse { + try await list( + beforeId: beforeId, + afterId: afterId, + limit: limit, + anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) + ) + } + + public func list( + beforeId: String?, + afterId: String?, + limit: Int, + anthropicHeaderProvider: AnthropicHeaderProvider, + authenticationHeaderProvider: AuthenticationHeaderProvider + ) async throws -> BatchListResponse { + let client = APIClient( + session: session, + anthropicHeaderProvider: anthropicHeaderProvider, + authenticationHeaderProvider: authenticationHeaderProvider + ) + + let queries: [String: CustomStringConvertible] = { + var queries: [String: CustomStringConvertible] = [ListMessageBatchesRequest.Parameter.limit.rawValue: limit] + if let beforeId { + queries[ListMessageBatchesRequest.Parameter.beforeId.rawValue] = beforeId + } + if let afterId { + queries[ListMessageBatchesRequest.Parameter.afterId.rawValue] = afterId + } + + return queries + }() + + let request = ListMessageBatchesRequest(queries: queries) + let (data, response) = try await client.send(request: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw ClientError.cannotHandleURLResponse(response) + } + + guard httpResponse.statusCode == 200 else { + throw AnthropicAPIError(fromHttpStatusCode: httpResponse.statusCode) + } + + return try anthropicJSONDecoder.decode(BatchListResponse.self, from: data) + } + + public func cancel(batchId: String) async throws -> BatchResponse { + try await cancel( + batchId: batchId, + anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) + ) + } + + public func cancel( + batchId: String, + anthropicHeaderProvider: AnthropicHeaderProvider, + authenticationHeaderProvider: AuthenticationHeaderProvider + ) async throws -> BatchResponse { + let client = APIClient( + session: session, + anthropicHeaderProvider: anthropicHeaderProvider, + authenticationHeaderProvider: authenticationHeaderProvider + ) + + let request = CancelMessageBatchRequest(batchId: batchId) + let (data, response) = try await client.send(request: request) - public func retrieve(batchId: String) {} + guard let httpResponse = response as? HTTPURLResponse else { + throw ClientError.cannotHandleURLResponse(response) + } - public func results(of batchId: String) {} + guard httpResponse.statusCode == 200 else { + throw AnthropicAPIError(fromHttpStatusCode: httpResponse.statusCode) + } - public func list(beforeId: String? = nil, afterId: String? = nil, limit: Int = 20) {} + return try anthropicJSONDecoder.decode(BatchResponse.self, from: data) - public func cancel(batchId: String) {} + } } From f43652532d365e2f1d23987d2e221fa8e325e940 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 18 Oct 2024 14:48:35 +0900 Subject: [PATCH 13/26] add interface for message batches api --- Sources/AnthropicSwiftSDK/Anthropic.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/AnthropicSwiftSDK/Anthropic.swift b/Sources/AnthropicSwiftSDK/Anthropic.swift index ff312d8..1214020 100644 --- a/Sources/AnthropicSwiftSDK/Anthropic.swift +++ b/Sources/AnthropicSwiftSDK/Anthropic.swift @@ -11,10 +11,14 @@ import Foundation public final class Anthropic { /// Messages API Interface public let messages: Messages + + /// MessageBatches API Interface + public let messageBatches: MessageBatches /// Construction of SDK /// - Parameter apiKey: API key to access Anthropic API. public init(apiKey: String) { self.messages = Messages(apiKey: apiKey, session: .shared) + self.messageBatches = MessageBatches(apiKey: apiKey, session: .shared) } } From 914bee6e5ca2a8e369bcf0c91dd8d7458ad75854 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 18 Oct 2024 15:24:17 +0900 Subject: [PATCH 14/26] add empty tests --- .../MessageBatchesTests.swift | 13 +++++++++++++ .../Network/BatchListResponseTests.swift | 13 +++++++++++++ .../Network/BatchResponseTests.swift | 13 +++++++++++++ .../Network/BatchResultResponseTests.swift | 13 +++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 Tests/AnthropicSwiftSDKTests/MessageBatchesTests.swift create mode 100644 Tests/AnthropicSwiftSDKTests/Network/BatchListResponseTests.swift create mode 100644 Tests/AnthropicSwiftSDKTests/Network/BatchResponseTests.swift create mode 100644 Tests/AnthropicSwiftSDKTests/Network/BatchResultResponseTests.swift diff --git a/Tests/AnthropicSwiftSDKTests/MessageBatchesTests.swift b/Tests/AnthropicSwiftSDKTests/MessageBatchesTests.swift new file mode 100644 index 0000000..4f9cbf2 --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/MessageBatchesTests.swift @@ -0,0 +1,13 @@ +// +// MessageBatchesTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/18. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class MessageBatchesTests: XCTestCase { + // TODO: write tests +} diff --git a/Tests/AnthropicSwiftSDKTests/Network/BatchListResponseTests.swift b/Tests/AnthropicSwiftSDKTests/Network/BatchListResponseTests.swift new file mode 100644 index 0000000..988c5af --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/BatchListResponseTests.swift @@ -0,0 +1,13 @@ +// +// BatchListResponseTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/18. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class BatchListResponseTests: XCTestCase { + // TODO: write tests +} diff --git a/Tests/AnthropicSwiftSDKTests/Network/BatchResponseTests.swift b/Tests/AnthropicSwiftSDKTests/Network/BatchResponseTests.swift new file mode 100644 index 0000000..af746bf --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/BatchResponseTests.swift @@ -0,0 +1,13 @@ +// +// BatchResponseTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/18. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class BatchResponseTests: XCTestCase { + // TODO: write tests +} diff --git a/Tests/AnthropicSwiftSDKTests/Network/BatchResultResponseTests.swift b/Tests/AnthropicSwiftSDKTests/Network/BatchResultResponseTests.swift new file mode 100644 index 0000000..a3e3f6e --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/BatchResultResponseTests.swift @@ -0,0 +1,13 @@ +// +// BatchResultResponseTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/18. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class BatchResultResponseTests: XCTestCase { + // TODO: write tests +} From 7de3a5f679403cac1e58f3415d9b992a36f1b9e0 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Wed, 23 Oct 2024 01:43:26 +0900 Subject: [PATCH 15/26] add tests for request object --- Sources/AnthropicSwiftSDK/Anthropic.swift | 2 +- .../Entity/Batch/BatchParameter.swift | 1 - .../Entity/Batch/MessageBatch.swift | 2 +- .../AnthropicSwiftSDK/MessageBatches.swift | 21 ++-- .../Request/ListMessageBatchesRequest.swift | 2 +- .../Request/MessageBatchesRequest.swift | 4 +- .../Network/Request/MessagesRequest.swift | 2 +- .../CancelMessageBatchRequestTests.swift | 24 ++++ .../ListMessageBatchesRequestTests.swift | 34 ++++++ .../Request/MessageBatchesRequestTests.swift | 61 ++++++++++ .../Request/MessagesRequestTests.swift | 109 ++++++++++++++++++ .../Network/Request/RequestTests.swift | 63 ++++++++++ ...rieveMessageBatchResultsRequestTests.swift | 23 ++++ .../RetrieveMessageBatchesRequestTests.swift | 23 ++++ 14 files changed, 353 insertions(+), 18 deletions(-) create mode 100644 Tests/AnthropicSwiftSDKTests/Network/Request/CancelMessageBatchRequestTests.swift create mode 100644 Tests/AnthropicSwiftSDKTests/Network/Request/ListMessageBatchesRequestTests.swift create mode 100644 Tests/AnthropicSwiftSDKTests/Network/Request/MessageBatchesRequestTests.swift create mode 100644 Tests/AnthropicSwiftSDKTests/Network/Request/MessagesRequestTests.swift create mode 100644 Tests/AnthropicSwiftSDKTests/Network/Request/RequestTests.swift create mode 100644 Tests/AnthropicSwiftSDKTests/Network/Request/RetrieveMessageBatchResultsRequestTests.swift create mode 100644 Tests/AnthropicSwiftSDKTests/Network/Request/RetrieveMessageBatchesRequestTests.swift diff --git a/Sources/AnthropicSwiftSDK/Anthropic.swift b/Sources/AnthropicSwiftSDK/Anthropic.swift index 1214020..6d35b5c 100644 --- a/Sources/AnthropicSwiftSDK/Anthropic.swift +++ b/Sources/AnthropicSwiftSDK/Anthropic.swift @@ -11,7 +11,7 @@ import Foundation public final class Anthropic { /// Messages API Interface public let messages: Messages - + /// MessageBatches API Interface public let messageBatches: MessageBatches diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift index 36d1fa5..7f0ee38 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift @@ -53,4 +53,3 @@ public struct BatchParameter { self.toolChoice = toolChoice } } - diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift index 22fec77..328fd25 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift @@ -8,7 +8,7 @@ public struct MessageBatch { public let customId: String public let parameter: BatchParameter - + public init(customId: String, parameter: BatchParameter) { self.customId = customId self.parameter = parameter diff --git a/Sources/AnthropicSwiftSDK/MessageBatches.swift b/Sources/AnthropicSwiftSDK/MessageBatches.swift index d560db5..47f48f1 100644 --- a/Sources/AnthropicSwiftSDK/MessageBatches.swift +++ b/Sources/AnthropicSwiftSDK/MessageBatches.swift @@ -24,7 +24,7 @@ public struct MessageBatches { authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) ) } - + public func createBatches( batches: [MessageBatch], anthropicHeaderProvider: AnthropicHeaderProvider, @@ -57,7 +57,7 @@ public struct MessageBatches { authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) ) } - + public func retrieve( batchId: String, anthropicHeaderProvider: AnthropicHeaderProvider, @@ -90,7 +90,7 @@ public struct MessageBatches { authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) ) } - + public func results( of batchId: String, anthropicHeaderProvider: AnthropicHeaderProvider, @@ -115,7 +115,7 @@ public struct MessageBatches { return try anthropicJSONDecoder.decode([BatchResultResponse].self, from: data) } - + public func results(streamOf batchId: String) async throws -> AsyncThrowingStream { try await results( streamOf: batchId, @@ -123,7 +123,7 @@ public struct MessageBatches { authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) ) } - + public func results( streamOf batchId: String, anthropicHeaderProvider: AnthropicHeaderProvider, @@ -145,14 +145,14 @@ public struct MessageBatches { guard httpResponse.statusCode == 200 else { throw AnthropicAPIError(fromHttpStatusCode: httpResponse.statusCode) } - + return AsyncThrowingStream.init { continuation in let task = Task { for try await line in data.lines { guard let data = line.data(using: .utf8) else { return } - + continuation.yield(try anthropicJSONDecoder.decode(BatchResultResponse.self, from: data)) } continuation.finish() @@ -172,7 +172,7 @@ public struct MessageBatches { authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) ) } - + public func list( beforeId: String?, afterId: String?, @@ -194,7 +194,7 @@ public struct MessageBatches { if let afterId { queries[ListMessageBatchesRequest.Parameter.afterId.rawValue] = afterId } - + return queries }() @@ -219,7 +219,7 @@ public struct MessageBatches { authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) ) } - + public func cancel( batchId: String, anthropicHeaderProvider: AnthropicHeaderProvider, @@ -243,6 +243,5 @@ public struct MessageBatches { } return try anthropicJSONDecoder.decode(BatchResponse.self, from: data) - } } diff --git a/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift index 0488e1f..0a85489 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift @@ -10,7 +10,7 @@ struct ListMessageBatchesRequest: Request { let path: String = RequestType.batches.basePath let queries: [String: CustomStringConvertible]? let body: Never? = nil - + enum Parameter: String { case beforeId = "before_id" case afterId = "after_id" diff --git a/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift index e57f2bd..87e7be1 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift @@ -20,7 +20,7 @@ struct MessageBatchesRequest: Request { struct MessageBatchesRequestBody: Encodable { /// List of requests for prompt completion. Each is an individual request to create a Message. let requests: [Batch] - + init(from batches: [MessageBatch]) { self.requests = batches.map { Batch(from: $0) } } @@ -36,7 +36,7 @@ struct Batch: Encodable { /// /// See the [Messages API reference](https://docs.anthropic.com/en/api/messages) for full documentation on available parameters. let params: MessagesRequestBody - + init(from batch: MessageBatch) { self.customId = batch.customId self.params = MessagesRequestBody(from: batch.parameter) diff --git a/Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift index 7aeb9d3..012a054 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/MessagesRequest.swift @@ -91,7 +91,7 @@ struct MessagesRequestBody: Encodable { self.tools = tools self.toolChoice = tools == nil ? nil : toolChoice // ToolChoice should be set if tools are specified. } - + init(from parameter: BatchParameter) { self.model = parameter.model self.messages = parameter.messages diff --git a/Tests/AnthropicSwiftSDKTests/Network/Request/CancelMessageBatchRequestTests.swift b/Tests/AnthropicSwiftSDKTests/Network/Request/CancelMessageBatchRequestTests.swift new file mode 100644 index 0000000..66c1579 --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/Request/CancelMessageBatchRequestTests.swift @@ -0,0 +1,24 @@ +// +// CancelMessageBatchRequestTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/23. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class CancelMessageBatchRequestTests: XCTestCase { + + func testCancelMessageBatchRequest() { + let testBatchId = "test_batch_123" + + let request = CancelMessageBatchRequest(batchId: testBatchId) + + XCTAssertEqual(request.method, .post) + XCTAssertEqual(request.path, "\(RequestType.batches.basePath)/\(testBatchId)/cancel") + XCTAssertNil(request.queries) + XCTAssertNil(request.body) + XCTAssertEqual(request.batchId, testBatchId) + } +} diff --git a/Tests/AnthropicSwiftSDKTests/Network/Request/ListMessageBatchesRequestTests.swift b/Tests/AnthropicSwiftSDKTests/Network/Request/ListMessageBatchesRequestTests.swift new file mode 100644 index 0000000..e0f8018 --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/Request/ListMessageBatchesRequestTests.swift @@ -0,0 +1,34 @@ +// +// ListMessageBatchesRequestTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/23. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class ListMessageBatchesRequestTests: XCTestCase { + + func testListMessageBatchesRequestProperties() { + let queries: [String: CustomStringConvertible] = [ + "before_id": "batch123", + "after_id": "batch456", + "limit": 10 + ] + let request = ListMessageBatchesRequest(queries: queries) + + XCTAssertEqual(request.method, .get) + XCTAssertEqual(request.path, RequestType.batches.basePath) + XCTAssertEqual(request.queries?["before_id"] as? String, "batch123") + XCTAssertEqual(request.queries?["after_id"] as? String, "batch456") + XCTAssertEqual(request.queries?["limit"] as? Int, 10) + XCTAssertNil(request.body) + } + + func testParameterRawValues() { + XCTAssertEqual(ListMessageBatchesRequest.Parameter.beforeId.rawValue, "before_id") + XCTAssertEqual(ListMessageBatchesRequest.Parameter.afterId.rawValue, "after_id") + XCTAssertEqual(ListMessageBatchesRequest.Parameter.limit.rawValue, "limit") + } +} diff --git a/Tests/AnthropicSwiftSDKTests/Network/Request/MessageBatchesRequestTests.swift b/Tests/AnthropicSwiftSDKTests/Network/Request/MessageBatchesRequestTests.swift new file mode 100644 index 0000000..e4ff3fe --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/Request/MessageBatchesRequestTests.swift @@ -0,0 +1,61 @@ +// +// MessageBatchesRequestTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/22. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class MessageBatchesRequestTests: XCTestCase { + + func testMessageBatchesRequest() throws { + let batchParameter1 = BatchParameter( + messages: [Message(role: .user, content: [.text("こんにちは")])], + model: .claude_3_Opus, + maxTokens: 100 + ) + let batchParameter2 = BatchParameter( + messages: [Message(role: .user, content: [.text("お元気ですか?")])], + model: .claude_3_Sonnet, + maxTokens: 200 + ) + + let messageBatch1 = MessageBatch(customId: "test1", parameter: batchParameter1) + let messageBatch2 = MessageBatch(customId: "test2", parameter: batchParameter2) + + let request = MessageBatchesRequest(body: .init(from: [messageBatch1, messageBatch2])) + + XCTAssertEqual(request.method, .post) + XCTAssertEqual(request.path, RequestType.batches.basePath) + XCTAssertNil(request.queries) + XCTAssertNotNil(request.body) + + XCTAssertEqual(request.body?.requests.count, 2) + + XCTAssertEqual(request.body?.requests[0].customId, "test1") + XCTAssertEqual(request.body?.requests[0].params.model.stringfy, Model.claude_3_Opus.stringfy) + XCTAssertEqual(request.body?.requests[0].params.maxTokens, 100) + XCTAssertEqual(request.body?.requests[0].params.messages.count, 1) + XCTAssertEqual(request.body?.requests[0].params.messages[0].role.rawValue, "user") + let content1 = try XCTUnwrap(request.body?.requests[0].params.messages[0].content) + guard case let .text(text1) = content1[0] else { + XCTFail("content1[0] is not .text") + return + } + XCTAssertEqual(text1, "こんにちは") + + XCTAssertEqual(request.body?.requests[1].customId, "test2") + XCTAssertEqual(request.body?.requests[1].params.model.stringfy, Model.claude_3_Sonnet.stringfy) + XCTAssertEqual(request.body?.requests[1].params.maxTokens, 200) + XCTAssertEqual(request.body?.requests[1].params.messages.count, 1) + XCTAssertEqual(request.body?.requests[1].params.messages[0].role.rawValue, "user") + let content2 = try XCTUnwrap(request.body?.requests[1].params.messages[0].content) + guard case let .text(text2) = content2[0] else { + XCTFail("content2[0] is not .text") + return + } + XCTAssertEqual(text2, "お元気ですか?") + } +} \ No newline at end of file diff --git a/Tests/AnthropicSwiftSDKTests/Network/Request/MessagesRequestTests.swift b/Tests/AnthropicSwiftSDKTests/Network/Request/MessagesRequestTests.swift new file mode 100644 index 0000000..7e1e613 --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/Request/MessagesRequestTests.swift @@ -0,0 +1,109 @@ +// +// MessagesRequestTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/22. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class MessagesRequestTests: XCTestCase { + func testMessagesRequest() throws { + let messages = [Message(role: .user, content: [.text("こんにちは")])] + let systemPrompt: [SystemPrompt] = [.text("あなたは親切なアシスタントです。", .ephemeral)] + + let requestBody = MessagesRequestBody( + model: .claude_3_Opus, + messages: messages, + system: systemPrompt, + maxTokens: 100, + metaData: MetaData(userId: "test-user"), + stopSequences: ["END"], + stream: false, + temperature: 0.7, + topP: 0.9, + topK: 10, + tools: nil, + toolChoice: .auto + ) + + let request = MessagesRequest(body: requestBody) + + XCTAssertEqual(request.method, .post) + XCTAssertEqual(request.path, RequestType.messages.basePath) + XCTAssertNil(request.queries) + + XCTAssertEqual(request.body?.model.stringfy, Model.claude_3_Opus.stringfy) + XCTAssertEqual(request.body?.messages.count, 1) + XCTAssertEqual(request.body?.messages[0].role.rawValue, "user") + let content1 = try XCTUnwrap(request.body?.messages[0].content) + guard case let .text(text1) = content1[0] else { + XCTFail("content1[0] is not .text") + return + } + XCTAssertEqual(text1, "こんにちは") + let system1 = try XCTUnwrap(request.body?.system) + guard case let .text(text1, _) = system1[0] else { + XCTFail("system1[0] is not .text") + return + } + XCTAssertEqual(text1, "あなたは親切なアシスタントです。") + XCTAssertEqual(request.body?.maxTokens, 100) + XCTAssertEqual(request.body?.metaData?.userId, "test-user") + XCTAssertEqual(request.body?.stopSequences, ["END"]) + XCTAssertFalse(request.body?.stream ?? true) + XCTAssertEqual(request.body?.temperature, 0.7) + XCTAssertEqual(request.body?.topP, 0.9) + XCTAssertEqual(request.body?.topK, 10) + XCTAssertNil(request.body?.tools) + XCTAssertNil(request.body?.toolChoice) + } + + func testMessagesRequestWithBatchParameter() throws { + let messages = [Message(role: .user, content: [.text("こんにちは")])] + let systemPrompt: [SystemPrompt] = [.text("あなたは親切なアシスタントです。", .ephemeral)] + + let batchParameter = BatchParameter( + messages: messages, + model: .claude_3_Opus, + system: systemPrompt, + maxTokens: 100, + metaData: MetaData(userId: "test-user"), + stopSequence: ["END"], + temperature: 0.7, + topP: 0.9, + topK: 10, + toolContainer: nil, + toolChoice: .auto + ) + + let requestBody = MessagesRequestBody(from: batchParameter) + let request = MessagesRequest(body: requestBody) + + XCTAssertEqual(request.body?.model.stringfy, Model.claude_3_Opus.stringfy) + XCTAssertEqual(request.body?.messages.count, 1) + XCTAssertEqual(request.body?.messages[0].role.rawValue, "user") + let content1 = try XCTUnwrap(request.body?.messages[0].content) + guard case let .text(text1) = content1[0] else { + XCTFail("content1[0] is not .text") + return + } + XCTAssertEqual(text1, "こんにちは") + let system1 = try XCTUnwrap(request.body?.system) + guard case let .text(text1, _) = system1[0] else { + XCTFail("system1[0] is not .text") + return + } + XCTAssertEqual(text1, "あなたは親切なアシスタントです。") + XCTAssertEqual(request.body?.maxTokens, 100) + XCTAssertEqual(request.body?.metaData?.userId, "test-user") + XCTAssertEqual(request.body?.stopSequences, ["END"]) + XCTAssertFalse(request.body?.stream ?? true) + XCTAssertEqual(request.body?.temperature, 0.7) + XCTAssertEqual(request.body?.topP, 0.9) + XCTAssertEqual(request.body?.topK, 10) + XCTAssertNil(request.body?.tools) + XCTAssertNil(request.body?.toolChoice) + } +} \ No newline at end of file diff --git a/Tests/AnthropicSwiftSDKTests/Network/Request/RequestTests.swift b/Tests/AnthropicSwiftSDKTests/Network/Request/RequestTests.swift new file mode 100644 index 0000000..bbc489b --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/Request/RequestTests.swift @@ -0,0 +1,63 @@ +// +// RequestTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/23. +// + +import XCTest +@testable import AnthropicSwiftSDK + +class RequestTests: XCTestCase { + + struct MockRequest: Request { + typealias Body = [String: String] + + var method: HttpMethod = .post + var path: String = "/test" + var queries: [String: CustomStringConvertible]? = ["key": "value"] + var body: Body? = ["test": "data"] + } + + func testToURLRequest() throws { + let baseURL = URL(string: "https://api.anthropic.com")! + let mockRequest = MockRequest() + + let urlRequest = try mockRequest.toURLRequest(for: baseURL) + + XCTAssertEqual(urlRequest.url?.absoluteString, "https://api.anthropic.com/test?key=value") + XCTAssertEqual(urlRequest.httpMethod, "POST") + + let bodyData = try XCTUnwrap(urlRequest.httpBody) + let decodedBody = try JSONDecoder().decode([String: String].self, from: bodyData) + XCTAssertEqual(decodedBody, ["test": "data"]) + } + + func testToURLRequestWithoutBody() throws { + let baseURL = URL(string: "https://api.anthropic.com")! + var mockRequest = MockRequest() + mockRequest.body = nil + mockRequest.method = .get + + let urlRequest = try mockRequest.toURLRequest(for: baseURL) + + XCTAssertEqual(urlRequest.url?.absoluteString, "https://api.anthropic.com/test?key=value") + XCTAssertEqual(urlRequest.httpMethod, "GET") + XCTAssertNil(urlRequest.httpBody) + } + + func testToURLRequestWithoutQueries() throws { + let baseURL = URL(string: "https://api.anthropic.com")! + var mockRequest = MockRequest() + mockRequest.queries = nil + + let urlRequest = try mockRequest.toURLRequest(for: baseURL) + + XCTAssertEqual(urlRequest.url?.absoluteString, "https://api.anthropic.com/test") + XCTAssertEqual(urlRequest.httpMethod, "POST") + + let bodyData = try XCTUnwrap(urlRequest.httpBody) + let decodedBody = try JSONDecoder().decode([String: String].self, from: bodyData) + XCTAssertEqual(decodedBody, ["test": "data"]) + } +} diff --git a/Tests/AnthropicSwiftSDKTests/Network/Request/RetrieveMessageBatchResultsRequestTests.swift b/Tests/AnthropicSwiftSDKTests/Network/Request/RetrieveMessageBatchResultsRequestTests.swift new file mode 100644 index 0000000..ea031d6 --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/Request/RetrieveMessageBatchResultsRequestTests.swift @@ -0,0 +1,23 @@ +// +// RetrieveMessageBatchResultsRequestTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/22. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class RetrieveMessageBatchResultsRequestTests: XCTestCase { + + func testRetrieveMessageBatchResultsRequest() { + let batchId = "test_batch_123" + let request = RetrieveMessageBatchResultsRequest(batchId: batchId) + + XCTAssertEqual(request.method, .get) + XCTAssertEqual(request.path, "\(RequestType.batches.basePath)/\(batchId)/results") + XCTAssertNil(request.queries) + XCTAssertNil(request.body) + XCTAssertEqual(request.batchId, batchId) + } +} \ No newline at end of file diff --git a/Tests/AnthropicSwiftSDKTests/Network/Request/RetrieveMessageBatchesRequestTests.swift b/Tests/AnthropicSwiftSDKTests/Network/Request/RetrieveMessageBatchesRequestTests.swift new file mode 100644 index 0000000..1c7c29f --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/Request/RetrieveMessageBatchesRequestTests.swift @@ -0,0 +1,23 @@ +// +// RetrieveMessageBatchesRequestTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/10/22. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class RetrieveMessageBatchesRequestTests: XCTestCase { + + func testRetrieveMessageBatchesRequest() { + let testBatchId = "test_batch_123" + let request = RetrieveMessageBatchesRequest(batchId: testBatchId) + + XCTAssertEqual(request.method, .get) + XCTAssertEqual(request.path, "\(RequestType.batches.basePath)/\(testBatchId)") + XCTAssertNil(request.queries) + XCTAssertNil(request.body) + XCTAssertEqual(request.batchId, testBatchId) + } +} \ No newline at end of file From fcf5db34bb631efda5a2416a655780c07bea3a31 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Wed, 23 Oct 2024 13:16:34 +0900 Subject: [PATCH 16/26] add tests for response object --- .../HTTPMock.swift | 67 ++++++++------ .../Entity/Batch/BatchResultType.swift | 2 +- .../Network/Response/BatchResponse.swift | 2 +- .../Response/BatchResultResponse.swift | 4 +- .../MessagesTests.swift | 2 +- .../Network/AnthropicAPIClientTests.swift | 8 +- .../Network/BatchListResponseTests.swift | 90 ++++++++++++++++++- .../Network/BatchResponseTests.swift | 90 ++++++++++++++++++- .../Network/BatchResultResponseTests.swift | 78 +++++++++++++++- 9 files changed, 302 insertions(+), 41 deletions(-) diff --git a/Sources/AnthropicSwiftSDK-TestUtils/HTTPMock.swift b/Sources/AnthropicSwiftSDK-TestUtils/HTTPMock.swift index 15b9ab9..ed76400 100644 --- a/Sources/AnthropicSwiftSDK-TestUtils/HTTPMock.swift +++ b/Sources/AnthropicSwiftSDK-TestUtils/HTTPMock.swift @@ -9,8 +9,8 @@ import Foundation public enum MockInspectType { case none - case request((URLRequest) -> Void) - case requestHeader(([String: String]?) -> Void) + case request((URLRequest) -> Void, String?) + case requestHeader(([String: String]?) -> Void, String?) case response(String) } @@ -26,11 +26,11 @@ public class HTTPMock: URLProtocol { } public override func startLoading() { - if case let .request(inspection) = Self.inspectType { + if case let .request(inspection, _) = Self.inspectType { inspection(request) } - if case let .requestHeader(inspection) = Self.inspectType { + if case let .requestHeader(inspection, _) = Self.inspectType { inspection(request.allHTTPHeaderFields) } @@ -38,31 +38,20 @@ public class HTTPMock: URLProtocol { let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/2", headerFields: nil) { client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - if case let .response(jsonString) = Self.inspectType, - let data = jsonString.data(using: .utf8) { + switch Self.inspectType { + case .none: + client?.urlProtocol(self, didLoad: getBasicJSONStringData()) + case .request(_, let jsonString), .requestHeader(_, let jsonString): + guard let jsonString, let data = jsonString.data(using: .utf8) else { + client?.urlProtocol(self, didLoad: getBasicJSONStringData()) + return + } client?.urlProtocol(self, didLoad: data) - } else { - let basicResponse = """ - { - "id": "msg_01XFDUDYJgAACzvnptvVoYEL", - "type": "message", - "role": "assistant", - "content": [ - { - "type": "text", - "text": "Hello!" - } - ], - "model": "claude-2.1", - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 12, - "output_tokens": 6 - } + case .response(let jsonString): + guard let data = jsonString.data(using: .utf8) else { + client?.urlProtocol(self, didLoad: getBasicJSONStringData()) + return } - """ - let data = basicResponse.data(using: .utf8)! client?.urlProtocol(self, didLoad: data) } } @@ -70,6 +59,30 @@ public class HTTPMock: URLProtocol { client?.urlProtocolDidFinishLoading(self) } + private func getBasicJSONStringData() -> Data { + let basicResponse = """ + { + "id": "msg_01XFDUDYJgAACzvnptvVoYEL", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Hello!" + } + ], + "model": "claude-2.1", + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 12, + "output_tokens": 6 + } + } + """ + return basicResponse.data(using: .utf8)! + } + public override func stopLoading() { } } diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift index f101c68..a3f7a63 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift @@ -6,7 +6,7 @@ // /// https://docs.anthropic.com/en/docs/build-with-claude/message-batches#retrieving-batch-results -public enum BatchResultType: Decodable { +public enum BatchResultType: String, Decodable { case succeeded // include the message result as jsonl case errored case cancelled diff --git a/Sources/AnthropicSwiftSDK/Network/Response/BatchResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/BatchResponse.swift index 3527ad6..18ca163 100644 --- a/Sources/AnthropicSwiftSDK/Network/Response/BatchResponse.swift +++ b/Sources/AnthropicSwiftSDK/Network/Response/BatchResponse.swift @@ -34,5 +34,5 @@ public struct BatchResponse: Decodable { /// URL to a .jsonl file containing the results of the Message Batch requests. Specified only once processing ends. /// /// Results in the file are not guaranteed to be in the same order as requests. Use the custom_id field to match results to requests. - public let resultsURL: String? + public let resultsUrl: String? } diff --git a/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift index a5d9cff..ecda647 100644 --- a/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift +++ b/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift @@ -7,11 +7,11 @@ public struct BatchResult: Decodable { public let type: BatchResultType - public let message: MessagesResponse + public let message: MessagesResponse? + public let error: StreamingError? } public struct BatchResultResponse: Decodable { public let customId: String public let result: BatchResult? - public let error: StreamingError? } diff --git a/Tests/AnthropicSwiftSDKTests/MessagesTests.swift b/Tests/AnthropicSwiftSDKTests/MessagesTests.swift index 2809022..bfa246d 100644 --- a/Tests/AnthropicSwiftSDKTests/MessagesTests.swift +++ b/Tests/AnthropicSwiftSDKTests/MessagesTests.swift @@ -26,7 +26,7 @@ final class MessagesTests: XCTestCase { HTTPMock.inspectType = .requestHeader({ headers in XCTAssertEqual(headers!["x-api-key"], "This-is-test-API-key") expectation.fulfill() - }) + }, nil) let message = Message(role: .user, content: [.text("This is test text")]) let _ = try await messages.createMessage([message], maxTokens: 1024) diff --git a/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift b/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift index c4ea56e..9b20968 100644 --- a/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift +++ b/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift @@ -32,7 +32,7 @@ final class AnthropicAPIClientTests: XCTestCase { XCTAssertEqual(request.httpMethod, "POST") expectation.fulfill() - }) + }, nil) let _ = try await client.send(request: NopRequest()) await fulfillment(of: [expectation], timeout: 1.0) @@ -51,7 +51,7 @@ final class AnthropicAPIClientTests: XCTestCase { XCTAssertEqual(request.httpMethod, "POST") expectation.fulfill() - }) + }, nil) let _ = try await client.stream(request: NopRequest()) await fulfillment(of: [expectation], timeout: 1.0) @@ -72,7 +72,7 @@ final class AnthropicAPIClientTests: XCTestCase { XCTAssertEqual(headers!["anthropic-beta"], "message-batches-2024-09-24") expectation.fulfill() - }) + }, nil) let _ = try await client.send(request: NopRequest()) await fulfillment(of: [expectation], timeout: 1.0) @@ -93,7 +93,7 @@ final class AnthropicAPIClientTests: XCTestCase { XCTAssertEqual(headers!["anthropic-beta"], "message-batches-2024-09-24") expectation.fulfill() - }) + }, nil) let _ = try await client.stream(request: NopRequest()) await fulfillment(of: [expectation], timeout: 1.0) diff --git a/Tests/AnthropicSwiftSDKTests/Network/BatchListResponseTests.swift b/Tests/AnthropicSwiftSDKTests/Network/BatchListResponseTests.swift index 988c5af..3d024f4 100644 --- a/Tests/AnthropicSwiftSDKTests/Network/BatchListResponseTests.swift +++ b/Tests/AnthropicSwiftSDKTests/Network/BatchListResponseTests.swift @@ -9,5 +9,91 @@ import XCTest @testable import AnthropicSwiftSDK final class BatchListResponseTests: XCTestCase { - // TODO: write tests -} + func testDecodeBatchListResponse() throws { + let json = """ + { + "data": [ + { + "id": "batch_123", + "type": "message_batch", + "processing_status": "ended", + "request_counts": { + "processing": 0, + "succeeeded": 95, + "errored": 5, + "canceled": 0, + "expired": 0 + }, + "ended_at": "2024-10-18T12:05:00Z", + "created_at": "2024-10-18T12:00:00Z", + "expires_at": "2024-10-19T12:00:00Z", + "cancel_initiated_at": null, + "results_url": "https://example.com/results/batch_123.jsonl" + }, + { + "id": "batch_456", + "type": "message_batch", + "processing_status": "in_progress", + "request_counts": { + "processing": 100, + "succeeeded": 0, + "errored": 0, + "canceled": 0, + "expired": 0 + }, + "ended_at": null, + "created_at": "2024-10-18T12:10:00Z", + "expires_at": "2024-10-19T12:10:00Z", + "cancel_initiated_at": null, + "results_url": null + } + ], + "has_more": true, + "first_id": "batch_123", + "last_id": "batch_456" + } + """ + + let jsonData = json.data(using: .utf8)! + + let response = try anthropicJSONDecoder.decode(BatchListResponse.self, from: jsonData) + + XCTAssertEqual(response.data.count, 2) + + // Test first batch + let firstBatch = response.data[0] + XCTAssertEqual(firstBatch.id, "batch_123") + XCTAssertEqual(firstBatch.type, .message) + XCTAssertEqual(firstBatch.processingStatus, .ended) + XCTAssertEqual(firstBatch.requestCounts.processing, 0) + XCTAssertEqual(firstBatch.requestCounts.succeeeded, 95) + XCTAssertEqual(firstBatch.requestCounts.errored, 5) + XCTAssertEqual(firstBatch.requestCounts.canceled, 0) + XCTAssertEqual(firstBatch.requestCounts.expired, 0) + XCTAssertEqual(firstBatch.endedAt, "2024-10-18T12:05:00Z") + XCTAssertEqual(firstBatch.createdAt, "2024-10-18T12:00:00Z") + XCTAssertEqual(firstBatch.expiresAt, "2024-10-19T12:00:00Z") + XCTAssertNil(firstBatch.cancelInitiatedAt) + XCTAssertEqual(firstBatch.resultsUrl, "https://example.com/results/batch_123.jsonl") + + // Test second batch + let secondBatch = response.data[1] + XCTAssertEqual(secondBatch.id, "batch_456") + XCTAssertEqual(secondBatch.type, .message) + XCTAssertEqual(secondBatch.processingStatus, .inProgress) + XCTAssertEqual(secondBatch.requestCounts.processing, 100) + XCTAssertEqual(secondBatch.requestCounts.succeeeded, 0) + XCTAssertEqual(secondBatch.requestCounts.errored, 0) + XCTAssertEqual(secondBatch.requestCounts.canceled, 0) + XCTAssertEqual(secondBatch.requestCounts.expired, 0) + XCTAssertNil(secondBatch.endedAt) + XCTAssertEqual(secondBatch.createdAt, "2024-10-18T12:10:00Z") + XCTAssertEqual(secondBatch.expiresAt, "2024-10-19T12:10:00Z") + XCTAssertNil(secondBatch.cancelInitiatedAt) + XCTAssertNil(secondBatch.resultsUrl) + + XCTAssertTrue(response.hasMore) + XCTAssertEqual(response.firstId, "batch_123") + XCTAssertEqual(response.lastId, "batch_456") + } +} \ No newline at end of file diff --git a/Tests/AnthropicSwiftSDKTests/Network/BatchResponseTests.swift b/Tests/AnthropicSwiftSDKTests/Network/BatchResponseTests.swift index af746bf..49dcb79 100644 --- a/Tests/AnthropicSwiftSDKTests/Network/BatchResponseTests.swift +++ b/Tests/AnthropicSwiftSDKTests/Network/BatchResponseTests.swift @@ -9,5 +9,91 @@ import XCTest @testable import AnthropicSwiftSDK final class BatchResponseTests: XCTestCase { - // TODO: write tests -} + func testDecodeBatchResponse() throws { + let json = """ + { + "id": "batch_123456", + "type": "message_batch", + "processing_status": "ended", + "request_counts": { + "processing": 0, + "succeeeded": 95, + "errored": 3, + "canceled": 1, + "expired": 1 + }, + "ended_at": "2024-10-18T15:30:00Z", + "created_at": "2024-10-18T15:00:00Z", + "expires_at": "2024-10-19T15:00:00Z", + "cancel_initiated_at": null, + "results_url": "https://example.com/results/batch_123456.jsonl" + } + """ + + let jsonData = json.data(using: .utf8)! + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let batchResponse = try decoder.decode(BatchResponse.self, from: jsonData) + + XCTAssertEqual(batchResponse.id, "batch_123456") + XCTAssertEqual(batchResponse.type, .message) + XCTAssertEqual(batchResponse.processingStatus, .ended) + + XCTAssertEqual(batchResponse.requestCounts.processing, 0) + XCTAssertEqual(batchResponse.requestCounts.succeeeded, 95) + XCTAssertEqual(batchResponse.requestCounts.errored, 3) + XCTAssertEqual(batchResponse.requestCounts.canceled, 1) + XCTAssertEqual(batchResponse.requestCounts.expired, 1) + + XCTAssertEqual(batchResponse.endedAt, "2024-10-18T15:30:00Z") + XCTAssertEqual(batchResponse.createdAt, "2024-10-18T15:00:00Z") + XCTAssertEqual(batchResponse.expiresAt, "2024-10-19T15:00:00Z") + XCTAssertNil(batchResponse.cancelInitiatedAt) + XCTAssertEqual(batchResponse.resultsUrl, "https://example.com/results/batch_123456.jsonl") + } + + func testDecodeBatchResponseWithCancellation() throws { + let json = """ + { + "id": "batch_789012", + "type": "message_batch", + "processing_status": "canceling", + "request_counts": { + "processing": 50, + "succeeeded": 40, + "errored": 10, + "canceled": 0, + "expired": 0 + }, + "ended_at": null, + "created_at": "2024-10-18T16:00:00Z", + "expires_at": "2024-10-19T16:00:00Z", + "cancel_initiated_at": "2024-10-18T16:30:00Z", + "results_url": null + } + """ + + let jsonData = json.data(using: .utf8)! + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let batchResponse = try decoder.decode(BatchResponse.self, from: jsonData) + + XCTAssertEqual(batchResponse.id, "batch_789012") + XCTAssertEqual(batchResponse.type, .message) + XCTAssertEqual(batchResponse.processingStatus, .canceling) + + XCTAssertEqual(batchResponse.requestCounts.processing, 50) + XCTAssertEqual(batchResponse.requestCounts.succeeeded, 40) + XCTAssertEqual(batchResponse.requestCounts.errored, 10) + XCTAssertEqual(batchResponse.requestCounts.canceled, 0) + XCTAssertEqual(batchResponse.requestCounts.expired, 0) + + XCTAssertNil(batchResponse.endedAt) + XCTAssertEqual(batchResponse.createdAt, "2024-10-18T16:00:00Z") + XCTAssertEqual(batchResponse.expiresAt, "2024-10-19T16:00:00Z") + XCTAssertEqual(batchResponse.cancelInitiatedAt, "2024-10-18T16:30:00Z") + XCTAssertNil(batchResponse.resultsUrl) + } +} \ No newline at end of file diff --git a/Tests/AnthropicSwiftSDKTests/Network/BatchResultResponseTests.swift b/Tests/AnthropicSwiftSDKTests/Network/BatchResultResponseTests.swift index a3e3f6e..682d2df 100644 --- a/Tests/AnthropicSwiftSDKTests/Network/BatchResultResponseTests.swift +++ b/Tests/AnthropicSwiftSDKTests/Network/BatchResultResponseTests.swift @@ -9,5 +9,81 @@ import XCTest @testable import AnthropicSwiftSDK final class BatchResultResponseTests: XCTestCase { - // TODO: write tests + func testDecodeBatchResultResponseSucceeded() throws { + let json = """ + {"custom_id":"request_123","result":{"type":"succeeded","message":{"id":"msg_123456","type":"message","role":"assistant","model":"claude-3-5-sonnet-20240620","content":[{"type":"text","text":"Hello again! It's nice to see you. How can I assist you today? Is there anything specific you'd like to chat about or any questions you have?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":11,"output_tokens":36}}}} + """ + + let jsonData = json.data(using: .utf8)! + let batchResultResponse = try anthropicJSONDecoder.decode(BatchResultResponse.self, from: jsonData) + + XCTAssertEqual(batchResultResponse.customId, "request_123") + XCTAssertNotNil(batchResultResponse.result) + XCTAssertNil(batchResultResponse.result?.error) + + XCTAssertEqual(batchResultResponse.result?.type, .succeeded) + XCTAssertEqual(batchResultResponse.result?.message?.id, "msg_123456") + XCTAssertEqual(batchResultResponse.result?.message?.type, .message) + XCTAssertEqual(batchResultResponse.result?.message?.role, .assistant) + guard case let .text(text1) = batchResultResponse.result?.message?.content.first else { + XCTFail("batchResultResponse.result?.message.content.first is not .text") + return + } + XCTAssertEqual(text1, "Hello again! It's nice to see you. How can I assist you today? Is there anything specific you'd like to chat about or any questions you have?") + XCTAssertEqual(batchResultResponse.result?.message?.model?.stringfy, Model.claude_3_5_Sonnet.stringfy) + XCTAssertEqual(batchResultResponse.result?.message?.stopReason, .endTurn) + XCTAssertNil(batchResultResponse.result?.message?.stopSequence) + XCTAssertEqual(batchResultResponse.result?.message?.usage.inputTokens, 11) + XCTAssertEqual(batchResultResponse.result?.message?.usage.outputTokens, 36) + } + + func testDecodeBatchResultResponseErrored() throws { + let json = """ + { + "custom_id": "request_456", + "result": { + "type": "errored", + "error": { + "type": "invalid_request_error", + "message": "Invalid input: The provided text contains inappropriate content." + } + } + } + """ + + let jsonData = json.data(using: .utf8)! + let batchResultResponse = try anthropicJSONDecoder.decode(BatchResultResponse.self, from: jsonData) + + XCTAssertEqual(batchResultResponse.customId, "request_456") + XCTAssertNotNil(batchResultResponse.result) + XCTAssertNotNil(batchResultResponse.result?.error) + + XCTAssertEqual(batchResultResponse.result?.type, .errored) + XCTAssertNil(batchResultResponse.result?.message) + + XCTAssertEqual(batchResultResponse.result?.error?.type, .invalidRequestError) + XCTAssertEqual(batchResultResponse.result?.error?.message, "Invalid input: The provided text contains inappropriate content.") + } + + func testDecodeBatchResultResponseCancelled() throws { + let json = """ + { + "custom_id": "request_789", + "result": { + "type": "cancelled", + "error": null + } + } + """ + + let jsonData = json.data(using: .utf8)! + let batchResultResponse = try anthropicJSONDecoder.decode(BatchResultResponse.self, from: jsonData) + + XCTAssertEqual(batchResultResponse.customId, "request_789") + XCTAssertNotNil(batchResultResponse.result) + XCTAssertNil(batchResultResponse.result?.error) + + XCTAssertEqual(batchResultResponse.result?.type, .cancelled) + XCTAssertNil(batchResultResponse.result?.message) + } } From f09c43749ccf9ebd23802037b524af2422f1d8e0 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 25 Oct 2024 01:05:20 +0900 Subject: [PATCH 17/26] update Mock to handle errors --- .../HTTPMock.swift | 18 ++++++++++++++-- .../MessagesTests.swift | 21 ++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Sources/AnthropicSwiftSDK-TestUtils/HTTPMock.swift b/Sources/AnthropicSwiftSDK-TestUtils/HTTPMock.swift index ed76400..6044bfa 100644 --- a/Sources/AnthropicSwiftSDK-TestUtils/HTTPMock.swift +++ b/Sources/AnthropicSwiftSDK-TestUtils/HTTPMock.swift @@ -12,6 +12,7 @@ public enum MockInspectType { case request((URLRequest) -> Void, String?) case requestHeader(([String: String]?) -> Void, String?) case response(String) + case error(String) } public class HTTPMock: URLProtocol { @@ -35,24 +36,37 @@ public class HTTPMock: URLProtocol { } if let url = request.url, - let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/2", headerFields: nil) { - client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + let succeedResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/2", headerFields: nil), + let errorResponse = HTTPURLResponse(url: url, statusCode: 400, httpVersion: "HTTP/2", headerFields: nil) { switch Self.inspectType { case .none: + client?.urlProtocol(self, didReceive: succeedResponse, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: getBasicJSONStringData()) + case .request(_, nil), .requestHeader(_, nil): + client?.urlProtocol(self, didReceive: succeedResponse, cacheStoragePolicy: .notAllowed) case .request(_, let jsonString), .requestHeader(_, let jsonString): + client?.urlProtocol(self, didReceive: succeedResponse, cacheStoragePolicy: .notAllowed) guard let jsonString, let data = jsonString.data(using: .utf8) else { client?.urlProtocol(self, didLoad: getBasicJSONStringData()) return } client?.urlProtocol(self, didLoad: data) case .response(let jsonString): + client?.urlProtocol(self, didReceive: succeedResponse, cacheStoragePolicy: .notAllowed) guard let data = jsonString.data(using: .utf8) else { client?.urlProtocol(self, didLoad: getBasicJSONStringData()) return } client?.urlProtocol(self, didLoad: data) + case .error(let responseJSON): + client?.urlProtocol(self, didReceive: errorResponse, cacheStoragePolicy: .notAllowed) + guard + let data = responseJSON.data(using: .utf8) else { + client?.urlProtocol(self, didLoad: getBasicJSONStringData()) + return + } + client?.urlProtocol(self, didLoad: data) } } diff --git a/Tests/AnthropicSwiftSDKTests/MessagesTests.swift b/Tests/AnthropicSwiftSDKTests/MessagesTests.swift index bfa246d..4ee026c 100644 --- a/Tests/AnthropicSwiftSDKTests/MessagesTests.swift +++ b/Tests/AnthropicSwiftSDKTests/MessagesTests.swift @@ -26,7 +26,26 @@ final class MessagesTests: XCTestCase { HTTPMock.inspectType = .requestHeader({ headers in XCTAssertEqual(headers!["x-api-key"], "This-is-test-API-key") expectation.fulfill() - }, nil) + }, """ + { + "id": "msg_01XFDUDYJgAACzvnptvVoYEL", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Hello!" + } + ], + "model": "claude-2.1", + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 12, + "output_tokens": 6 + } + } + """) let message = Message(role: .user, content: [.text("This is test text")]) let _ = try await messages.createMessage([message], maxTokens: 1024) From 3dc3cc2f0d0bcca52c721bd416e5bce999d46c02 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Wed, 23 Oct 2024 13:16:45 +0900 Subject: [PATCH 18/26] add tests for message batches api --- .../MessageBatchesTests.swift | 232 +++++++++++++++++- 1 file changed, 231 insertions(+), 1 deletion(-) diff --git a/Tests/AnthropicSwiftSDKTests/MessageBatchesTests.swift b/Tests/AnthropicSwiftSDKTests/MessageBatchesTests.swift index 4f9cbf2..0b798b6 100644 --- a/Tests/AnthropicSwiftSDKTests/MessageBatchesTests.swift +++ b/Tests/AnthropicSwiftSDKTests/MessageBatchesTests.swift @@ -6,8 +6,238 @@ // import XCTest +import AnthropicSwiftSDK_TestUtils @testable import AnthropicSwiftSDK final class MessageBatchesTests: XCTestCase { - // TODO: write tests + var session = URLSession.shared + + override func setUp() { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [HTTPMock.self] + + session = URLSession(configuration: configuration) + } + + func testMessageBatchesUsesProvidedAPIKey() async throws { + let batches = MessageBatches(apiKey: "This-is-test-API-key", session: session) + let expectation = XCTestExpectation(description: "MessageBatches uses provided api key in request header.") + + HTTPMock.inspectType = .requestHeader({ headers in + XCTAssertEqual(headers!["x-api-key"], "This-is-test-API-key") + expectation.fulfill() + }, + """ + { + "id": "batch_123456", + "type": "message_batch", + "processing_status": "ended", + "request_counts": { + "processing": 0, + "succeeeded": 95, + "errored": 3, + "canceled": 1, + "expired": 1 + }, + "ended_at": "2024-10-18T15:30:00Z", + "created_at": "2024-10-18T15:00:00Z", + "expires_at": "2024-10-19T15:00:00Z", + "cancel_initiated_at": null, + "results_url": "https://example.com/results/batch_123456.jsonl" + } + """) + + let batch = MessageBatch( + customId: "This-is-custom-id", + parameter: .init( + messages: [ + .init( + role: .user, + content: [ + .text("This is test text") + ] + ) + ], + maxTokens: 1024 + ) + ) + let _ = try await batches.createBatches(batches: [batch]) + + await fulfillment(of: [expectation], timeout: 1.0) + } + + func testCreateMessageBatchesReturnsBatchResponse() async throws { + let batches = MessageBatches(apiKey: "This-is-test-API-key", session: session) + + HTTPMock.inspectType = .response( + """ + { + "id": "batch_123456", + "type": "message_batch", + "processing_status": "ended", + "request_counts": { + "processing": 0, + "succeeeded": 95, + "errored": 3, + "canceled": 1, + "expired": 1 + }, + "ended_at": "2024-10-18T15:30:00Z", + "created_at": "2024-10-18T15:00:00Z", + "expires_at": "2024-10-19T15:00:00Z", + "cancel_initiated_at": null, + "results_url": "https://example.com/results/batch_123456.jsonl" + } + """) + + let batch = MessageBatch( + customId: "This-is-custom-id", + parameter: .init(messages: [.init(role: .user, content: [.text("This is test text")])], maxTokens: 1024) + ) + let response = try await batches.createBatches(batches: [batch]) + XCTAssertEqual(response.id, "batch_123456") + XCTAssertEqual(response.type, .message) + XCTAssertEqual(response.processingStatus, .ended) + XCTAssertEqual(response.requestCounts.processing, 0) + XCTAssertEqual(response.requestCounts.succeeeded, 95) + XCTAssertEqual(response.requestCounts.errored, 3) + XCTAssertEqual(response.requestCounts.canceled, 1) + XCTAssertEqual(response.requestCounts.expired, 1) + XCTAssertEqual(response.endedAt, "2024-10-18T15:30:00Z") + XCTAssertEqual(response.createdAt, "2024-10-18T15:00:00Z") + XCTAssertEqual(response.expiresAt, "2024-10-19T15:00:00Z") + XCTAssertNil(response.cancelInitiatedAt) + XCTAssertEqual(response.resultsUrl, "https://example.com/results/batch_123456.jsonl") + } + + func testGetBatch() async throws { + let batches = MessageBatches(apiKey: "This-is-test-API-key", session: session) + + HTTPMock.inspectType = .response(""" + { + "id": "batch_123456", + "type": "message_batch", + "processing_status": "in_progress", + "request_counts": { + "processing": 50, + "succeeeded": 0, + "errored": 0, + "canceled": 0, + "expired": 0 + }, + "created_at": "2024-10-18T15:00:00Z", + "expires_at": "2024-10-19T15:00:00Z", + "cancel_initiated_at": null, + "results_url": null + } + """) + + let response = try await batches.retrieve(batchId: "batch_123456") + XCTAssertEqual(response.id, "batch_123456") + XCTAssertEqual(response.type, .message) + XCTAssertEqual(response.processingStatus, .inProgress) + XCTAssertEqual(response.requestCounts.processing, 50) + XCTAssertEqual(response.createdAt, "2024-10-18T15:00:00Z") + XCTAssertEqual(response.expiresAt, "2024-10-19T15:00:00Z") + XCTAssertNil(response.cancelInitiatedAt) + XCTAssertNil(response.resultsUrl) + } + + func testCancelBatch() async throws { + let batches = MessageBatches(apiKey: "This-is-test-API-key", session: session) + + HTTPMock.inspectType = .response(""" + { + "id": "batch_123456", + "type": "message_batch", + "processing_status": "canceling", + "request_counts": { + "processing": 25, + "succeeeded": 25, + "errored": 0, + "canceled": 0, + "expired": 0 + }, + "created_at": "2024-10-18T15:00:00Z", + "expires_at": "2024-10-19T15:00:00Z", + "cancel_initiated_at": "2024-10-18T15:30:00Z", + "results_url": null + } + """) + + let response = try await batches.cancel(batchId: "batch_123456") + XCTAssertEqual(response.id, "batch_123456") + XCTAssertEqual(response.processingStatus, .canceling) + XCTAssertEqual(response.cancelInitiatedAt, "2024-10-18T15:30:00Z") + } + + func testGetBatchResults() async throws { + let batches = MessageBatches(apiKey: "This-is-test-API-key", session: session) + + HTTPMock.inspectType = .response(""" + {"custom_id":"request_123","result":{"type":"succeeded","message":{"id":"msg_123456","type":"message","role":"assistant","model":"claude-3-opus-20240229","content":[{"type":"text","text":"Hello!"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":11,"output_tokens":1}}}} + {"custom_id":"request_456","result":{"type":"errored","error":{"type":"invalid_request_error","message":"Invalid input"}}} + """) + + let results = try await batches.results(of: "batch_123456") + XCTAssertEqual(results.count, 2) + XCTAssertEqual(results[0].customId, "request_123") + XCTAssertEqual(results[0].result?.type, .succeeded) + XCTAssertEqual(results[1].customId, "request_456") + XCTAssertEqual(results[1].result?.type, .errored) + } + + func testCreateMultipleBatches() async throws { + let batches = MessageBatches(apiKey: "This-is-test-API-key", session: session) + + HTTPMock.inspectType = .response(""" + { + "id": "batch_789012", + "type": "message_batch", + "processing_status": "in_progress", + "request_counts": { + "processing": 100, + "succeeeded": 0, + "errored": 0, + "canceled": 0, + "expired": 0 + }, + "created_at": "2024-10-18T16:00:00Z", + "expires_at": "2024-10-19T16:00:00Z", + "cancel_initiated_at": null, + "results_url": null + } + """) + + let batch1 = MessageBatch(customId: "custom_id_1", parameter: .init(messages: [.init(role: .user, content: [.text("Test 1")])], maxTokens: 1024)) + let batch2 = MessageBatch(customId: "custom_id_2", parameter: .init(messages: [.init(role: .user, content: [.text("Test 2")])], maxTokens: 1024)) + let response = try await batches.createBatches(batches: [batch1, batch2]) + + XCTAssertEqual(response.id, "batch_789012") + XCTAssertEqual(response.processingStatus, .inProgress) + XCTAssertEqual(response.requestCounts.processing, 100) + } + + func testCreateBatchesWithInvalidAPIKey() async throws { + let batches = MessageBatches(apiKey: "Invalid-API-Key", session: session) + + HTTPMock.inspectType = .error(""" + { + "type": "error", + "error": { + "type": "invalid_request_error", + "message": "There was an issue with the format or content of your request." + } + } + """) + + let batch = MessageBatch(customId: "custom_id", parameter: .init(messages: [.init(role: .user, content: [.text("Test")])], maxTokens: 1024)) + + do { + let _ = try await batches.createBatches(batches: [batch]) + XCTFail("Expected an error to be thrown") + } catch let error as AnthropicAPIError { + XCTAssertEqual(error, .invalidRequestError) + } + } } From 235316215b7c8e8ac2d2ad83db7bd46498a0260d Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 25 Oct 2024 01:06:23 +0900 Subject: [PATCH 19/26] use `SwiftyJSONLines` to handle json lines fetched from batche results api --- .swiftpm/configuration/Package.resolved | 86 ------------------- Package.resolved | 21 +++-- Package.swift | 6 +- .../AnthropicSwiftSDK/MessageBatches.swift | 3 +- 4 files changed, 21 insertions(+), 95 deletions(-) delete mode 100644 .swiftpm/configuration/Package.resolved diff --git a/.swiftpm/configuration/Package.resolved b/.swiftpm/configuration/Package.resolved deleted file mode 100644 index fd5eda2..0000000 --- a/.swiftpm/configuration/Package.resolved +++ /dev/null @@ -1,86 +0,0 @@ -{ - "pins" : [ - { - "identity" : "aws-crt-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/awslabs/aws-crt-swift", - "state" : { - "revision" : "0d0a0cf2e2cb780ceeceac190b4ede94f4f96902", - "version" : "0.26.0" - } - }, - { - "identity" : "aws-sdk-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/awslabs/aws-sdk-swift", - "state" : { - "revision" : "7b0d55676eb896e472662922e80ebe30ae3ca8ed", - "version" : "0.39.0" - } - }, - { - "identity" : "documentationcomment", - "kind" : "remoteSourceControl", - "location" : "https://github.com/fumito-ito/DocumentationComment.git", - "state" : { - "revision" : "90d3c5b9cccddb202c8adb99138733c868d7b082", - "version" : "0.0.6" - } - }, - { - "identity" : "grmustache.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/GRMustache.swift", - "state" : { - "revision" : "6e9dcfb807959e19f2915be1928aeccf01babba5", - "version" : "4.2.0" - } - }, - { - "identity" : "smithy-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/smithy-lang/smithy-swift", - "state" : { - "revision" : "0267f0c649558cbc1323bbc5a0c8b2403239655b", - "version" : "0.44.0" - } - }, - { - "identity" : "swift-cmark", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-cmark.git", - "state" : { - "revision" : "3bc2f3e25df0cecc5dc269f7ccae65d0f386f06a", - "version" : "0.4.0" - } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", - "version" : "1.5.4" - } - }, - { - "identity" : "swift-markdown", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-markdown.git", - "state" : { - "revision" : "4aae40bf6fff5286e0e1672329d17824ce16e081", - "version" : "0.4.0" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" - } - } - ], - "version" : 2 -} diff --git a/Package.resolved b/Package.resolved index e64f613..c548fa4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-cmark.git", "state" : { - "revision" : "3bc2f3e25df0cecc5dc269f7ccae65d0f386f06a", - "version" : "0.4.0" + "revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53", + "version" : "0.5.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-markdown.git", "state" : { - "revision" : "4aae40bf6fff5286e0e1672329d17824ce16e081", - "version" : "0.4.0" + "revision" : "8f79cb175981458a0a27e76cb42fee8e17b1a993", + "version" : "0.5.0" } }, { @@ -50,8 +50,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25", - "version" : "600.0.0" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, + { + "identity" : "swiftyjsonlines", + "kind" : "remoteSourceControl", + "location" : "https://github.com/fumito-ito/SwiftyJSONLines.git", + "state" : { + "revision" : "ae30455420897b2c45621a88b3ae3f052a8b5f3d", + "version" : "0.0.2" } } ], diff --git a/Package.swift b/Package.swift index 1dd197a..426119e 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,8 @@ let package = Package( targets: ["AnthropicSwiftSDK"]), ], dependencies: [ - .package(url: "https://github.com/fumito-ito/FunctionCalling", from: "0.4.0") + .package(url: "https://github.com/fumito-ito/FunctionCalling", from: "0.4.0"), + .package(url: "https://github.com/fumito-ito/SwiftyJSONLines.git", from: "0.0.2") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -24,7 +25,8 @@ let package = Package( .target( name: "AnthropicSwiftSDK", dependencies: [ - .product(name: "FunctionCalling", package: "FunctionCalling") + .product(name: "FunctionCalling", package: "FunctionCalling"), + .product(name: "SwiftyJSONLines", package: "SwiftyJSONLines") ] ), .testTarget( diff --git a/Sources/AnthropicSwiftSDK/MessageBatches.swift b/Sources/AnthropicSwiftSDK/MessageBatches.swift index 47f48f1..e16e49f 100644 --- a/Sources/AnthropicSwiftSDK/MessageBatches.swift +++ b/Sources/AnthropicSwiftSDK/MessageBatches.swift @@ -7,6 +7,7 @@ import Foundation import FunctionCalling +import SwiftyJSONLines public struct MessageBatches { private let apiKey: String @@ -113,7 +114,7 @@ public struct MessageBatches { throw AnthropicAPIError(fromHttpStatusCode: httpResponse.statusCode) } - return try anthropicJSONDecoder.decode([BatchResultResponse].self, from: data) + return try JSONLines(data: data).toObjects(with: anthropicJSONDecoder) } public func results(streamOf batchId: String) async throws -> AsyncThrowingStream { From 934d6851bda76bb54ca8a0b8ce20cf9c326761fa Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 25 Oct 2024 02:43:51 +0900 Subject: [PATCH 20/26] add docs --- .../Entity/Batch/BatchParameter.swift | 15 +++ .../Entity/Batch/BatchRequestCounts.swift | 14 +++ .../Entity/Batch/BatchResultType.swift | 10 +- .../Entity/Batch/MessageBatch.swift | 6 ++ .../Entity/Batch/ProcessingStatus.swift | 2 +- .../AnthropicSwiftSDK/MessageBatches.swift | 91 +++++++++++++++++++ .../Request/CancelMessageBatchRequest.swift | 5 + .../Request/ListMessageBatchesRequest.swift | 6 ++ .../Request/MessageBatchesRequest.swift | 10 +- .../RetrieveMessageBatchResultsRequest.swift | 7 ++ .../RetrieveMessageBatchesRequest.swift | 3 + .../Response/BatchResultResponse.swift | 5 + 12 files changed, 169 insertions(+), 5 deletions(-) diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift index 7f0ee38..995f0dc 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchParameter.swift @@ -8,11 +8,21 @@ import FunctionCalling public struct BatchParameter { + /// Input messages. public let messages: [Message] + /// The model that will complete your prompt. + /// + /// See [models](https://docs.anthropic.com/en/docs/models-overview) for additional details and options. public let model: Model + /// System prompt. + /// + /// A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. public let system: [SystemPrompt] + /// The maximum number of tokens to generate before stopping. public let maxTokens: Int + /// An object describing metadata about the request. public let metaData: MetaData? + /// Custom text sequences that will cause the model to stop generating. public let stopSequence: [String]? /// Whether the response should be handles as streaming or not, /// @@ -21,10 +31,15 @@ public struct BatchParameter { /// - Note: Streaming is not supported for batch requests. /// For more details, see https://docs.anthropic.com/en/docs/build-with-claude/message-batches#can-i-use-the-message-batches-api-with-other-api-features public let stream: Bool = false + /// Amount of randomness injected into the response. public let temperature: Double? + /// Use nucleus sampling. public let topP: Double? + /// Only sample from the top K options for each subsequent token. public let topK: Int? + /// Definitions of tools that the model may use. public let toolContainer: ToolContainer? + /// How the model should use the provided tools. The model can use a specific tool, any available tool, or decide by itself. public let toolChoice: ToolChoice public init( diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift index 0814875..ef8591c 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchRequestCounts.swift @@ -5,10 +5,24 @@ // Created by 伊藤史 on 2024/10/09. // +/// Tallies requests within the Message Batch, categorized by their status. public struct BatchRequestCounts: Decodable { + /// Number of requests in the Message Batch that are processing. let processing: Int + /// Number of requests in the Message Batch that have completed successfully. + /// + /// This is zero until processing of the entire Message Batch has ended. let succeeeded: Int + /// Number of requests in the Message Batch that encountered an error. + /// + /// This is zero until processing of the entire Message Batch has ended. let errored: Int + /// Number of requests in the Message Batch that have been canceled. + /// + /// This is zero until processing of the entire Message Batch has ended. let canceled: Int + /// Number of requests in the Message Batch that have expired. + /// + /// This is zero until processing of the entire Message Batch has ended. let expired: Int } diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift index a3f7a63..59ac1c7 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/BatchResultType.swift @@ -5,10 +5,16 @@ // Created by 伊藤史 on 2024/10/09. // -/// https://docs.anthropic.com/en/docs/build-with-claude/message-batches#retrieving-batch-results +/// Once batch processing has ended, each Messages request in the batch will have a result. public enum BatchResultType: String, Decodable { - case succeeded // include the message result as jsonl + /// Request was successful. Includes the message result. + case succeeded + /// Request encountered an error and a message was not created. + /// + /// Possible errors include invalid requests and internal server errors. You will not be billed for these requests. case errored + /// User canceled the batch before this request could be sent to the model. You will not be billed for these requests. case cancelled + /// Batch reached its 24 hour expiration before this request could be sent to the model. You will not be billed for these requests. case expired } diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift index 328fd25..c5f2bfd 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/MessageBatch.swift @@ -6,7 +6,13 @@ // public struct MessageBatch { + /// Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order. + /// + /// Must be unique for each request within the Message Batch. public let customId: String + /// Messages API creation parameters for the individual request. + /// + /// See the [Messages API](https://docs.anthropic.com/en/api/messages) reference for full documentation on available parameters. public let parameter: BatchParameter public init(customId: String, parameter: BatchParameter) { diff --git a/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift b/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift index 737ffe9..4ac9363 100644 --- a/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift +++ b/Sources/AnthropicSwiftSDK/Entity/Batch/ProcessingStatus.swift @@ -5,7 +5,7 @@ // Created by 伊藤史 on 2024/10/09. // -/// https://docs.anthropic.com/en/api/retrieving-message-batches +/// Processing status of the Message Batch. public enum ProcessingStatus: String, RawRepresentable, Decodable { case inProgress = "in_progress" case canceling diff --git a/Sources/AnthropicSwiftSDK/MessageBatches.swift b/Sources/AnthropicSwiftSDK/MessageBatches.swift index e16e49f..3e9774c 100644 --- a/Sources/AnthropicSwiftSDK/MessageBatches.swift +++ b/Sources/AnthropicSwiftSDK/MessageBatches.swift @@ -9,15 +9,28 @@ import Foundation import FunctionCalling import SwiftyJSONLines +/// A class responsible for managing message batches in the Anthropic API. public struct MessageBatches { + /// The API key used for authentication with the Anthropic API. private let apiKey: String + /// The URL session used for network requests. private let session: URLSession + /// Initializes a new instance of `MessageBatches`. + /// + /// - Parameters: + /// - apiKey: The API key for authentication. + /// - session: The URL session for network requests. init(apiKey: String, session: URLSession) { self.apiKey = apiKey self.session = session } + /// Creates new message batches. + /// + /// - Parameter batches: An array of `MessageBatch` to be created. + /// - Returns: A `BatchResponse` containing the details of the created batches. + /// - Throws: An error if the request fails. public func createBatches(batches: [MessageBatch]) async throws -> BatchResponse { try await createBatches( batches: batches, @@ -26,6 +39,14 @@ public struct MessageBatches { ) } + /// Creates new message batches with custom header providers. + /// + /// - Parameters: + /// - batches: An array of `MessageBatch` to be created. + /// - anthropicHeaderProvider: The provider for Anthropic-specific headers. + /// - authenticationHeaderProvider: The provider for authentication headers. + /// - Returns: A `BatchResponse` containing the details of the created batches. + /// - Throws: An error if the request fails. public func createBatches( batches: [MessageBatch], anthropicHeaderProvider: AnthropicHeaderProvider, @@ -51,6 +72,11 @@ public struct MessageBatches { return try anthropicJSONDecoder.decode(BatchResponse.self, from: data) } + /// Retrieves a specific message batch by its ID. + /// + /// - Parameter batchId: The ID of the batch to retrieve. + /// - Returns: A `BatchResponse` containing the details of the retrieved batch. + /// - Throws: An error if the request fails. public func retrieve(batchId: String) async throws -> BatchResponse { try await retrieve( batchId: batchId, @@ -59,6 +85,14 @@ public struct MessageBatches { ) } + /// Retrieves a specific message batch by its ID with custom header providers. + /// + /// - Parameters: + /// - batchId: The ID of the batch to retrieve. + /// - anthropicHeaderProvider: The provider for Anthropic-specific headers. + /// - authenticationHeaderProvider: The provider for authentication headers. + /// - Returns: A `BatchResponse` containing the details of the retrieved batch. + /// - Throws: An error if the request fails. public func retrieve( batchId: String, anthropicHeaderProvider: AnthropicHeaderProvider, @@ -84,6 +118,11 @@ public struct MessageBatches { return try anthropicJSONDecoder.decode(BatchResponse.self, from: data) } + /// Retrieves the results of a specific message batch by its ID. + /// + /// - Parameter batchId: The ID of the batch to retrieve results for. + /// - Returns: An array of `BatchResultResponse` containing the results of the batch. + /// - Throws: An error if the request fails. public func results(of batchId: String) async throws -> [BatchResultResponse] { try await results( of: batchId, @@ -92,6 +131,14 @@ public struct MessageBatches { ) } + /// Retrieves the results of a specific message batch by its ID with custom header providers. + /// + /// - Parameters: + /// - batchId: The ID of the batch to retrieve results for. + /// - anthropicHeaderProvider: The provider for Anthropic-specific headers. + /// - authenticationHeaderProvider: The provider for authentication headers. + /// - Returns: An array of `BatchResultResponse` containing the results of the batch. + /// - Throws: An error if the request fails. public func results( of batchId: String, anthropicHeaderProvider: AnthropicHeaderProvider, @@ -117,6 +164,11 @@ public struct MessageBatches { return try JSONLines(data: data).toObjects(with: anthropicJSONDecoder) } + /// Streams the results of a specific message batch by its ID. + /// + /// - Parameter batchId: The ID of the batch to stream results for. + /// - Returns: An `AsyncThrowingStream` of `BatchResultResponse` containing the streamed results of the batch. + /// - Throws: An error if the request fails. public func results(streamOf batchId: String) async throws -> AsyncThrowingStream { try await results( streamOf: batchId, @@ -125,6 +177,14 @@ public struct MessageBatches { ) } + /// Streams the results of a specific message batch by its ID with custom header providers. + /// + /// - Parameters: + /// - batchId: The ID of the batch to stream results for. + /// - anthropicHeaderProvider: The provider for Anthropic-specific headers. + /// - authenticationHeaderProvider: The provider for authentication headers. + /// - Returns: An `AsyncThrowingStream` of `BatchResultResponse` containing the streamed results of the batch. + /// - Throws: An error if the request fails. public func results( streamOf batchId: String, anthropicHeaderProvider: AnthropicHeaderProvider, @@ -164,6 +224,14 @@ public struct MessageBatches { } } + /// Lists message batches with optional pagination. + /// + /// - Parameters: + /// - beforeId: The ID to list batches before. + /// - afterId: The ID to list batches after. + /// - limit: The maximum number of batches to list. + /// - Returns: A `BatchListResponse` containing the list of batches. + /// - Throws: An error if the request fails. public func list(beforeId: String? = nil, afterId: String? = nil, limit: Int = 20) async throws -> BatchListResponse { try await list( beforeId: beforeId, @@ -174,6 +242,16 @@ public struct MessageBatches { ) } + /// Lists message batches with optional pagination and custom header providers. + /// + /// - Parameters: + /// - beforeId: The ID to list batches before. + /// - afterId: The ID to list batches after. + /// - limit: The maximum number of batches to list. + /// - anthropicHeaderProvider: The provider for Anthropic-specific headers. + /// - authenticationHeaderProvider: The provider for authentication headers. + /// - Returns: A `BatchListResponse` containing the list of batches. + /// - Throws: An error if the request fails. public func list( beforeId: String?, afterId: String?, @@ -213,6 +291,11 @@ public struct MessageBatches { return try anthropicJSONDecoder.decode(BatchListResponse.self, from: data) } + /// Cancels a specific message batch by its ID. + /// + /// - Parameter batchId: The ID of the batch to cancel. + /// - Returns: A `BatchResponse` containing the details of the canceled batch. + /// - Throws: An error if the request fails. public func cancel(batchId: String) async throws -> BatchResponse { try await cancel( batchId: batchId, @@ -221,6 +304,14 @@ public struct MessageBatches { ) } + /// Cancels a specific message batch by its ID with custom header providers. + /// + /// - Parameters: + /// - batchId: The ID of the batch to cancel. + /// - anthropicHeaderProvider: The provider for Anthropic-specific headers. + /// - authenticationHeaderProvider: The provider for authentication headers. + /// - Returns: A `BatchResponse` containing the details of the canceled batch. + /// - Throws: An error if the request fails. public func cancel( batchId: String, anthropicHeaderProvider: AnthropicHeaderProvider, diff --git a/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift index a2770dd..cb418ef 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/CancelMessageBatchRequest.swift @@ -5,6 +5,10 @@ // Created by 伊藤史 on 2024/10/16. // +/// Batches may be canceled any time before processing ends. +/// +/// Once cancellation is initiated, the batch enters a canceling state, at which time the system may complete any in-progress, non-interruptible requests before finalizing cancellation. +/// The number of canceled requests is specified in request_counts. To determine which requests were canceled, check the individual results within the batch. Note that cancellation may not result in any canceled requests if they were non-interruptible. struct CancelMessageBatchRequest: Request { let method: HttpMethod = .post var path: String { @@ -12,5 +16,6 @@ struct CancelMessageBatchRequest: Request { } let queries: [String: CustomStringConvertible]? = nil let body: Never? = nil + /// ID of the Message Batch. let batchId: String } diff --git a/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift index 0a85489..59fdcc5 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/ListMessageBatchesRequest.swift @@ -5,6 +5,7 @@ // Created by 伊藤史 on 2024/10/16. // +/// List all Message Batches within a Workspace. Most recently created batches are returned first. struct ListMessageBatchesRequest: Request { let method: HttpMethod = .get let path: String = RequestType.batches.basePath @@ -12,8 +13,13 @@ struct ListMessageBatchesRequest: Request { let body: Never? = nil enum Parameter: String { + /// ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object. case beforeId = "before_id" + /// ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object. case afterId = "after_id" + /// Number of items to return per page. + /// + /// Defaults to 20. Ranges from 1 to 100. case limit } } diff --git a/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift index 87e7be1..5adcb34 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/MessageBatchesRequest.swift @@ -5,6 +5,14 @@ // Created by 伊藤史 on 2024/10/09. // +/// The Message Batches API can be used to process multiple Messages API requests at once. +/// +/// Once a Message Batch is created, it begins processing immediately. +/// Batches can take up to 24 hours to complete. +/// +/// The Message Batches API supports the following models: Claude 3 Haiku, Claude 3 Opus, and Claude 3.5 Sonnet. +/// All features available in the Messages API, including beta features, are available through the Message Batches API. +/// While in beta, batches may contain up to 10,000 requests and be up to 32 MB in total size. struct MessageBatchesRequest: Request { typealias Body = MessageBatchesRequestBody @@ -16,7 +24,6 @@ struct MessageBatchesRequest: Request { // MARK: Request Body -/// https://docs.anthropic.com/en/api/creating-message-batches struct MessageBatchesRequestBody: Encodable { /// List of requests for prompt completion. Each is an individual request to create a Message. let requests: [Batch] @@ -26,7 +33,6 @@ struct MessageBatchesRequestBody: Encodable { } } -/// https://docs.anthropic.com/en/api/creating-message-batches struct Batch: Encodable { /// Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order. /// diff --git a/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchResultsRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchResultsRequest.swift index 2361ac9..a038f80 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchResultsRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchResultsRequest.swift @@ -5,6 +5,12 @@ // Created by 伊藤史 on 2024/10/16. // +/// Streams the results of a Message Batch as a .jsonl file. +/// +/// Each line in the file is a JSON object containing the result of a single request in the Message Batch. +/// Results are not guaranteed to be in the same order as requests. Use the custom_id field to match results to requests. +/// +/// While in beta, this endpoint requires passing the anthropic-beta header with value message-batches-2024-09-24 struct RetrieveMessageBatchResultsRequest: Request { let method: HttpMethod = .get var path: String { @@ -13,5 +19,6 @@ struct RetrieveMessageBatchResultsRequest: Request { let queries: [String: CustomStringConvertible]? = nil let body: Never? = nil + /// ID of the Message Batch. let batchId: String } diff --git a/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchesRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchesRequest.swift index ca2896a..c35557a 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchesRequest.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/RetrieveMessageBatchesRequest.swift @@ -5,6 +5,8 @@ // Created by 伊藤史 on 2024/10/16. // +/// This endpoint is idempotent and can be used to poll for Message Batch completion. +/// To access the results of a Message Batch, make a request to the results_url field in the response. struct RetrieveMessageBatchesRequest: Request { let method: HttpMethod = .get var path: String { @@ -13,5 +15,6 @@ struct RetrieveMessageBatchesRequest: Request { let queries: [String: CustomStringConvertible]? = nil let body: Never? = nil + /// ID of the Message Batch. let batchId: String } diff --git a/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift index ecda647..9a2164f 100644 --- a/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift +++ b/Sources/AnthropicSwiftSDK/Network/Response/BatchResultResponse.swift @@ -11,7 +11,12 @@ public struct BatchResult: Decodable { public let error: StreamingError? } +/// The results will be in .jsonl format, where each line is a valid JSON object representing the result of a single request in the Message Batch. +/// +/// For each streamed result, you can do something different depending on its custom_id and result type. public struct BatchResultResponse: Decodable { + /// ID of the request. public let customId: String + /// Result of the request. public let result: BatchResult? } From 55482b9a1bf997fcd72f461a2c889bcbb7574058 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 25 Oct 2024 10:30:20 +0900 Subject: [PATCH 21/26] add readme about prompt caching --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index d8450fc..c7173c9 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,27 @@ let result = try await Anthropic(apiKey: "your_claude_api_key") ) ``` +### [Prompt Caching (beta)](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) + +Prompt caching is a powerful feature that optimizes your API usage by allowing resuming from specific prefixes in your prompts. + +You can cache large text content, e.g. “Pride and Prejudice”, using the cache_control parameter. This enables reuse of this large text across multiple API calls without reprocessing it each time. Changing only the user message allows you to ask various questions about the book while utilizing the cached content, leading to faster responses and improved efficiency. + +Here’s an example of how to implement prompt caching with the Messages API using a cache_control block: + +```swift +let anthropic = Anthropic(apiKey: "YOUR_OWN_API_KEY") + +let message = Message(role: .user, content: [.text("Analyze the major themes in Pride and Prejudice.")]) +let response = try await anthropic.messages.createMessage( + [message], + system: [ + .text("You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n", nil), + .text("", .ephemeral) + ], + maxTokens: 1024 +) +``` ## Extensions By introducing an extension Swift package, it is possible to access the Anthropic Claude API through AWS Bedrock and Vertex AI. The supported services are as follows: From e8bcc2f7cc3741525ad1755e7da67c409c48f844 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 25 Oct 2024 10:31:20 +0900 Subject: [PATCH 22/26] add readme about message batches --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index c7173c9..19b94dc 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,37 @@ let response = try await anthropic.messages.createMessage( maxTokens: 1024 ) ``` + +### [Message Batches (beta)](https://docs.anthropic.com/en/docs/build-with-claude/message-batches) + +The Message Batches API is a powerful, cost-effective way to asynchronously process large volumes of [Messages](https://docs.anthropic.com/en/api/messages) requests. This approach is well-suited to tasks that do not require immediate responses, reducing costs by 50% while increasing throughput. + +This is especially useful for bulk operations that don’t require immediate results. + +Here's an example of how to process many messages with the Message Bathches API: + +```swift +let anthropic = Anthropic(apiKey: "YOUR_OWN_API_KEY") + +let messages = [ + Message(role: .user, content: [.text("Write a haiku about robots.")]), + Message(role: .user, content: [.text("Write a haiku about robots. Skip the preamble; go straight into the poem.")]), + Message(role: .user, content: [.text("Who is the best basketball player of all time?")]), + Message(role: .user, content: [.text("Who is the best basketball player of all time? Yes, there are differing opinions, but if you absolutely had to pick one player, who would it be?")]) + // .... +] + +let batch = MessageBatch( + customId: "my-first-batch-request", + parameter: .init( + messages: messages, + maxTokens: 1024 + ) +) + +let response = try await anthropic.messageBatches.createBatches(batches: [batch]) +``` + ## Extensions By introducing an extension Swift package, it is possible to access the Anthropic Claude API through AWS Bedrock and Vertex AI. The supported services are as follows: From 7fb1dddeffa477f68fa900463c33f4231a18e108 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 25 Oct 2024 10:33:07 +0900 Subject: [PATCH 23/26] update runner for swift 6 --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 8453d2e..e4ef9a6 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -12,7 +12,7 @@ on: jobs: build: - runs-on: macos-14 + runs-on: macos-15 steps: - uses: actions/checkout@v4 From 2040347ef8d9f55c3276e28ff552c9f243dc9093 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 25 Oct 2024 13:04:11 +0900 Subject: [PATCH 24/26] [temp] update example --- Example.swiftpm/ContentView.swift | 100 +++++------------- .../Extension/Messages+Extension.swift | 7 +- Example.swiftpm/Package.resolved | 45 ++------ Example.swiftpm/Package.swift | 6 +- .../Protocol/MessagesSubject.swift | 12 +++ Example.swiftpm/View/SendBatchView.swift | 78 ++++++++++++++ .../ViewModel/SendBatchViewModel.swift | 75 +++++++++++++ 7 files changed, 203 insertions(+), 120 deletions(-) create mode 100644 Example.swiftpm/View/SendBatchView.swift create mode 100644 Example.swiftpm/ViewModel/SendBatchViewModel.swift diff --git a/Example.swiftpm/ContentView.swift b/Example.swiftpm/ContentView.swift index 418a9b7..70aaf6d 100644 --- a/Example.swiftpm/ContentView.swift +++ b/Example.swiftpm/ContentView.swift @@ -1,26 +1,13 @@ import SwiftUI -import AnthropicSwiftSDK_VertexAI import AnthropicSwiftSDK -import AWSBedrockRuntime -import AnthropicSwiftSDK_Bedrock struct ContentView: View { // MARK: Properties for Claude @State private var claudeAPIKey = "" - @State private var isStreamClaude: Bool = false - - // MARK: Properties for Bedrock - @State private var bedrockRegion = "" - @State private var isStreamBedrock: Bool = false - - // MARK: Properties for Vertex - @State private var vertexProjectID = "" - @State private var vertexAuthToken = "" - @State private var isStreamVertex: Bool = false var body: some View { TabView { - // MARK: Claude + // MARK: Claude Messages API NavigationStack { VStack { Spacer() @@ -29,20 +16,10 @@ struct ContentView: View { .padding() .textFieldStyle(.roundedBorder) - Toggle(isOn: $isStreamClaude) { - Text("Enable Stream API") - } - .padding() - NavigationLink { let claude = Anthropic(apiKey: claudeAPIKey) - if isStreamClaude { - let observable = StreamViewModel(messageHandler: claude.messages, title: "Stream \\w Claude") - StreamView(observable: observable) - } else { - let observable = SendViewModel(messageHandler: claude.messages, title: "Message \\w Claude") - SendView(observable: observable) - } + let observable = SendViewModel(messageHandler: claude.messages, title: "Message \\w Claude") + SendView(observable: observable) } label: { Text("Continue") .frame(maxWidth: .infinity, minHeight: 48) @@ -59,37 +36,26 @@ struct ContentView: View { Spacer() } - .navigationTitle("Claude Demo") + .navigationTitle("Claude Send Message Demo") } .tabItem { Image(systemName: "pencil.and.scribble") - Text("Claude") + Text("Send Message") } - - // MARK: Bedrock + + // MARK: Claude Stream Messages API NavigationStack { VStack { Spacer() - TextField("Enter Region Code", text: $bedrockRegion) + TextField("Enter API Key", text: $claudeAPIKey) .padding() .textFieldStyle(.roundedBorder) - Toggle(isOn: $isStreamBedrock) { - Text("Enable Stream API") - } - .padding() - NavigationLink { - let bedrockClient = try! BedrockRuntimeClient(region: bedrockRegion) - let claude = bedrockClient.useAnthropic() - if isStreamBedrock { - let observable = StreamViewModel(messageHandler: claude.messages, title: "Stream \\w Bedrock", model: .claude_3_Opus) - StreamView(observable: observable) - } else { - let observable = SendViewModel(messageHandler: claude.messages, title: "Message \\w Bedrock", model: .claude_3_Opus) - SendView(observable: observable) - } + let claude = Anthropic(apiKey: claudeAPIKey) + let observable = StreamViewModel(messageHandler: claude.messages, title: "Stream \\w Claude") + StreamView(observable: observable) } label: { Text("Continue") .frame(maxWidth: .infinity, minHeight: 48) @@ -97,49 +63,35 @@ struct ContentView: View { .background( Capsule() .foregroundColor( - bedrockRegion.isEmpty ? .gray.opacity(0.2) : .blue + claudeAPIKey.isEmpty ? .gray.opacity(0.2) : .blue ) ) } .padding() - .disabled(bedrockRegion.isEmpty) + .disabled(claudeAPIKey.isEmpty) Spacer() } - .navigationTitle("Bedrock Demo") + .navigationTitle("Claude Stream Message Demo") } .tabItem { - Image(systemName: "globe.americas.fill") - Text("Bedrock") + Image(systemName: "pencil.and.scribble") + Text("Stream Message") } - // MARK: Vertex + // MARK: Claude Send Message Batches API NavigationStack { VStack { Spacer() - TextField("Enter Project ID", text: $vertexProjectID) - .padding() - .textFieldStyle(.roundedBorder) - - TextField("Enter Auth Token", text: $vertexAuthToken) + TextField("Enter API Key", text: $claudeAPIKey) .padding() .textFieldStyle(.roundedBorder) - Toggle(isOn: $isStreamVertex) { - Text("Enable Stream API") - } - .padding() - NavigationLink { - let claude = AnthropicVertexAIClient(projectId: vertexProjectID, accessToken: vertexAuthToken, region: .europeWest1) - if isStreamVertex { - let observable = StreamViewModel(messageHandler: claude.messages, title: "Stream \\w Vertex") - StreamView(observable: observable) - } else { - let observable = SendViewModel(messageHandler: claude.messages, title: "Message \\w Vertex") - SendView(observable: observable) - } + let claude = Anthropic(apiKey: claudeAPIKey) + let observable = SendMessageBatchesViewModel(messageHandler: claude.messageBatches, title: "Batch \\w Claude") + SendMessageBatchView(observable: observable) } label: { Text("Continue") .frame(maxWidth: .infinity, minHeight: 48) @@ -147,20 +99,20 @@ struct ContentView: View { .background( Capsule() .foregroundColor( - vertexProjectID.isEmpty || vertexAuthToken.isEmpty ? .gray.opacity(0.2) : .blue + claudeAPIKey.isEmpty ? .gray.opacity(0.2) : .blue ) ) } .padding() - .disabled(vertexProjectID.isEmpty || vertexAuthToken.isEmpty) + .disabled(claudeAPIKey.isEmpty) Spacer() } - .navigationTitle("VertexAI Demo") + .navigationTitle("Claude Batch Message Demo") } .tabItem { - Image(systemName: "mountain.2.fill") - Text("Vertex") + Image(systemName: "pencil.and.scribble") + Text("Batch Message") } } diff --git a/Example.swiftpm/Extension/Messages+Extension.swift b/Example.swiftpm/Extension/Messages+Extension.swift index 6f19ada..736778b 100644 --- a/Example.swiftpm/Extension/Messages+Extension.swift +++ b/Example.swiftpm/Extension/Messages+Extension.swift @@ -7,12 +7,7 @@ import Foundation import AnthropicSwiftSDK -import AnthropicSwiftSDK_Bedrock -import AnthropicSwiftSDK_VertexAI extension AnthropicSwiftSDK.Messages: MessageSendable {} extension AnthropicSwiftSDK.Messages: MessageStreamable {} -extension AnthropicSwiftSDK_Bedrock.Messages: MessageSendable {} -extension AnthropicSwiftSDK_Bedrock.Messages: MessageStreamable {} -extension AnthropicSwiftSDK_VertexAI.Messages: MessageSendable {} -extension AnthropicSwiftSDK_VertexAI.Messages: MessageStreamable {} +extension AnthropicSwiftSDK.MessageBatches: MessageBatchSendable {} diff --git a/Example.swiftpm/Package.resolved b/Example.swiftpm/Package.resolved index b081541..e904133 100644 --- a/Example.swiftpm/Package.resolved +++ b/Example.swiftpm/Package.resolved @@ -1,23 +1,5 @@ { "pins" : [ - { - "identity" : "aws-crt-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/awslabs/aws-crt-swift", - "state" : { - "revision" : "7b42e0343f28b3451aab20840dc670abd12790bd", - "version" : "0.36.0" - } - }, - { - "identity" : "aws-sdk-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/awslabs/aws-sdk-swift", - "state" : { - "revision" : "192bc5984cb765a88f8a59d0678aeb85a888a9f5", - "version" : "1.0.8" - } - }, { "identity" : "documentationcomment", "kind" : "remoteSourceControl", @@ -45,15 +27,6 @@ "version" : "5.0.1" } }, - { - "identity" : "smithy-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/smithy-lang/smithy-swift", - "state" : { - "revision" : "c5242d4e06e721293ee91c7affd1d527912f596d", - "version" : "0.76.0" - } - }, { "identity" : "swift-cmark", "kind" : "remoteSourceControl", @@ -63,15 +36,6 @@ "version" : "0.4.0" } }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", - "version" : "1.6.1" - } - }, { "identity" : "swift-markdown", "kind" : "remoteSourceControl", @@ -89,6 +53,15 @@ "revision" : "0687f71944021d616d34d922343dcef086855920", "version" : "600.0.1" } + }, + { + "identity" : "swiftyjsonlines", + "kind" : "remoteSourceControl", + "location" : "https://github.com/fumito-ito/SwiftyJSONLines.git", + "state" : { + "revision" : "ae30455420897b2c45621a88b3ae3f052a8b5f3d", + "version" : "0.0.2" + } } ], "version" : 2 diff --git a/Example.swiftpm/Package.swift b/Example.swiftpm/Package.swift index 04b8360..36d9b3d 100644 --- a/Example.swiftpm/Package.swift +++ b/Example.swiftpm/Package.swift @@ -17,7 +17,7 @@ let package = Package( name: "Example", targets: ["AppModule"], bundleIdentifier: "com.github.fumito-ito.AnthropicSwiftSDK.Example", - teamIdentifier: "L8LPZ499U7", + teamIdentifier: "K489QY5CFD", displayVersion: "1.0", bundleVersion: "1", appIcon: .placeholder(icon: .clock), @@ -44,9 +44,7 @@ let package = Package( .executableTarget( name: "AppModule", dependencies: [ - .product(name: "AnthropicSwiftSDK", package: "AnthropicSwiftSDK"), - .product(name: "AnthropicSwiftSDK-Bedrock", package: "AnthropicSwiftSDK"), - .product(name: "AnthropicSwiftSDK-VertexAI", package: "AnthropicSwiftSDK") + .product(name: "AnthropicSwiftSDK", package: "AnthropicSwiftSDK") ], path: "." ) diff --git a/Example.swiftpm/Protocol/MessagesSubject.swift b/Example.swiftpm/Protocol/MessagesSubject.swift index a63414b..e18bfdf 100644 --- a/Example.swiftpm/Protocol/MessagesSubject.swift +++ b/Example.swiftpm/Protocol/MessagesSubject.swift @@ -33,6 +33,12 @@ protocol StreamMessagesSubject: MessagesSubject { func streamMessage(text: String) async throws } +protocol SendMessageBatchesSubject: MessagesSubject { + init(messageHandler: MessageBatchSendable, title: String, model: Model) + + func sendMessageBatch(text: String) async throws +} + protocol MessageSendable { func createMessage( _ messages: [Message], @@ -49,6 +55,12 @@ protocol MessageSendable { ) async throws -> MessagesResponse } +protocol MessageBatchSendable { + func createBatches(batches: [MessageBatch]) async throws -> BatchResponse + + func results(streamOf batchId: String) async throws -> AsyncThrowingStream +} + protocol MessageStreamable { func streamMessage( _ messages: [Message], diff --git a/Example.swiftpm/View/SendBatchView.swift b/Example.swiftpm/View/SendBatchView.swift new file mode 100644 index 0000000..6edf82e --- /dev/null +++ b/Example.swiftpm/View/SendBatchView.swift @@ -0,0 +1,78 @@ +// +// SwiftUIView.swift +// Example +// +// Created by 伊藤史 on 2024/10/25. +// + +import SwiftUI + +struct SendMessageBatchView: View { + @State var observable: SendMessageBatchesSubject + @State private var prompt = "" + + var body: some View { + VStack { + List(observable.messages) { message in + HStack { + if message.user == .assistant { + Image(systemName: "brain.filled.head.profile").frame(alignment: .leading) + Text(message.text).frame(maxWidth: .infinity, alignment: .leading) + + } else { + Text(message.text).frame(maxWidth: .infinity, alignment: .trailing) + Image(systemName: "person.fill").frame(alignment: .trailing) + } + } + } + .padding(.bottom, 1) + .alert("Error", isPresented: $observable.isShowingError) { + Button("OK") {} + } message: { + Text(observable.errorMessage) + } + .overlay( + Group { + if observable.isLoading { + ProgressView() + } else { + EmptyView() + } + } + ) + } + .safeAreaInset(edge: .bottom) { + VStack(spacing: 0) { + textArea + } + } + .navigationTitle(observable.title) + } + + var textArea: some View { + HStack(spacing: 2) { + Button { + observable.clear() + } label: { + Image(systemName: "eraser") + } + .buttonStyle(.bordered) + TextField("Enter prompt", text: $prompt, axis: .vertical) + .textFieldStyle(.roundedBorder) + .padding() + Button { + Task { + do { + try await observable.sendMessageBatch(text: prompt) + } catch { + print(error) + } + } + } label: { + Image(systemName: "paperplane") + } + .buttonStyle(.bordered) + } + .padding() + } +} diff --git a/Example.swiftpm/ViewModel/SendBatchViewModel.swift b/Example.swiftpm/ViewModel/SendBatchViewModel.swift new file mode 100644 index 0000000..96370b2 --- /dev/null +++ b/Example.swiftpm/ViewModel/SendBatchViewModel.swift @@ -0,0 +1,75 @@ +// +// SendBatchViewModel.swift +// Example +// +// Created by 伊藤史 on 2024/10/25. +// + +import Foundation +import AnthropicSwiftSDK + +@Observable class SendMessageBatchesViewModel: SendMessageBatchesSubject { + + private let messageHandler: MessageBatchSendable + private let functionTools = FunctionTools() + let title: String + let model: Model + + var messages: [ChatMessage] = [] + + var errorMessage: String = "" { + didSet { + isShowingError = errorMessage.isEmpty == false + } + } + + var isShowingError: Bool = false + + var isLoading: Bool = false + + private var task: Task? = nil + + required init( + messageHandler: any MessageBatchSendable, + title: String, + model: Model = .claude_3_5_Sonnet) { + self.messageHandler = messageHandler + self.title = title + self.model = model + + } + + func sendMessageBatch(text: String) async throws { + messages.append(.init(user: .user, text: text)) + + let message = Message(role: .user, content: [.text(text)]) + let batch = MessageBatch(customId: text, parameter: .init(messages: [message], maxTokens: 1024)) + + task = Task { + do { + isLoading = true + let result = try await messageHandler.createBatches(batches: [batch]) + let stream = try await messageHandler.results(streamOf: result.id) + for try await chunk in stream { + guard case .text(let text) = chunk.result?.message?.content.first else { + return + } + messages.append(.init(user: .assistant, text: text)) + } + isLoading = false + } catch { + errorMessage = "\(error)" + isLoading = false + } + + } + } + + func cancel() { + task?.cancel() + } + + func clear() { + messages = [] + } +} From 943b0745cc7196657ddfed7c00c7b1377568b15c Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Fri, 25 Oct 2024 13:04:11 +0900 Subject: [PATCH 25/26] update example --- Example.swiftpm/ViewModel/SendBatchViewModel.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Example.swiftpm/ViewModel/SendBatchViewModel.swift b/Example.swiftpm/ViewModel/SendBatchViewModel.swift index 96370b2..0177c14 100644 --- a/Example.swiftpm/ViewModel/SendBatchViewModel.swift +++ b/Example.swiftpm/ViewModel/SendBatchViewModel.swift @@ -43,8 +43,9 @@ import AnthropicSwiftSDK messages.append(.init(user: .user, text: text)) let message = Message(role: .user, content: [.text(text)]) - let batch = MessageBatch(customId: text, parameter: .init(messages: [message], maxTokens: 1024)) - + let customId = UUID().uuidString + let batch = MessageBatch(customId: customId, parameter: .init(messages: [message], maxTokens: 1024)) + task = Task { do { isLoading = true From 2069387222cf63013a0dbdf1dc32911fb22f3196 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Sat, 26 Oct 2024 21:40:00 +0900 Subject: [PATCH 26/26] fix SwiftyJSONLines version for iOS --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index c548fa4..aadb139 100644 --- a/Package.resolved +++ b/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/fumito-ito/SwiftyJSONLines.git", "state" : { - "revision" : "ae30455420897b2c45621a88b3ae3f052a8b5f3d", - "version" : "0.0.2" + "revision" : "fb957dada1e920b6916c7083e99895f8f7fc885a", + "version" : "0.0.3" } } ], diff --git a/Package.swift b/Package.swift index 426119e..811b161 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/fumito-ito/FunctionCalling", from: "0.4.0"), - .package(url: "https://github.com/fumito-ito/SwiftyJSONLines.git", from: "0.0.2") + .package(url: "https://github.com/fumito-ito/SwiftyJSONLines.git", from: "0.0.3") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite.