From 9913ae5a5f4386e66550860a101913935f0f1c9c Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Tue, 23 Jan 2024 16:04:13 -0800 Subject: [PATCH] put errors into api methods --- .../ChatSample/Views/ErrorDetailsView.swift | 4 +- .../ChatSample/Views/ErrorView.swift | 2 +- Sources/GoogleAI/Chat.swift | 22 +++++++++-- Sources/GoogleAI/GenerativeModel.swift | 13 ++++++- Sources/GoogleAI/ModelContent.swift | 13 ++----- Tests/GoogleAITests/ChatTests.swift | 2 +- Tests/GoogleAITests/GoogleAITests.swift | 38 +++++++++---------- 7 files changed, 56 insertions(+), 38 deletions(-) diff --git a/Examples/GenerativeAISample/ChatSample/Views/ErrorDetailsView.swift b/Examples/GenerativeAISample/ChatSample/Views/ErrorDetailsView.swift index 5483f03..7f7e25c 100644 --- a/Examples/GenerativeAISample/ChatSample/Views/ErrorDetailsView.swift +++ b/Examples/GenerativeAISample/ChatSample/Views/ErrorDetailsView.swift @@ -162,7 +162,7 @@ struct ErrorDetailsView: View { NavigationView { let _ = GenerateContentError.promptBlocked( response: GenerateContentResponse(candidates: [ - CandidateResponse(content: ModelContent(role: "model", [ + CandidateResponse(content: try! ModelContent(role: "model", [ """ A _hypothetical_ model response. Cillum ex aliqua amet aliquip labore amet eiusmod consectetur reprehenderit sit commodo. @@ -183,7 +183,7 @@ struct ErrorDetailsView: View { let errorFinishedEarly = GenerateContentError.responseStoppedEarly( reason: .maxTokens, response: GenerateContentResponse(candidates: [ - CandidateResponse(content: ModelContent(role: "model", [ + CandidateResponse(content: try! ModelContent(role: "model", [ """ A _hypothetical_ model response. Cillum ex aliqua amet aliquip labore amet eiusmod consectetur reprehenderit sit commodo. diff --git a/Examples/GenerativeAISample/ChatSample/Views/ErrorView.swift b/Examples/GenerativeAISample/ChatSample/Views/ErrorView.swift index aafdcd2..77abea6 100644 --- a/Examples/GenerativeAISample/ChatSample/Views/ErrorView.swift +++ b/Examples/GenerativeAISample/ChatSample/Views/ErrorView.swift @@ -37,7 +37,7 @@ struct ErrorView: View { NavigationView { let errorPromptBlocked = GenerateContentError.promptBlocked( response: GenerateContentResponse(candidates: [ - CandidateResponse(content: ModelContent(role: "model", [ + CandidateResponse(content: try! ModelContent(role: "model", [ """ A _hypothetical_ model response. Cillum ex aliqua amet aliquip labore amet eiusmod consectetur reprehenderit sit commodo. diff --git a/Sources/GoogleAI/Chat.swift b/Sources/GoogleAI/Chat.swift index 51c0fc4..78ecb1f 100644 --- a/Sources/GoogleAI/Chat.swift +++ b/Sources/GoogleAI/Chat.swift @@ -40,9 +40,14 @@ public class Chat { /// - Parameter content: The new content to send as a single chat message. /// - Returns: The model's response if no error occurred. /// - Throws: A ``GenerateContentError`` if an error occurred. - public func sendMessage(_ content: [ModelContent]) async throws -> GenerateContentResponse { + public func sendMessage(_ content: @autoclosure () throws -> [ModelContent]) async throws -> GenerateContentResponse { // Ensure that the new content has the role set. - let newContent: [ModelContent] = content.map(populateContentRole(_:)) + let newContent: [ModelContent] + do { + newContent = try content().map(populateContentRole(_:)) + } catch(let underlying) { + throw GenerateContentError.internalError(underlying: underlying) + } // Send the history alongside the new message as context. let request = history + newContent @@ -69,7 +74,7 @@ public class Chat { @available(macOS 12.0, *) public func sendMessageStream(_ parts: any PartsRepresentable...) -> AsyncThrowingStream { - return sendMessageStream([ModelContent(parts: parts)]) + return sendMessageStream(try [ModelContent(parts: parts)]) } /// Sends a message using the existing history of this chat as context. If successful, the message @@ -77,8 +82,17 @@ public class Chat { /// - Parameter content: The new content to send as a single chat message. /// - Returns: A stream containing the model's response or an error if an error occurred. @available(macOS 12.0, *) - public func sendMessageStream(_ content: [ModelContent]) + public func sendMessageStream(_ contentClosure: @autoclosure () throws -> [ModelContent]) -> AsyncThrowingStream { + let content: [ModelContent] + do { + content = try contentClosure() + } catch(let underlying) { + return AsyncThrowingStream { continuation in + continuation.finish(throwing: GenerateContentError.internalError(underlying: underlying)) + } + } + return AsyncThrowingStream { continuation in Task { var aggregatedContent: [ModelContent] = [] diff --git a/Sources/GoogleAI/GenerativeModel.swift b/Sources/GoogleAI/GenerativeModel.swift index ea97053..e73811f 100644 --- a/Sources/GoogleAI/GenerativeModel.swift +++ b/Sources/GoogleAI/GenerativeModel.swift @@ -141,7 +141,7 @@ public final class GenerativeModel { @available(macOS 12.0, *) public func generateContentStream(_ parts: any PartsRepresentable...) -> AsyncThrowingStream { - return generateContentStream([ModelContent(parts: parts)]) + return generateContentStream(try [ModelContent(parts: parts)]) } /// Generates new content from input content given to the model as a prompt. @@ -150,8 +150,17 @@ public final class GenerativeModel { /// - Returns: A stream wrapping content generated by the model or a ``GenerateContentError`` /// error if an error occurred. @available(macOS 12.0, *) - public func generateContentStream(_ content: [ModelContent]) + public func generateContentStream(_ contentClosure: @autoclosure () throws -> [ModelContent]) -> AsyncThrowingStream { + let content: [ModelContent] + do { + content = try contentClosure() + } catch(let underlying) { + return AsyncThrowingStream { continuation in + continuation.finish(throwing: GenerateContentError.internalError(underlying: underlying)) + } + } + let generateContentRequest = GenerateContentRequest(model: modelResourceName, contents: content, generationConfig: generationConfig, diff --git a/Sources/GoogleAI/ModelContent.swift b/Sources/GoogleAI/ModelContent.swift index df99efa..05148bf 100644 --- a/Sources/GoogleAI/ModelContent.swift +++ b/Sources/GoogleAI/ModelContent.swift @@ -104,14 +104,9 @@ public struct ModelContent: Codable, Equatable { /// Creates a new value from any data or `Array` of data interpretable as a /// ``Part``. See ``PartsRepresentable`` for types that can be interpreted as `Part`s. - public init(role: String? = "user", parts: some PartsRepresentable) { + public init(role: String? = "user", parts: some PartsRepresentable) throws { self.role = role - do { - try self.parts = parts.tryPartsValue() - } catch { - Logging.default.error("Error creating parts: \(error)") - self.parts = [] - } + try self.parts = parts.tryPartsValue() } /// Creates a new value from a list of ``Part``s. @@ -122,7 +117,7 @@ public struct ModelContent: Codable, Equatable { /// Creates a new value from any data interpretable as a ``Part``. See ``PartsRepresentable`` /// for types that can be interpreted as `Part`s. - public init(role: String? = "user", _ parts: any PartsRepresentable...) { - self.init(role: role, parts: parts) + public init(role: String? = "user", _ parts: any PartsRepresentable...) throws { + try self.init(role: role, parts: parts) } } diff --git a/Tests/GoogleAITests/ChatTests.swift b/Tests/GoogleAITests/ChatTests.swift index 4020d4b..0757327 100644 --- a/Tests/GoogleAITests/ChatTests.swift +++ b/Tests/GoogleAITests/ChatTests.swift @@ -60,7 +60,7 @@ final class ChatTests: XCTestCase { XCTAssertEqual(chat.history[0].parts[0].text, input) let finalText = "1 2 3 4 5 6 7 8 9 10" - let assembledExpectation = ModelContent(role: "model", parts: finalText) + let assembledExpectation = try ModelContent(role: "model", parts: finalText) XCTAssertEqual(chat.history[0].parts[0].text, input) XCTAssertEqual(chat.history[1], assembledExpectation) } diff --git a/Tests/GoogleAITests/GoogleAITests.swift b/Tests/GoogleAITests/GoogleAITests.swift index 6389ddb..be84d46 100644 --- a/Tests/GoogleAITests/GoogleAITests.swift +++ b/Tests/GoogleAITests/GoogleAITests.swift @@ -69,8 +69,8 @@ final class GoogleGenerativeAITests: XCTestCase { .generateContent([str, UIImage(), ModelContent.Part.text(str)]) _ = try await genAI.generateContent(str, UIImage(), "def", UIImage()) _ = try await genAI.generateContent([str, UIImage(), "def", UIImage()]) - _ = try await genAI.generateContent([ModelContent("def", UIImage()), - ModelContent("def", UIImage())]) + _ = try await genAI.generateContent([try ModelContent("def", UIImage()), + try ModelContent("def", UIImage())]) #elseif canImport(AppKit) _ = try await genAI.generateContent(NSImage()) _ = try await genAI.generateContent([NSImage()]) @@ -81,40 +81,40 @@ final class GoogleGenerativeAITests: XCTestCase { // PartsRepresentable combinations. let _ = ModelContent(parts: [.text(str)]) let _ = ModelContent(role: "model", parts: [.text(str)]) - let _ = ModelContent(parts: "Constant String") - let _ = ModelContent(parts: str) - let _ = ModelContent(parts: [str]) + let _ = try ModelContent(parts: "Constant String") + let _ = try ModelContent(parts: str) + let _ = try ModelContent(parts: [str]) // Note: without `as [any PartsRepresentable]` this will fail to compile with "Cannot convert // value of type `[Any]` to expected type `[any PartsRepresentable]`. Not sure if there's a // way we can get it to work. - let _ = ModelContent(parts: [str, ModelContent.Part.data( + let _ = try ModelContent(parts: [str, ModelContent.Part.data( mimetype: "foo", Data() )] as [any PartsRepresentable]) #if canImport(UIKit) - _ = ModelContent(role: "user", parts: UIImage()) - _ = ModelContent(role: "user", parts: [UIImage()]) + _ = try ModelContent(role: "user", parts: UIImage()) + _ = try ModelContent(role: "user", parts: [UIImage()]) // Note: without `as [any PartsRepresentable]` this will fail to compile with "Cannot convert // value of type `[Any]` to expected type `[any PartsRepresentable]`. Not sure if there's a // way we can get it to work. - _ = ModelContent(parts: [str, UIImage()] as [any PartsRepresentable]) + _ = try ModelContent(parts: [str, UIImage()] as [any PartsRepresentable]) // Alternatively, you can explicitly declare the type in a variable and pass it in. let representable2: [any PartsRepresentable] = [str, UIImage()] - _ = ModelContent(parts: representable2) - _ = ModelContent(parts: [str, UIImage(), + _ = try ModelContent(parts: representable2) + _ = try ModelContent(parts: [str, UIImage(), ModelContent.Part.text(str)] as [any PartsRepresentable]) #elseif canImport(AppKit) - _ = ModelContent(role: "user", parts: NSImage()) - _ = ModelContent(role: "user", parts: [NSImage()]) + _ = try ModelContent(role: "user", parts: NSImage()) + _ = try ModelContent(role: "user", parts: [NSImage()]) // Note: without `as [any PartsRepresentable]` this will fail to compile with "Cannot convert // value of type `[Any]` to expected type `[any PartsRepresentable]`. Not sure if there's a // way we can get it to work. - _ = ModelContent(parts: [str, NSImage()] as [any PartsRepresentable]) + _ = try ModelContent(parts: [str, NSImage()] as [any PartsRepresentable]) // Alternatively, you can explicitly declare the type in a variable and pass it in. let representable2: [any PartsRepresentable] = [str, NSImage()] - _ = ModelContent(parts: representable2) + _ = try ModelContent(parts: representable2) _ = - ModelContent(parts: [str, NSImage(), + try ModelContent(parts: [str, NSImage(), ModelContent.Part.text(str)] as [any PartsRepresentable]) #endif @@ -124,14 +124,14 @@ final class GoogleGenerativeAITests: XCTestCase { let _: CountTokensResponse = try await genAI.countTokens("What color is the Sky?", UIImage()) let _: CountTokensResponse = try await genAI.countTokens([ - ModelContent("What color is the Sky?", UIImage()), - ModelContent(UIImage(), "What color is the Sky?", UIImage()), + try ModelContent("What color is the Sky?", UIImage()), + try ModelContent(UIImage(), "What color is the Sky?", UIImage()), ]) #endif // Chat _ = genAI.startChat() - _ = genAI.startChat(history: [ModelContent(parts: "abc")]) + _ = genAI.startChat(history: [try ModelContent(parts: "abc")]) } // Result builder alternative