From 83072409767a9a76ac233bf4b1323e0fc5e0fb96 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 5 Mar 2024 19:19:10 +0000 Subject: [PATCH] Add `FunctionCall` decoding (#114) --- Sources/GoogleAI/Chat.swift | 28 +--- Sources/GoogleAI/FunctionCalling.swift | 200 +------------------------ Sources/GoogleAI/ModelContent.swift | 11 +- 3 files changed, 7 insertions(+), 232 deletions(-) diff --git a/Sources/GoogleAI/Chat.swift b/Sources/GoogleAI/Chat.swift index fd48de9..cd33453 100644 --- a/Sources/GoogleAI/Chat.swift +++ b/Sources/GoogleAI/Chat.swift @@ -74,32 +74,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 } /// Sends a message using the existing history of this chat as context. If successful, the message @@ -195,10 +173,6 @@ 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 7e4d84d..5d8ded5 100644 --- a/Sources/GoogleAI/FunctionCalling.swift +++ b/Sources/GoogleAI/FunctionCalling.swift @@ -15,193 +15,14 @@ import Foundation /// A predicted function call returned from the model. -public struct FunctionCall: Equatable, Encodable { +public struct FunctionCall: Equatable { /// The name of the function to call. - public let name: String - - /// The function parameters and values. - public let args: JSONObject -} - -/// 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 { - case type - case format - case description - case nullable - case enumValues = "enum" - case items - case properties - 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, - properties: [String: Schema]? = nil, - requiredProperties: [String]? = nil) { - self.type = type - self.format = format - self.description = description - self.nullable = nullable - self.enumValues = enumValues - self.items = items - self.properties = properties - self.requiredProperties = requiredProperties - } -} - -/// 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" -} - -/// 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 - self.description = description - self.parameters = Schema( - type: .object, - properties: parameters, - requiredProperties: requiredParameters - ) - } -} - -/// 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 - } -} - -/// 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 - } + /// The function parameters and values. + let args: JSONObject } -// MARK: - Codable Conformance - extension FunctionCall: Decodable { enum CodingKeys: CodingKey { case name @@ -218,18 +39,3 @@ 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/ModelContent.swift b/Sources/GoogleAI/ModelContent.swift index 136fc1d..2ce8876 100644 --- a/Sources/GoogleAI/ModelContent.swift +++ b/Sources/GoogleAI/ModelContent.swift @@ -26,7 +26,6 @@ public struct ModelContent: Codable, Equatable { case text case inlineData case functionCall - case functionResponse } enum InlineDataKeys: String, CodingKey { @@ -43,9 +42,6 @@ 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 /// Convenience function for populating a Part with JPEG data. @@ -72,10 +68,9 @@ public struct ModelContent: Codable, Equatable { ) try inlineDataContainer.encode(mimetype, forKey: .mimeType) try inlineDataContainer.encode(bytes, forKey: .bytes) - case let .functionCall(functionCall): - try container.encode(functionCall, forKey: .functionCall) - case let .functionResponse(functionResponse): - try container.encode(functionResponse, forKey: .functionResponse) + case .functionCall: + // TODO(andrewheard): Encode FunctionCalls when when encoding is implemented. + fatalError("FunctionCall encoding not implemented.") } }