From 5d8b31cc64439c8ae10e70eb0b65b5c1d828b1b6 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 27 Feb 2024 10:21:44 -0500 Subject: [PATCH 01/12] Add types to represent JSON values (#112) --- Sources/GoogleAI/JSONValue.swift | 71 ++++++++++++++++++++ Tests/GoogleAITests/JSONValueTests.swift | 85 ++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 Sources/GoogleAI/JSONValue.swift create mode 100644 Tests/GoogleAITests/JSONValueTests.swift diff --git a/Sources/GoogleAI/JSONValue.swift b/Sources/GoogleAI/JSONValue.swift new file mode 100644 index 0000000..b6166bb --- /dev/null +++ b/Sources/GoogleAI/JSONValue.swift @@ -0,0 +1,71 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +/// A collection of name-value pairs representing a JSON object. +/// +/// This may be decoded from, or encoded to, a +/// [`google.protobuf.Struct`](https://protobuf.dev/reference/protobuf/google.protobuf/#struct). +public typealias JSONObject = [String: JSONValue] + +/// Represents a value in one of JSON's data types. +/// +/// This may be decoded from, or encoded to, a +/// [`google.protobuf.Value`](https://protobuf.dev/reference/protobuf/google.protobuf/#value). +public enum JSONValue { + /// A `null` value. + case null + + /// A numeric value. + case number(Double) + + /// A string value. + case string(String) + + /// A boolean value. + case bool(Bool) + + /// A JSON object. + case object(JSONObject) + + /// An array of `JSONValue`s. + case array([JSONValue]) +} + +extension JSONValue: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let numberValue = try? container.decode(Double.self) { + self = .number(numberValue) + } else if let stringValue = try? container.decode(String.self) { + self = .string(stringValue) + } else if let boolValue = try? container.decode(Bool.self) { + self = .bool(boolValue) + } else if let objectValue = try? container.decode(JSONObject.self) { + self = .object(objectValue) + } else if let arrayValue = try? container.decode([JSONValue].self) { + self = .array(arrayValue) + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Failed to decode JSON value." + ) + } + } +} + +extension JSONValue: Equatable {} diff --git a/Tests/GoogleAITests/JSONValueTests.swift b/Tests/GoogleAITests/JSONValueTests.swift new file mode 100644 index 0000000..14f9d96 --- /dev/null +++ b/Tests/GoogleAITests/JSONValueTests.swift @@ -0,0 +1,85 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import XCTest + +@testable import GoogleGenerativeAI + +final class JSONValueTests: XCTestCase { + func testDecodeNull() throws { + let jsonData = try XCTUnwrap("null".data(using: .utf8)) + + let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .null) + } + + func testDecodeNumber() throws { + let expectedNumber = 3.14159 + let jsonData = try XCTUnwrap("\(expectedNumber)".data(using: .utf8)) + + let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .number(expectedNumber)) + } + + func testDecodeString() throws { + let expectedString = "hello-world" + let jsonData = try XCTUnwrap("\"\(expectedString)\"".data(using: .utf8)) + + let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .string(expectedString)) + } + + func testDecodeBool() throws { + let expectedBool = true + let jsonData = try XCTUnwrap("\(expectedBool)".data(using: .utf8)) + + let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .bool(expectedBool)) + } + + func testDecodeObject() throws { + let numberKey = "pi" + let numberValue = 3.14159 + let stringKey = "hello" + let stringValue = "world" + let expectedObject: JSONObject = [ + numberKey: .number(numberValue), + stringKey: .string(stringValue), + ] + let json = """ + { + "\(numberKey)": \(numberValue), + "\(stringKey)": "\(stringValue)" + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .object(expectedObject)) + } + + func testDecodeArray() throws { + let numberValue = 3.14159 + let expectedArray: [JSONValue] = [.null, .number(numberValue)] + let jsonData = try XCTUnwrap("[ null, \(numberValue) ]".data(using: .utf8)) + + let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .array(expectedArray)) + } +} From 45bc200e2afe9861067d7dcee0959f61ed911ee4 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 28 Feb 2024 13:40:31 -0500 Subject: [PATCH 02/12] Add `Encodable` conformance to `JSONValue` (#113) --- Sources/GoogleAI/JSONValue.swift | 25 +++++++ Tests/GoogleAITests/JSONValueTests.swift | 94 +++++++++++++++++++----- 2 files changed, 102 insertions(+), 17 deletions(-) diff --git a/Sources/GoogleAI/JSONValue.swift b/Sources/GoogleAI/JSONValue.swift index b6166bb..5ce52cd 100644 --- a/Sources/GoogleAI/JSONValue.swift +++ b/Sources/GoogleAI/JSONValue.swift @@ -68,4 +68,29 @@ extension JSONValue: Decodable { } } +extension JSONValue: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .null: + try container.encodeNil() + case let .number(numberValue): + // Convert to `Decimal` before encoding for consistent floating-point serialization across + // platforms. E.g., `Double` serializes 3.14159 as 3.1415899999999999 in some cases and + // 3.14159 in others. See + // https://forums.swift.org/t/jsonencoder-encodable-floating-point-rounding-error/41390/4 for + // more details. + try container.encode(Decimal(numberValue)) + case let .string(stringValue): + try container.encode(stringValue) + case let .bool(boolValue): + try container.encode(boolValue) + case let .object(objectValue): + try container.encode(objectValue) + case let .array(arrayValue): + try container.encode(arrayValue) + } + } +} + extension JSONValue: Equatable {} diff --git a/Tests/GoogleAITests/JSONValueTests.swift b/Tests/GoogleAITests/JSONValueTests.swift index 14f9d96..19c871e 100644 --- a/Tests/GoogleAITests/JSONValueTests.swift +++ b/Tests/GoogleAITests/JSONValueTests.swift @@ -16,46 +16,53 @@ import XCTest @testable import GoogleGenerativeAI final class JSONValueTests: XCTestCase { + let decoder = JSONDecoder() + let encoder = JSONEncoder() + + let numberKey = "pi" + let numberValue = 3.14159 + let numberValueEncoded = "3.14159" + let stringKey = "hello" + let stringValue = "Hello, world!" + + override func setUp() { + encoder.outputFormatting = .sortedKeys + } + func testDecodeNull() throws { let jsonData = try XCTUnwrap("null".data(using: .utf8)) - let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) XCTAssertEqual(jsonObject, .null) } func testDecodeNumber() throws { - let expectedNumber = 3.14159 - let jsonData = try XCTUnwrap("\(expectedNumber)".data(using: .utf8)) + let jsonData = try XCTUnwrap("\(numberValue)".data(using: .utf8)) - let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) - XCTAssertEqual(jsonObject, .number(expectedNumber)) + XCTAssertEqual(jsonObject, .number(numberValue)) } func testDecodeString() throws { - let expectedString = "hello-world" - let jsonData = try XCTUnwrap("\"\(expectedString)\"".data(using: .utf8)) + let jsonData = try XCTUnwrap("\"\(stringValue)\"".data(using: .utf8)) - let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) - XCTAssertEqual(jsonObject, .string(expectedString)) + XCTAssertEqual(jsonObject, .string(stringValue)) } func testDecodeBool() throws { let expectedBool = true let jsonData = try XCTUnwrap("\(expectedBool)".data(using: .utf8)) - let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) XCTAssertEqual(jsonObject, .bool(expectedBool)) } func testDecodeObject() throws { - let numberKey = "pi" - let numberValue = 3.14159 - let stringKey = "hello" - let stringValue = "world" let expectedObject: JSONObject = [ numberKey: .number(numberValue), stringKey: .string(stringValue), @@ -68,18 +75,71 @@ final class JSONValueTests: XCTestCase { """ let jsonData = try XCTUnwrap(json.data(using: .utf8)) - let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) XCTAssertEqual(jsonObject, .object(expectedObject)) } func testDecodeArray() throws { - let numberValue = 3.14159 let expectedArray: [JSONValue] = [.null, .number(numberValue)] let jsonData = try XCTUnwrap("[ null, \(numberValue) ]".data(using: .utf8)) - let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData)) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) XCTAssertEqual(jsonObject, .array(expectedArray)) } + + func testEncodeNull() throws { + let jsonData = try encoder.encode(JSONValue.null) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual(json, "null") + } + + func testEncodeNumber() throws { + let jsonData = try encoder.encode(JSONValue.number(numberValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual(json, "\(numberValue)") + } + + func testEncodeString() throws { + let jsonData = try encoder.encode(JSONValue.string(stringValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual(json, "\"\(stringValue)\"") + } + + func testEncodeBool() throws { + let boolValue = true + + let jsonData = try encoder.encode(JSONValue.bool(boolValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual(json, "\(boolValue)") + } + + func testEncodeObject() throws { + let objectValue: JSONObject = [ + numberKey: .number(numberValue), + stringKey: .string(stringValue), + ] + + let jsonData = try encoder.encode(JSONValue.object(objectValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual( + json, + "{\"\(stringKey)\":\"\(stringValue)\",\"\(numberKey)\":\(numberValueEncoded)}" + ) + } + + func testEncodeArray() throws { + let arrayValue: [JSONValue] = [.null, .number(numberValue)] + + let jsonData = try encoder.encode(JSONValue.array(arrayValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual(json, "[null,\(numberValueEncoded)]") + } } From 667eccf5661d5bbe22219ca6b178c3f098998f0e Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 5 Mar 2024 19:19:10 +0000 Subject: [PATCH 03/12] Add `FunctionCall` decoding (#114) --- Sources/GoogleAI/Chat.swift | 4 ++ Sources/GoogleAI/FunctionCalling.swift | 41 ++++++++++++ Sources/GoogleAI/ModelContent.swift | 11 ++- ...success-function-call-empty-arguments.json | 19 ++++++ ...ry-success-function-call-no-arguments.json | 19 ++++++ ...-success-function-call-with-arguments.json | 22 ++++++ .../GoogleAITests/GenerativeModelTests.swift | 67 +++++++++++++++++++ 7 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 Sources/GoogleAI/FunctionCalling.swift create mode 100644 Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-empty-arguments.json create mode 100644 Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-no-arguments.json create mode 100644 Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-with-arguments.json diff --git a/Sources/GoogleAI/Chat.swift b/Sources/GoogleAI/Chat.swift index c7cfb85..6e7e885 100644 --- a/Sources/GoogleAI/Chat.swift +++ b/Sources/GoogleAI/Chat.swift @@ -162,6 +162,10 @@ public class Chat { } parts.append(part) + + case .functionCall: + // TODO(andrewheard): Add function call to the chat history when encoding is implemented. + fatalError("Function calling not yet implemented in chat.") } } } diff --git a/Sources/GoogleAI/FunctionCalling.swift b/Sources/GoogleAI/FunctionCalling.swift new file mode 100644 index 0000000..5d8ded5 --- /dev/null +++ b/Sources/GoogleAI/FunctionCalling.swift @@ -0,0 +1,41 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +/// A predicted function call returned from the model. +public struct FunctionCall: Equatable { + /// The name of the function to call. + let name: String + + /// The function parameters and values. + let args: JSONObject +} + +extension FunctionCall: Decodable { + enum CodingKeys: CodingKey { + case name + case args + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + if let args = try container.decodeIfPresent(JSONObject.self, forKey: .args) { + self.args = args + } else { + args = JSONObject() + } + } +} diff --git a/Sources/GoogleAI/ModelContent.swift b/Sources/GoogleAI/ModelContent.swift index 44648c5..2ce8876 100644 --- a/Sources/GoogleAI/ModelContent.swift +++ b/Sources/GoogleAI/ModelContent.swift @@ -25,6 +25,7 @@ public struct ModelContent: Codable, Equatable { enum CodingKeys: String, CodingKey { case text case inlineData + case functionCall } enum InlineDataKeys: String, CodingKey { @@ -38,6 +39,9 @@ public struct ModelContent: Codable, Equatable { /// Data with a specified media type. Not all media types may be supported by the AI model. case data(mimetype: String, Data) + /// A predicted function call returned from the model. + case functionCall(FunctionCall) + // MARK: Convenience Initializers /// Convenience function for populating a Part with JPEG data. @@ -64,6 +68,9 @@ public struct ModelContent: Codable, Equatable { ) try inlineDataContainer.encode(mimetype, forKey: .mimeType) try inlineDataContainer.encode(bytes, forKey: .bytes) + case .functionCall: + // TODO(andrewheard): Encode FunctionCalls when when encoding is implemented. + fatalError("FunctionCall encoding not implemented.") } } @@ -79,10 +86,12 @@ public struct ModelContent: Codable, Equatable { let mimetype = try dataContainer.decode(String.self, forKey: .mimeType) let bytes = try dataContainer.decode(Data.self, forKey: .bytes) self = .data(mimetype: mimetype, bytes) + } else if values.contains(.functionCall) { + self = try .functionCall(values.decode(FunctionCall.self, forKey: .functionCall)) } else { throw DecodingError.dataCorrupted(.init( codingPath: [CodingKeys.text, CodingKeys.inlineData], - debugDescription: "Neither text or inline data was found." + debugDescription: "No text, inline data or function call was found." )) } } diff --git a/Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-empty-arguments.json b/Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-empty-arguments.json new file mode 100644 index 0000000..703bdf8 --- /dev/null +++ b/Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-empty-arguments.json @@ -0,0 +1,19 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "current_time" + } + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ] +} + diff --git a/Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-no-arguments.json b/Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-no-arguments.json new file mode 100644 index 0000000..05f4f4d --- /dev/null +++ b/Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-no-arguments.json @@ -0,0 +1,19 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "current_time", + "args": {} + } + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ] +} diff --git a/Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-with-arguments.json b/Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-with-arguments.json new file mode 100644 index 0000000..025735a --- /dev/null +++ b/Tests/GoogleAITests/GenerateContentResponses/unary-success-function-call-with-arguments.json @@ -0,0 +1,22 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "sum", + "args": { + "y": 5, + "x": 4 + } + } + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ] +} diff --git a/Tests/GoogleAITests/GenerativeModelTests.swift b/Tests/GoogleAITests/GenerativeModelTests.swift index 2b90ec1..41f6844 100644 --- a/Tests/GoogleAITests/GenerativeModelTests.swift +++ b/Tests/GoogleAITests/GenerativeModelTests.swift @@ -169,6 +169,73 @@ final class GenerativeModelTests: XCTestCase { _ = try await model.generateContent(testPrompt) } + func testGenerateContent_success_functionCall_emptyArguments() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-function-call-empty-arguments", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + guard case let .functionCall(functionCall) = part else { + XCTFail("Part is not a FunctionCall.") + return + } + XCTAssertEqual(functionCall.name, "current_time") + XCTAssertTrue(functionCall.args.isEmpty) + } + + func testGenerateContent_success_functionCall_noArguments() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-function-call-no-arguments", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + guard case let .functionCall(functionCall) = part else { + XCTFail("Part is not a FunctionCall.") + return + } + XCTAssertEqual(functionCall.name, "current_time") + XCTAssertTrue(functionCall.args.isEmpty) + } + + func testGenerateContent_success_functionCall_withArguments() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-function-call-with-arguments", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + guard case let .functionCall(functionCall) = part else { + XCTFail("Part is not a FunctionCall.") + return + } + XCTAssertEqual(functionCall.name, "sum") + XCTAssertEqual(functionCall.args.count, 2) + let argX = try XCTUnwrap(functionCall.args["x"]) + XCTAssertEqual(argX, .number(4)) + let argY = try XCTUnwrap(functionCall.args["y"]) + XCTAssertEqual(argY, .number(5)) + } + func testGenerateContent_failure_invalidAPIKey() async throws { let expectedStatusCode = 400 MockURLProtocol From d1e97b63bb3c02a90efdf5e8f08d3cbe67ea47b6 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 11 Mar 2024 12:09:11 -0400 Subject: [PATCH 04/12] Function calling prototype --- .../Sources/GenerateContent.swift | 150 +++++++++++++++++- Sources/GoogleAI/Chat.swift | 28 +++- Sources/GoogleAI/FunctionCalling.swift | 104 +++++++++++- Sources/GoogleAI/GenerateContentRequest.swift | 2 + Sources/GoogleAI/GenerativeModel.swift | 34 ++++ Sources/GoogleAI/ModelContent.swift | 10 +- 6 files changed, 318 insertions(+), 10 deletions(-) diff --git a/Examples/GenerativeAICLI/Sources/GenerateContent.swift b/Examples/GenerativeAICLI/Sources/GenerateContent.swift index ab71c43..87893b8 100644 --- a/Examples/GenerativeAICLI/Sources/GenerateContent.swift +++ b/Examples/GenerativeAICLI/Sources/GenerateContent.swift @@ -70,9 +70,26 @@ struct GenerateContent: AsyncParsableCommand { name: modelNameOrDefault(), apiKey: apiKey, generationConfig: config, - safetySettings: safetySettings + safetySettings: safetySettings, + tools: [Tool(functionDeclarations: [ + FunctionDeclaration( + name: "get_exchange_rate", + description: "Get the exchange rate for currencies between countries", + parameters: getExchangeRateSchema(), + function: getExchangeRateWrapper + ), + FunctionDeclaration( + name: "sum_integer_list", + description: "Sums a list of integer values", + parameters: sumIntegerListSchema(), + function: sumIntegerListWrapper + ), + ])], + requestOptions: RequestOptions(apiVersion: "v1beta") ) + let chat = model.startChat() + var parts = [ModelContent.Part]() if let textPrompt = textPrompt { @@ -96,7 +113,7 @@ struct GenerateContent: AsyncParsableCommand { let input = [ModelContent(parts: parts)] if isStreaming { - let contentStream = model.generateContentStream(input) + let contentStream = chat.sendMessageStream(input) print("Generated Content :") for try await content in contentStream { if let text = content.text { @@ -104,7 +121,8 @@ struct GenerateContent: AsyncParsableCommand { } } } else { - let content = try await model.generateContent(input) + // Unary generate content + let content = try await chat.sendMessage(input) if let text = content.text { print("Generated Content:\n\(text)") } @@ -123,6 +141,132 @@ struct GenerateContent: AsyncParsableCommand { return "gemini-1.0-pro" } } + + // MARK: - Callable Functions + + // Returns exchange rates from the Frankfurter API + // This is an example function that a developer might provide. + func getExchangeRate(amount: Double, date: String, from: String, + to: String) async throws -> String { + var urlComponents = URLComponents(string: "https://api.frankfurter.app")! + urlComponents.path = "/\(date)" + urlComponents.queryItems = [ + .init(name: "amount", value: String(amount)), + .init(name: "from", value: from), + .init(name: "to", value: to), + ] + + let (data, _) = try await URLSession.shared.data(from: urlComponents.url!) + return String(data: data, encoding: .utf8)! + } + + // This is a wrapper for the `getExchangeRate` function. + func getExchangeRateWrapper(args: JSONObject) async throws -> JSONObject { + // 1. Validate and extract the parameters provided by the model (from a `FunctionCall`) + guard case let .string(date) = args["currency_date"] else { + fatalError() + } + guard case let .string(from) = args["currency_from"] else { + fatalError() + } + guard case let .string(to) = args["currency_to"] else { + fatalError() + } + guard case let .number(amount) = args["amount"] else { + fatalError() + } + + // 2. Call the wrapped function + let response = try await getExchangeRate(amount: amount, date: date, from: from, to: to) + + // 3. Return the exchange rates as a JSON object (returned to the model in a `FunctionResponse`) + return ["content": .string(response)] + } + + // Returns the schema of the `getExchangeRate` function + func getExchangeRateSchema() -> Schema { + return Schema( + type: .object, + properties: [ + "currency_date": Schema( + type: .string, + description: """ + A date that must always be in YYYY-MM-DD format or the value 'latest' if a time period + is not specified + """ + ), + "currency_from": Schema( + type: .string, + description: "The currency to convert from in ISO 4217 format" + ), + "currency_to": Schema( + type: .string, + description: "The currency to convert to in ISO 4217 format" + ), + "amount": Schema( + type: .number, + description: "The amount of currency to convert as a double value" + ), + ], + required: ["currency_date", "currency_from", "currency_to", "amount"] + ) + } + + // Returns the sum of a list of integers. + // This is an example function that a developer could provide. + func sumIntegerList(_ integers: [Int]) -> Int { + var sum = 0 + for integer in integers { + sum += integer + } + return sum + } + + // This is a wrapper for the `sumIntegerList` function. + func sumIntegerListWrapper(args: JSONObject) -> JSONObject { + // 1. Validate and extract the parameters provided by the model (from a `FunctionCall`) + guard let values = args["values"] else { + fatalError("Expected a `values` parameter.") + } + guard case let .array(argArray) = values else { + fatalError("Expected `values` to be an array.") + } + + var integerArray = [Int]() + for arg in argArray { + guard case let .number(number) = arg else { + fatalError("Expected `values` array elements to be numbers.") + } + guard let integer = Int(exactly: number) else { + fatalError("Expected `values` array numbers to be integers.") + } + integerArray.append(integer) + } + + // 2. Call the wrapped function + let sum = sumIntegerList(integerArray) + + // 3. Return the sum as a JSON object (to be returned to the model in a `FunctionResponse`) + return ["sum": .number(Double(sum))] + } + + // Returns the schema of the `sumIntegerList` function. + func sumIntegerListSchema() -> Schema { + return Schema( + type: .object, + properties: [ + "values": Schema( + type: .array, + description: "The integer values to sum", + items: Schema( + type: .integer, + format: "int64" + ) + ), + ], + required: ["values"] + ) + } } enum CLIError: Error { diff --git a/Sources/GoogleAI/Chat.swift b/Sources/GoogleAI/Chat.swift index 6e7e885..74fdebd 100644 --- a/Sources/GoogleAI/Chat.swift +++ b/Sources/GoogleAI/Chat.swift @@ -70,10 +70,32 @@ public class Chat { // Make sure we inject the role into the content received. let toAdd = ModelContent(role: "model", parts: reply.parts) + var functionResponses = [FunctionResponse]() + for part in reply.parts { + if case let .functionCall(functionCall) = part { + try functionResponses.append(await model.executeFunction(functionCall: functionCall)) + } + } + + // Call the functions requested by the model, if any. + let functionResponseContent = try ModelContent( + role: "function", + functionResponses.map { functionResponse in + ModelContent.Part.functionResponse(functionResponse) + } + ) + // Append the request and successful result to history, then return the value. history.append(contentsOf: newContent) history.append(toAdd) - return result + + // If no function calls requested, return the results. + if functionResponses.isEmpty { + return result + } + + // Re-send the message with the function responses. + return try await sendMessage([functionResponseContent]) } /// See ``sendMessageStream(_:)-4abs3``. @@ -166,6 +188,10 @@ public class Chat { case .functionCall: // TODO(andrewheard): Add function call to the chat history when encoding is implemented. fatalError("Function calling not yet implemented in chat.") + + case .functionResponse: + // TODO(andrewheard): Add function response to chat history when encoding is implemented. + fatalError("Function calling not yet implemented in chat.") } } } diff --git a/Sources/GoogleAI/FunctionCalling.swift b/Sources/GoogleAI/FunctionCalling.swift index 5d8ded5..664ca29 100644 --- a/Sources/GoogleAI/FunctionCalling.swift +++ b/Sources/GoogleAI/FunctionCalling.swift @@ -15,14 +15,97 @@ import Foundation /// A predicted function call returned from the model. -public struct FunctionCall: Equatable { +/// +/// REST Docs: https://ai.google.dev/api/rest/v1beta/Content#functioncall +public struct FunctionCall: Equatable, Encodable { /// The name of the function to call. - let name: String + public let name: String /// The function parameters and values. - let args: JSONObject + public let args: JSONObject } +// REST Docs: https://ai.google.dev/api/rest/v1beta/Tool#schema +public class Schema: Encodable { + let type: DataType + + let format: String? + + let description: String? + + let nullable: Bool? + + let enumValues: [String]? + + let items: Schema? + + let properties: [String: Schema]? + + let required: [String]? + + public init(type: DataType, format: String? = nil, description: String? = nil, + nullable: Bool? = nil, + enumValues: [String]? = nil, items: Schema? = nil, + properties: [String: Schema]? = nil, + required: [String]? = nil) { + self.type = type + self.format = format + self.description = description + self.nullable = nullable + self.enumValues = enumValues + self.items = items + self.properties = properties + self.required = required + } +} + +// REST Docs: https://ai.google.dev/api/rest/v1beta/Tool#Type +public enum DataType: String, Encodable { + case string = "STRING" + case number = "NUMBER" + case integer = "INTEGER" + case boolean = "BOOLEAN" + case array = "ARRAY" + case object = "OBJECT" +} + +// REST Docs: https://ai.google.dev/api/rest/v1beta/Tool#FunctionDeclaration +public struct FunctionDeclaration { + let name: String + + let description: String + + let parameters: Schema + + let function: ((JSONObject) async throws -> JSONObjectRepresentable)? + + public init(name: String, description: String, parameters: Schema, + function: ((JSONObject) async throws -> JSONObjectRepresentable)?) { + self.name = name + self.description = description + self.parameters = parameters + self.function = function + } +} + +// REST Docs: https://ai.google.dev/api/rest/v1beta/Tool +public struct Tool: Encodable { + let functionDeclarations: [FunctionDeclaration]? + + public init(functionDeclarations: [FunctionDeclaration]?) { + self.functionDeclarations = functionDeclarations + } +} + +// REST Docs: https://ai.google.dev/api/rest/v1beta/Content#functionresponse +public struct FunctionResponse: Equatable, Encodable { + let name: String + + let response: JSONObject +} + +// MARK: - Codable Conformance + extension FunctionCall: Decodable { enum CodingKeys: CodingKey { case name @@ -39,3 +122,18 @@ extension FunctionCall: Decodable { } } } + +extension FunctionDeclaration: Encodable { + enum CodingKeys: String, CodingKey { + case name + case description + case parameters + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(description, forKey: .description) + try container.encode(parameters, forKey: .parameters) + } +} diff --git a/Sources/GoogleAI/GenerateContentRequest.swift b/Sources/GoogleAI/GenerateContentRequest.swift index 417260b..535ac4d 100644 --- a/Sources/GoogleAI/GenerateContentRequest.swift +++ b/Sources/GoogleAI/GenerateContentRequest.swift @@ -21,6 +21,7 @@ struct GenerateContentRequest { let contents: [ModelContent] let generationConfig: GenerationConfig? let safetySettings: [SafetySetting]? + let tools: [Tool]? let isStreaming: Bool let options: RequestOptions } @@ -31,6 +32,7 @@ extension GenerateContentRequest: Encodable { case contents case generationConfig case safetySettings + case tools } } diff --git a/Sources/GoogleAI/GenerativeModel.swift b/Sources/GoogleAI/GenerativeModel.swift index 03e0191..8842522 100644 --- a/Sources/GoogleAI/GenerativeModel.swift +++ b/Sources/GoogleAI/GenerativeModel.swift @@ -36,6 +36,8 @@ public final class GenerativeModel { /// The safety settings to be used for prompts. let safetySettings: [SafetySetting]? + let tools: [Tool]? + /// Configuration parameters for sending requests to the backend. let requestOptions: RequestOptions @@ -52,12 +54,14 @@ public final class GenerativeModel { apiKey: String, generationConfig: GenerationConfig? = nil, safetySettings: [SafetySetting]? = nil, + tools: [Tool]? = nil, requestOptions: RequestOptions = RequestOptions()) { self.init( name: name, apiKey: apiKey, generationConfig: generationConfig, safetySettings: safetySettings, + tools: tools, requestOptions: requestOptions, urlSession: .shared ) @@ -68,12 +72,14 @@ public final class GenerativeModel { apiKey: String, generationConfig: GenerationConfig? = nil, safetySettings: [SafetySetting]? = nil, + tools: [Tool]? = nil, requestOptions: RequestOptions = RequestOptions(), urlSession: URLSession) { modelResourceName = GenerativeModel.modelResourceName(name: name) generativeAIService = GenerativeAIService(apiKey: apiKey, urlSession: urlSession) self.generationConfig = generationConfig self.safetySettings = safetySettings + self.tools = tools self.requestOptions = requestOptions Logging.default.info(""" @@ -119,6 +125,7 @@ public final class GenerativeModel { contents: content(), generationConfig: generationConfig, safetySettings: safetySettings, + tools: tools, isStreaming: false, options: requestOptions) response = try await generativeAIService.loadRequest(request: generateContentRequest) @@ -190,6 +197,7 @@ public final class GenerativeModel { contents: evaluatedContent, generationConfig: generationConfig, safetySettings: safetySettings, + tools: tools, isStreaming: true, options: requestOptions) @@ -270,6 +278,30 @@ public final class GenerativeModel { } } + func executeFunction(functionCall: FunctionCall) async throws -> FunctionResponse { + guard let tools = tools else { + throw GenerateContentError.internalError(underlying: FunctionCallError()) + } + guard let tool = tools.first(where: { tool in + tool.functionDeclarations != nil + }) else { + throw GenerateContentError.internalError(underlying: FunctionCallError()) + } + guard let functionDeclaration = tool.functionDeclarations?.first(where: { functionDeclaration in + functionDeclaration.name == functionCall.name + }) else { + throw GenerateContentError.internalError(underlying: FunctionCallError()) + } + guard let function = functionDeclaration.function else { + throw GenerateContentError.internalError(underlying: FunctionCallError()) + } + + return try FunctionResponse( + name: functionCall.name, + response: await function(functionCall.args).jsonObject + ) + } + /// Returns a model resource name of the form "models/model-name" based on `name`. private static func modelResourceName(name: String) -> String { if name.contains("/") { @@ -299,3 +331,5 @@ public final class GenerativeModel { public enum CountTokensError: Error { case internalError(underlying: Error) } + +struct FunctionCallError: Error {} diff --git a/Sources/GoogleAI/ModelContent.swift b/Sources/GoogleAI/ModelContent.swift index 2ce8876..4aefe7b 100644 --- a/Sources/GoogleAI/ModelContent.swift +++ b/Sources/GoogleAI/ModelContent.swift @@ -26,6 +26,7 @@ public struct ModelContent: Codable, Equatable { case text case inlineData case functionCall + case functionResponse } enum InlineDataKeys: String, CodingKey { @@ -42,6 +43,8 @@ public struct ModelContent: Codable, Equatable { /// A predicted function call returned from the model. case functionCall(FunctionCall) + case functionResponse(FunctionResponse) + // MARK: Convenience Initializers /// Convenience function for populating a Part with JPEG data. @@ -68,9 +71,10 @@ public struct ModelContent: Codable, Equatable { ) try inlineDataContainer.encode(mimetype, forKey: .mimeType) try inlineDataContainer.encode(bytes, forKey: .bytes) - case .functionCall: - // TODO(andrewheard): Encode FunctionCalls when when encoding is implemented. - fatalError("FunctionCall encoding not implemented.") + case let .functionCall(functionCall): + try container.encode(functionCall, forKey: .functionCall) + case let .functionResponse(functionResponse): + try container.encode(functionResponse, forKey: .functionResponse) } } From 3d0642dcc8a0cabf83db11dcfae145e3e60413e9 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 11 Mar 2024 16:14:13 -0400 Subject: [PATCH 05/12] Add prototype for function calling --- .../Sources/GenerateContent.swift | 94 +++++++++++++++---- Sources/GoogleAI/Chat.swift | 34 +------ Sources/GoogleAI/FunctionCalling.swift | 11 ++- Sources/GoogleAI/GenerativeModel.swift | 26 ----- 4 files changed, 82 insertions(+), 83 deletions(-) diff --git a/Examples/GenerativeAICLI/Sources/GenerateContent.swift b/Examples/GenerativeAICLI/Sources/GenerateContent.swift index 87893b8..d936080 100644 --- a/Examples/GenerativeAICLI/Sources/GenerateContent.swift +++ b/Examples/GenerativeAICLI/Sources/GenerateContent.swift @@ -44,6 +44,12 @@ struct GenerateContent: AsyncParsableCommand { help: "Enable additional debug logging." ) var debugLogEnabled = false + // Function calls pending processing + var functionCalls = [FunctionCall]() + + // Input to the model + var input = [ModelContent]() + mutating func validate() throws { if textPrompt == nil && imageURL == nil { throw ValidationError( @@ -75,21 +81,17 @@ struct GenerateContent: AsyncParsableCommand { FunctionDeclaration( name: "get_exchange_rate", description: "Get the exchange rate for currencies between countries", - parameters: getExchangeRateSchema(), - function: getExchangeRateWrapper + parameters: getExchangeRateSchema() ), FunctionDeclaration( name: "sum_integer_list", description: "Sums a list of integer values", - parameters: sumIntegerListSchema(), - function: sumIntegerListWrapper + parameters: sumIntegerListSchema() ), ])], requestOptions: RequestOptions(apiVersion: "v1beta") ) - let chat = model.startChat() - var parts = [ModelContent.Part]() if let textPrompt = textPrompt { @@ -110,28 +112,80 @@ struct GenerateContent: AsyncParsableCommand { parts.append(.data(mimetype: mimeType, imageData)) } - let input = [ModelContent(parts: parts)] + input = [ModelContent(parts: parts)] - if isStreaming { - let contentStream = chat.sendMessageStream(input) - print("Generated Content :") - for try await content in contentStream { - if let text = content.text { - print(text) + repeat { + try await processFunctionCalls() + + if isStreaming { + let contentStream = model.generateContentStream(input) + print("Generated Content :") + for try await content in contentStream { + processResponseContent(content: content) } + } else { + // Unary generate content + let content = try await model.generateContent(input) + print("Generated Content:") + processResponseContent(content: content) } - } else { - // Unary generate content - let content = try await chat.sendMessage(input) - if let text = content.text { - print("Generated Content:\n\(text)") - } - } + } while !functionCalls.isEmpty } catch { print("Generate Content Error: \(error)") } } + mutating func processResponseContent(content: GenerateContentResponse) { + guard let candidate = content.candidates.first else { + fatalError("No candidate.") + } + + for part in candidate.content.parts { + switch part { + case let .text(text): + print(text) + case .data: + fatalError("Inline data not supported.") + case let .functionCall(functionCall): + functionCalls.append(functionCall) + case let .functionResponse(functionResponse): + print("FunctionResponse: \(functionResponse)") + } + } + } + + mutating func processFunctionCalls() async throws { + for functionCall in functionCalls { + input.append(ModelContent( + role: "model", + parts: [ModelContent.Part.functionCall(functionCall)] + )) + switch functionCall.name { + case "sum_integer_list": + let sum = sumIntegerListWrapper(args: functionCall.args) + input.append(ModelContent( + role: "function", + parts: [ModelContent.Part.functionResponse(FunctionResponse( + name: "sum_integer_list", + response: sum + ))] + )) + case "get_exchange_rate": + let exchangeRates = try await getExchangeRateWrapper(args: functionCall.args) + input.append(ModelContent( + role: "function", + parts: [ModelContent.Part.functionResponse(FunctionResponse( + name: "get_exchange_rate", + response: exchangeRates + ))] + )) + default: + fatalError("Unknown function named \"\(functionCall.name)\".") + } + } + functionCalls = [] + } + func modelNameOrDefault() -> String { if let modelName = modelName { return modelName diff --git a/Sources/GoogleAI/Chat.swift b/Sources/GoogleAI/Chat.swift index 74fdebd..e443e18 100644 --- a/Sources/GoogleAI/Chat.swift +++ b/Sources/GoogleAI/Chat.swift @@ -70,32 +70,10 @@ public class Chat { // Make sure we inject the role into the content received. let toAdd = ModelContent(role: "model", parts: reply.parts) - var functionResponses = [FunctionResponse]() - for part in reply.parts { - if case let .functionCall(functionCall) = part { - try functionResponses.append(await model.executeFunction(functionCall: functionCall)) - } - } - - // Call the functions requested by the model, if any. - let functionResponseContent = try ModelContent( - role: "function", - functionResponses.map { functionResponse in - ModelContent.Part.functionResponse(functionResponse) - } - ) - // Append the request and successful result to history, then return the value. history.append(contentsOf: newContent) history.append(toAdd) - - // If no function calls requested, return the results. - if functionResponses.isEmpty { - return result - } - - // Re-send the message with the function responses. - return try await sendMessage([functionResponseContent]) + return result } /// See ``sendMessageStream(_:)-4abs3``. @@ -175,7 +153,7 @@ public class Chat { case let .text(str): combinedText += str - case .data(mimetype: _, _): + case .data, .functionCall, .functionResponse: // Don't combine it, just add to the content. If there's any text pending, add that as // a part. if !combinedText.isEmpty { @@ -184,14 +162,6 @@ public class Chat { } parts.append(part) - - case .functionCall: - // TODO(andrewheard): Add function call to the chat history when encoding is implemented. - fatalError("Function calling not yet implemented in chat.") - - case .functionResponse: - // TODO(andrewheard): Add function response to chat history when encoding is implemented. - fatalError("Function calling not yet implemented in chat.") } } } diff --git a/Sources/GoogleAI/FunctionCalling.swift b/Sources/GoogleAI/FunctionCalling.swift index 664ca29..5b5a9eb 100644 --- a/Sources/GoogleAI/FunctionCalling.swift +++ b/Sources/GoogleAI/FunctionCalling.swift @@ -77,14 +77,10 @@ public struct FunctionDeclaration { let parameters: Schema - let function: ((JSONObject) async throws -> JSONObjectRepresentable)? - - public init(name: String, description: String, parameters: Schema, - function: ((JSONObject) async throws -> JSONObjectRepresentable)?) { + public init(name: String, description: String, parameters: Schema) { self.name = name self.description = description self.parameters = parameters - self.function = function } } @@ -102,6 +98,11 @@ public struct FunctionResponse: Equatable, Encodable { let name: String let response: JSONObject + + public init(name: String, response: JSONObject) { + self.name = name + self.response = response + } } // MARK: - Codable Conformance diff --git a/Sources/GoogleAI/GenerativeModel.swift b/Sources/GoogleAI/GenerativeModel.swift index 8842522..3384622 100644 --- a/Sources/GoogleAI/GenerativeModel.swift +++ b/Sources/GoogleAI/GenerativeModel.swift @@ -278,30 +278,6 @@ public final class GenerativeModel { } } - func executeFunction(functionCall: FunctionCall) async throws -> FunctionResponse { - guard let tools = tools else { - throw GenerateContentError.internalError(underlying: FunctionCallError()) - } - guard let tool = tools.first(where: { tool in - tool.functionDeclarations != nil - }) else { - throw GenerateContentError.internalError(underlying: FunctionCallError()) - } - guard let functionDeclaration = tool.functionDeclarations?.first(where: { functionDeclaration in - functionDeclaration.name == functionCall.name - }) else { - throw GenerateContentError.internalError(underlying: FunctionCallError()) - } - guard let function = functionDeclaration.function else { - throw GenerateContentError.internalError(underlying: FunctionCallError()) - } - - return try FunctionResponse( - name: functionCall.name, - response: await function(functionCall.args).jsonObject - ) - } - /// Returns a model resource name of the form "models/model-name" based on `name`. private static func modelResourceName(name: String) -> String { if name.contains("/") { @@ -331,5 +307,3 @@ public final class GenerativeModel { public enum CountTokensError: Error { case internalError(underlying: Error) } - -struct FunctionCallError: Error {} From 44edbb1bab0d2e87a44395e865fcb76732afcaca Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 11 Mar 2024 18:40:29 -0400 Subject: [PATCH 06/12] Use static exchange rate data --- .../Sources/GenerateContent.swift | 166 ++++-------------- Sources/GoogleAI/FunctionCalling.swift | 15 +- 2 files changed, 50 insertions(+), 131 deletions(-) diff --git a/Examples/GenerativeAICLI/Sources/GenerateContent.swift b/Examples/GenerativeAICLI/Sources/GenerateContent.swift index d936080..af38b03 100644 --- a/Examples/GenerativeAICLI/Sources/GenerateContent.swift +++ b/Examples/GenerativeAICLI/Sources/GenerateContent.swift @@ -81,12 +81,24 @@ struct GenerateContent: AsyncParsableCommand { FunctionDeclaration( name: "get_exchange_rate", description: "Get the exchange rate for currencies between countries", - parameters: getExchangeRateSchema() - ), - FunctionDeclaration( - name: "sum_integer_list", - description: "Sums a list of integer values", - parameters: sumIntegerListSchema() + parameters: Schema( + type: .object, + properties: [ + "currency_from": Schema( + type: .string, + format: "enum", + description: "The currency to convert from in ISO 4217 format", + enumValues: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"] + ), + "currency_to": Schema( + type: .string, + format: "enum", + description: "The currency to convert to in ISO 4217 format", + enumValues: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"] + ), + ], + required: ["currency_from", "currency_to"] + ) ), ])], requestOptions: RequestOptions(apiVersion: "v1beta") @@ -161,17 +173,8 @@ struct GenerateContent: AsyncParsableCommand { parts: [ModelContent.Part.functionCall(functionCall)] )) switch functionCall.name { - case "sum_integer_list": - let sum = sumIntegerListWrapper(args: functionCall.args) - input.append(ModelContent( - role: "function", - parts: [ModelContent.Part.functionResponse(FunctionResponse( - name: "sum_integer_list", - response: sum - ))] - )) case "get_exchange_rate": - let exchangeRates = try await getExchangeRateWrapper(args: functionCall.args) + let exchangeRates = getExchangeRate(args: functionCall.args) input.append(ModelContent( role: "function", parts: [ModelContent.Part.functionResponse(FunctionResponse( @@ -198,128 +201,33 @@ struct GenerateContent: AsyncParsableCommand { // MARK: - Callable Functions - // Returns exchange rates from the Frankfurter API - // This is an example function that a developer might provide. - func getExchangeRate(amount: Double, date: String, from: String, - to: String) async throws -> String { - var urlComponents = URLComponents(string: "https://api.frankfurter.app")! - urlComponents.path = "/\(date)" - urlComponents.queryItems = [ - .init(name: "amount", value: String(amount)), - .init(name: "from", value: from), - .init(name: "to", value: to), - ] - - let (data, _) = try await URLSession.shared.data(from: urlComponents.url!) - return String(data: data, encoding: .utf8)! - } - - // This is a wrapper for the `getExchangeRate` function. - func getExchangeRateWrapper(args: JSONObject) async throws -> JSONObject { + func getExchangeRate(args: JSONObject) -> JSONObject { // 1. Validate and extract the parameters provided by the model (from a `FunctionCall`) - guard case let .string(date) = args["currency_date"] else { - fatalError() - } guard case let .string(from) = args["currency_from"] else { - fatalError() + fatalError("Missing `currency_from` parameter.") } guard case let .string(to) = args["currency_to"] else { - fatalError() - } - guard case let .number(amount) = args["amount"] else { - fatalError() - } - - // 2. Call the wrapped function - let response = try await getExchangeRate(amount: amount, date: date, from: from, to: to) - - // 3. Return the exchange rates as a JSON object (returned to the model in a `FunctionResponse`) - return ["content": .string(response)] - } - - // Returns the schema of the `getExchangeRate` function - func getExchangeRateSchema() -> Schema { - return Schema( - type: .object, - properties: [ - "currency_date": Schema( - type: .string, - description: """ - A date that must always be in YYYY-MM-DD format or the value 'latest' if a time period - is not specified - """ - ), - "currency_from": Schema( - type: .string, - description: "The currency to convert from in ISO 4217 format" - ), - "currency_to": Schema( - type: .string, - description: "The currency to convert to in ISO 4217 format" - ), - "amount": Schema( - type: .number, - description: "The amount of currency to convert as a double value" - ), - ], - required: ["currency_date", "currency_from", "currency_to", "amount"] - ) - } - - // Returns the sum of a list of integers. - // This is an example function that a developer could provide. - func sumIntegerList(_ integers: [Int]) -> Int { - var sum = 0 - for integer in integers { - sum += integer + fatalError("Missing `currency_to` parameter.") } - return sum - } - // This is a wrapper for the `sumIntegerList` function. - func sumIntegerListWrapper(args: JSONObject) -> JSONObject { - // 1. Validate and extract the parameters provided by the model (from a `FunctionCall`) - guard let values = args["values"] else { - fatalError("Expected a `values` parameter.") - } - guard case let .array(argArray) = values else { - fatalError("Expected `values` to be an array.") + // 2. Get the exchange rate + let allRates: [String: [String: Double]] = [ + "AUD": ["CAD": 0.89265, "EUR": 0.6072, "GBP": 0.51714, "JPY": 97.75, "USD": 0.66379], + "CAD": ["AUD": 1.1203, "EUR": 0.68023, "GBP": 0.57933, "JPY": 109.51, "USD": 0.74362], + "EUR": ["AUD": 1.6469, "CAD": 1.4701, "GBP": 0.85168, "JPY": 160.99, "USD": 1.0932], + "GBP": ["AUD": 1.9337, "CAD": 1.7261, "EUR": 1.1741, "JPY": 189.03, "USD": 1.2836], + "JPY": ["AUD": 0.01023, "CAD": 0.00913, "EUR": 0.00621, "GBP": 0.00529, "USD": 0.00679], + "USD": ["AUD": 1.5065, "CAD": 1.3448, "EUR": 0.91475, "GBP": 0.77907, "JPY": 147.26], + ] + guard let fromRates = allRates[from] else { + return ["error": .string("No data for currency \(from).")] } - - var integerArray = [Int]() - for arg in argArray { - guard case let .number(number) = arg else { - fatalError("Expected `values` array elements to be numbers.") - } - guard let integer = Int(exactly: number) else { - fatalError("Expected `values` array numbers to be integers.") - } - integerArray.append(integer) + guard let toRate = fromRates[to] else { + return ["error": .string("No data for currency \(to).")] } - // 2. Call the wrapped function - let sum = sumIntegerList(integerArray) - - // 3. Return the sum as a JSON object (to be returned to the model in a `FunctionResponse`) - return ["sum": .number(Double(sum))] - } - - // Returns the schema of the `sumIntegerList` function. - func sumIntegerListSchema() -> Schema { - return Schema( - type: .object, - properties: [ - "values": Schema( - type: .array, - description: "The integer values to sum", - items: Schema( - type: .integer, - format: "int64" - ) - ), - ], - required: ["values"] - ) + // 3. Return the exchange rates as a JSON object (returned to the model in a `FunctionResponse`) + return ["rates": .number(toRate)] } } diff --git a/Sources/GoogleAI/FunctionCalling.swift b/Sources/GoogleAI/FunctionCalling.swift index 5b5a9eb..dc924ee 100644 --- a/Sources/GoogleAI/FunctionCalling.swift +++ b/Sources/GoogleAI/FunctionCalling.swift @@ -43,6 +43,17 @@ public class Schema: Encodable { let required: [String]? + enum CodingKeys: String, CodingKey { + case type + case format + case description + case nullable + case enumValues = "enum" + case items + case properties + case required + } + public init(type: DataType, format: String? = nil, description: String? = nil, nullable: Bool? = nil, enumValues: [String]? = nil, items: Schema? = nil, @@ -75,9 +86,9 @@ public struct FunctionDeclaration { let description: String - let parameters: Schema + let parameters: Schema? - public init(name: String, description: String, parameters: Schema) { + public init(name: String, description: String, parameters: Schema?) { self.name = name self.description = description self.parameters = parameters From ae60f57a26581e084d59244c97e55f02b30d0de5 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 14 Mar 2024 16:13:07 +0000 Subject: [PATCH 07/12] Simplify top-level schema declaration in `FunctionDeclaration` (#117) --- .../Sources/GenerateContent.swift | 33 +++++++++---------- Sources/GoogleAI/FunctionCalling.swift | 17 ++++++---- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/Examples/GenerativeAICLI/Sources/GenerateContent.swift b/Examples/GenerativeAICLI/Sources/GenerateContent.swift index af38b03..beaca99 100644 --- a/Examples/GenerativeAICLI/Sources/GenerateContent.swift +++ b/Examples/GenerativeAICLI/Sources/GenerateContent.swift @@ -81,24 +81,21 @@ struct GenerateContent: AsyncParsableCommand { FunctionDeclaration( name: "get_exchange_rate", description: "Get the exchange rate for currencies between countries", - parameters: Schema( - type: .object, - properties: [ - "currency_from": Schema( - type: .string, - format: "enum", - description: "The currency to convert from in ISO 4217 format", - enumValues: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"] - ), - "currency_to": Schema( - type: .string, - format: "enum", - description: "The currency to convert to in ISO 4217 format", - enumValues: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"] - ), - ], - required: ["currency_from", "currency_to"] - ) + parameters: [ + "currency_from": Schema( + type: .string, + format: "enum", + description: "The currency to convert from in ISO 4217 format", + enumValues: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"] + ), + "currency_to": Schema( + type: .string, + format: "enum", + description: "The currency to convert to in ISO 4217 format", + enumValues: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"] + ), + ], + requiredParameters: ["currency_from", "currency_to"] ), ])], requestOptions: RequestOptions(apiVersion: "v1beta") diff --git a/Sources/GoogleAI/FunctionCalling.swift b/Sources/GoogleAI/FunctionCalling.swift index dc924ee..5a87021 100644 --- a/Sources/GoogleAI/FunctionCalling.swift +++ b/Sources/GoogleAI/FunctionCalling.swift @@ -41,7 +41,7 @@ public class Schema: Encodable { let properties: [String: Schema]? - let required: [String]? + let requiredProperties: [String]? enum CodingKeys: String, CodingKey { case type @@ -51,14 +51,14 @@ public class Schema: Encodable { case enumValues = "enum" case items case properties - case required + case requiredProperties = "required" } public init(type: DataType, format: String? = nil, description: String? = nil, nullable: Bool? = nil, enumValues: [String]? = nil, items: Schema? = nil, properties: [String: Schema]? = nil, - required: [String]? = nil) { + requiredProperties: [String]? = nil) { self.type = type self.format = format self.description = description @@ -66,7 +66,7 @@ public class Schema: Encodable { self.enumValues = enumValues self.items = items self.properties = properties - self.required = required + self.requiredProperties = requiredProperties } } @@ -88,10 +88,15 @@ public struct FunctionDeclaration { let parameters: Schema? - public init(name: String, description: String, parameters: Schema?) { + public init(name: String, description: String, parameters: [String: Schema]?, + requiredParameters: [String]?) { self.name = name self.description = description - self.parameters = parameters + self.parameters = Schema( + type: .object, + properties: parameters, + requiredProperties: requiredParameters + ) } } From 8ddd10cfb34bbfe814f5477994edd84731e0a63b Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 26 Mar 2024 17:10:20 -0400 Subject: [PATCH 08/12] Add docs to function calling types --- Sources/GoogleAI/FunctionCalling.swift | 93 ++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/Sources/GoogleAI/FunctionCalling.swift b/Sources/GoogleAI/FunctionCalling.swift index 5a87021..7432e33 100644 --- a/Sources/GoogleAI/FunctionCalling.swift +++ b/Sources/GoogleAI/FunctionCalling.swift @@ -15,8 +15,6 @@ import Foundation /// A predicted function call returned from the model. -/// -/// REST Docs: https://ai.google.dev/api/rest/v1beta/Content#functioncall public struct FunctionCall: Equatable, Encodable { /// The name of the function to call. public let name: String @@ -25,22 +23,33 @@ public struct FunctionCall: Equatable, Encodable { public let args: JSONObject } -// REST Docs: https://ai.google.dev/api/rest/v1beta/Tool#schema +/// A `Schema` object allows the definition of input and output data types. +/// +/// These types can be objects, but also primitives and arrays. Represents a select subset of an +/// [OpenAPI 3.0 schema object](https://spec.openapis.org/oas/v3.0.3#schema). public class Schema: Encodable { + /// The data type. let type: DataType + /// The format of the data. let format: String? + /// A brief description of the parameter. let description: String? + /// Indicates if the value may be null. let nullable: Bool? + /// Possible values of the element of type ``DataType/string`` with "enum" format. let enumValues: [String]? + /// Schema of the elements of type ``DataType/array``. let items: Schema? + /// Properties of type ``DataType/object``. let properties: [String: Schema]? + /// Required properties of type ``DataType/object``. let requiredProperties: [String]? enum CodingKeys: String, CodingKey { @@ -54,6 +63,22 @@ public class Schema: Encodable { case requiredProperties = "required" } + /// Constructs a new `Schema`. + /// + /// - Parameters: + /// - type: The data type. + /// - format: The format of the data; used only for primitive datatypes. + /// Supported formats: + /// - ``DataType/integer``: int32, int64 + /// - ``DataType/number``: float, double + /// - ``DataType/string``: enum + /// - description: A brief description of the parameter; may be formatted as Markdown. + /// - nullable: Indicates if the value may be null. + /// - enumValues: Possible values of the element of type ``DataType/string`` with "enum" format. + /// For example, an enum `Direction` may be defined as `["EAST", NORTH", "SOUTH", "WEST"]`. + /// - items: Schema of the elements of type ``DataType/array``. + /// - properties: Properties of type ``DataType/object``. + /// - requiredProperties: Required properties of type ``DataType/object``. public init(type: DataType, format: String? = nil, description: String? = nil, nullable: Bool? = nil, enumValues: [String]? = nil, items: Schema? = nil, @@ -70,24 +95,52 @@ public class Schema: Encodable { } } -// REST Docs: https://ai.google.dev/api/rest/v1beta/Tool#Type +/// A data type. +/// +/// Contains the set of OpenAPI [data types](https://spec.openapis.org/oas/v3.0.3#data-types). public enum DataType: String, Encodable { + /// A `String` type. case string = "STRING" + + /// A floating-point number type. case number = "NUMBER" + + /// An integer type. case integer = "INTEGER" + + /// A boolean type. case boolean = "BOOLEAN" + + /// An array type. case array = "ARRAY" + + /// An object type. case object = "OBJECT" } -// REST Docs: https://ai.google.dev/api/rest/v1beta/Tool#FunctionDeclaration +/// Structured representation of a function declaration. +/// +/// This `FunctionDeclaration` is a representation of a block of code that can be used as a ``Tool`` +/// by the model and executed by the client. public struct FunctionDeclaration { + /// The name of the function. let name: String + /// A brief description of the function. let description: String + /// Describes the parameters to this function; must be of type ``DataType/object``. let parameters: Schema? + /// Constructs a new `FunctionDeclaration`. + /// + /// - Parameters: + /// - name: The name of the function; must be a-z, A-Z, 0-9, or contain underscores and dashes, + /// with a maximum length of 63. + /// - description: A brief description of the function. + /// - parameters: Describes the parameters to this function; the keys are parameter names and + /// the values are ``Schema`` objects describing them. + /// - requiredParameters: A list of required parameters by name. public init(name: String, description: String, parameters: [String: Schema]?, requiredParameters: [String]?) { self.name = name @@ -100,21 +153,47 @@ public struct FunctionDeclaration { } } -// REST Docs: https://ai.google.dev/api/rest/v1beta/Tool +/// Helper tools that the model may use to generate response. +/// +/// A `Tool` is a piece of code that enables the system to interact with external systems to +/// perform an action, or set of actions, outside of knowledge and scope of the model. public struct Tool: Encodable { + /// A list of `FunctionDeclarations` available to the model. let functionDeclarations: [FunctionDeclaration]? + /// Constructs a new `Tool`. + /// + /// - Parameters: + /// - functionDeclarations: A list of `FunctionDeclarations` available to the model that can be + /// used for function calling. + /// The model or system does not execute the function. Instead the defined function may be + /// returned as a ``FunctionCall`` in ``ModelContent/Part/functionCall(_:)`` with arguments to + /// the client side for execution. The model may decide to call a subset of these functions by + /// populating ``FunctionCall`` in the response. The next conversation turn may contain a + /// ``FunctionResponse`` in ``ModelContent/Part/functionResponse(_:)`` with the + /// ``ModelContent/role`` "function", providing generation context for the next model turn. public init(functionDeclarations: [FunctionDeclaration]?) { self.functionDeclarations = functionDeclarations } } -// REST Docs: https://ai.google.dev/api/rest/v1beta/Content#functionresponse +/// Result output from a ``FunctionCall``. +/// +/// Contains a string representing the `FunctionDeclaration.name` and a structured JSON object +/// containing any output from the function is used as context to the model. This should contain the +/// result of a ``FunctionCall`` made based on model prediction. public struct FunctionResponse: Equatable, Encodable { + /// The name of the function that was called. let name: String + /// The function's response. let response: JSONObject + /// Constructs a new `FunctionResponse`. + /// + /// - Parameters: + /// - name: The name of the function that was called. + /// - response: The function's response. public init(name: String, response: JSONObject) { self.name = name self.response = response From a9a17cb4b1f8e6060b6ac66fb618c13d2088cb83 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 26 Mar 2024 17:13:52 -0400 Subject: [PATCH 09/12] Fix formatting --- Sources/GoogleAI/FunctionCalling.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/GoogleAI/FunctionCalling.swift b/Sources/GoogleAI/FunctionCalling.swift index 7432e33..7e4d84d 100644 --- a/Sources/GoogleAI/FunctionCalling.swift +++ b/Sources/GoogleAI/FunctionCalling.swift @@ -101,19 +101,19 @@ public class Schema: Encodable { public enum DataType: String, Encodable { /// A `String` type. case string = "STRING" - + /// A floating-point number type. case number = "NUMBER" - + /// An integer type. case integer = "INTEGER" - + /// A boolean type. case boolean = "BOOLEAN" - + /// An array type. case array = "ARRAY" - + /// An object type. case object = "OBJECT" } From c75a49837f72098167eb7ce469c2a9d1bc9b7f3a Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 26 Mar 2024 17:15:13 -0400 Subject: [PATCH 10/12] Revert CLI tool changes --- .../Sources/GenerateContent.swift | 131 ++---------------- 1 file changed, 14 insertions(+), 117 deletions(-) diff --git a/Examples/GenerativeAICLI/Sources/GenerateContent.swift b/Examples/GenerativeAICLI/Sources/GenerateContent.swift index beaca99..ab71c43 100644 --- a/Examples/GenerativeAICLI/Sources/GenerateContent.swift +++ b/Examples/GenerativeAICLI/Sources/GenerateContent.swift @@ -44,12 +44,6 @@ struct GenerateContent: AsyncParsableCommand { help: "Enable additional debug logging." ) var debugLogEnabled = false - // Function calls pending processing - var functionCalls = [FunctionCall]() - - // Input to the model - var input = [ModelContent]() - mutating func validate() throws { if textPrompt == nil && imageURL == nil { throw ValidationError( @@ -76,29 +70,7 @@ struct GenerateContent: AsyncParsableCommand { name: modelNameOrDefault(), apiKey: apiKey, generationConfig: config, - safetySettings: safetySettings, - tools: [Tool(functionDeclarations: [ - FunctionDeclaration( - name: "get_exchange_rate", - description: "Get the exchange rate for currencies between countries", - parameters: [ - "currency_from": Schema( - type: .string, - format: "enum", - description: "The currency to convert from in ISO 4217 format", - enumValues: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"] - ), - "currency_to": Schema( - type: .string, - format: "enum", - description: "The currency to convert to in ISO 4217 format", - enumValues: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"] - ), - ], - requiredParameters: ["currency_from", "currency_to"] - ), - ])], - requestOptions: RequestOptions(apiVersion: "v1beta") + safetySettings: safetySettings ) var parts = [ModelContent.Part]() @@ -121,71 +93,27 @@ struct GenerateContent: AsyncParsableCommand { parts.append(.data(mimetype: mimeType, imageData)) } - input = [ModelContent(parts: parts)] - - repeat { - try await processFunctionCalls() + let input = [ModelContent(parts: parts)] - if isStreaming { - let contentStream = model.generateContentStream(input) - print("Generated Content :") - for try await content in contentStream { - processResponseContent(content: content) + if isStreaming { + let contentStream = model.generateContentStream(input) + print("Generated Content :") + for try await content in contentStream { + if let text = content.text { + print(text) } - } else { - // Unary generate content - let content = try await model.generateContent(input) - print("Generated Content:") - processResponseContent(content: content) } - } while !functionCalls.isEmpty + } else { + let content = try await model.generateContent(input) + if let text = content.text { + print("Generated Content:\n\(text)") + } + } } catch { print("Generate Content Error: \(error)") } } - mutating func processResponseContent(content: GenerateContentResponse) { - guard let candidate = content.candidates.first else { - fatalError("No candidate.") - } - - for part in candidate.content.parts { - switch part { - case let .text(text): - print(text) - case .data: - fatalError("Inline data not supported.") - case let .functionCall(functionCall): - functionCalls.append(functionCall) - case let .functionResponse(functionResponse): - print("FunctionResponse: \(functionResponse)") - } - } - } - - mutating func processFunctionCalls() async throws { - for functionCall in functionCalls { - input.append(ModelContent( - role: "model", - parts: [ModelContent.Part.functionCall(functionCall)] - )) - switch functionCall.name { - case "get_exchange_rate": - let exchangeRates = getExchangeRate(args: functionCall.args) - input.append(ModelContent( - role: "function", - parts: [ModelContent.Part.functionResponse(FunctionResponse( - name: "get_exchange_rate", - response: exchangeRates - ))] - )) - default: - fatalError("Unknown function named \"\(functionCall.name)\".") - } - } - functionCalls = [] - } - func modelNameOrDefault() -> String { if let modelName = modelName { return modelName @@ -195,37 +123,6 @@ struct GenerateContent: AsyncParsableCommand { return "gemini-1.0-pro" } } - - // MARK: - Callable Functions - - func getExchangeRate(args: JSONObject) -> JSONObject { - // 1. Validate and extract the parameters provided by the model (from a `FunctionCall`) - guard case let .string(from) = args["currency_from"] else { - fatalError("Missing `currency_from` parameter.") - } - guard case let .string(to) = args["currency_to"] else { - fatalError("Missing `currency_to` parameter.") - } - - // 2. Get the exchange rate - let allRates: [String: [String: Double]] = [ - "AUD": ["CAD": 0.89265, "EUR": 0.6072, "GBP": 0.51714, "JPY": 97.75, "USD": 0.66379], - "CAD": ["AUD": 1.1203, "EUR": 0.68023, "GBP": 0.57933, "JPY": 109.51, "USD": 0.74362], - "EUR": ["AUD": 1.6469, "CAD": 1.4701, "GBP": 0.85168, "JPY": 160.99, "USD": 1.0932], - "GBP": ["AUD": 1.9337, "CAD": 1.7261, "EUR": 1.1741, "JPY": 189.03, "USD": 1.2836], - "JPY": ["AUD": 0.01023, "CAD": 0.00913, "EUR": 0.00621, "GBP": 0.00529, "USD": 0.00679], - "USD": ["AUD": 1.5065, "CAD": 1.3448, "EUR": 0.91475, "GBP": 0.77907, "JPY": 147.26], - ] - guard let fromRates = allRates[from] else { - return ["error": .string("No data for currency \(from).")] - } - guard let toRate = fromRates[to] else { - return ["error": .string("No data for currency \(to).")] - } - - // 3. Return the exchange rates as a JSON object (returned to the model in a `FunctionResponse`) - return ["rates": .number(toRate)] - } } enum CLIError: Error { From d082819b8909f55c0e6d59bf79c5b98382450d2d Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 26 Mar 2024 17:16:57 -0400 Subject: [PATCH 11/12] Add doc to `functionResponse` case --- Sources/GoogleAI/ModelContent.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/GoogleAI/ModelContent.swift b/Sources/GoogleAI/ModelContent.swift index 4aefe7b..136fc1d 100644 --- a/Sources/GoogleAI/ModelContent.swift +++ b/Sources/GoogleAI/ModelContent.swift @@ -43,6 +43,7 @@ public struct ModelContent: Codable, Equatable { /// A predicted function call returned from the model. case functionCall(FunctionCall) + /// A response to a function call. case functionResponse(FunctionResponse) // MARK: Convenience Initializers From cd43d8d35ccf8b2926394f54c2e6cdfea3cfa716 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 26 Mar 2024 17:26:18 -0400 Subject: [PATCH 12/12] Add `tools` documentation in `GenerativeModel` --- Sources/GoogleAI/GenerativeModel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/GoogleAI/GenerativeModel.swift b/Sources/GoogleAI/GenerativeModel.swift index 3384622..1c40411 100644 --- a/Sources/GoogleAI/GenerativeModel.swift +++ b/Sources/GoogleAI/GenerativeModel.swift @@ -36,6 +36,7 @@ public final class GenerativeModel { /// The safety settings to be used for prompts. let safetySettings: [SafetySetting]? + /// A list of tools the model may use to generate the next response. let tools: [Tool]? /// Configuration parameters for sending requests to the backend. @@ -49,6 +50,7 @@ public final class GenerativeModel { /// - apiKey: The API key for your project. /// - generationConfig: The content generation parameters your model should use. /// - safetySettings: A value describing what types of harmful content your model should allow. + /// - tools: A list of ``Tool`` objects that the model may use to generate the next response. /// - requestOptions Configuration parameters for sending requests to the backend. public convenience init(name: String, apiKey: String,