diff --git a/FirebaseVertexAI/CHANGELOG.md b/FirebaseVertexAI/CHANGELOG.md index 6057cf42d90..9950b1c0190 100644 --- a/FirebaseVertexAI/CHANGELOG.md +++ b/FirebaseVertexAI/CHANGELOG.md @@ -11,6 +11,13 @@ renamed to `inlineData`; no functionality changes. (#13700) - [changed] **Breaking Change**: The property `citationSources` of `CitationMetadata` has been renamed to `citations`. (#13702) +- [changed] **Breaking Change**: The constructor for `Schema` is now deprecated; + use the new static methods `Schema.string(...)`, `Schema.object(...)`, etc., + instead. (#13616) +- [changed] **Breaking Change**: The constructor for `FunctionDeclaration` now + accepts an array of *optional* parameters instead of a list of *required* + parameters; if a parameter is not listed as optional it is assumed to be + required. (#13616) # 11.3.0 - [added] Added `Decodable` conformance for `FunctionResponse`. (#13606) diff --git a/FirebaseVertexAI/Sample/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift b/FirebaseVertexAI/Sample/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift index 7adcadc123a..110cab9ce27 100644 --- a/FirebaseVertexAI/Sample/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift +++ b/FirebaseVertexAI/Sample/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift @@ -45,20 +45,15 @@ class FunctionCallingViewModel: ObservableObject { 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_from": .enumeration( + values: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"], + description: "The currency to convert from in ISO 4217 format" ), - "currency_to": Schema( - type: .string, - format: "enum", - description: "The currency to convert to in ISO 4217 format", - enumValues: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"] + "currency_to": .enumeration( + values: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"], + description: "The currency to convert to in ISO 4217 format" ), - ], - requiredParameters: ["currency_from", "currency_to"] + ] ), ])] ) diff --git a/FirebaseVertexAI/Sources/FunctionCalling.swift b/FirebaseVertexAI/Sources/FunctionCalling.swift index 3c88279c6df..4641a6f400f 100644 --- a/FirebaseVertexAI/Sources/FunctionCalling.swift +++ b/FirebaseVertexAI/Sources/FunctionCalling.swift @@ -43,17 +43,15 @@ public struct FunctionDeclaration { /// - 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]? = nil) { + /// - parameters: Describes the parameters to this function. + public init(name: String, description: String, parameters: [String: Schema], + optionalParameters: [String] = []) { self.name = name self.description = description - self.parameters = Schema( - type: .object, + self.parameters = Schema.object( properties: parameters, - requiredProperties: requiredParameters + optionalProperties: optionalParameters, + nullable: false ) } } diff --git a/FirebaseVertexAI/Sources/Schema.swift b/FirebaseVertexAI/Sources/Schema.swift index 6ffadfd9025..f24a4d203b3 100644 --- a/FirebaseVertexAI/Sources/Schema.swift +++ b/FirebaseVertexAI/Sources/Schema.swift @@ -19,29 +19,45 @@ import Foundation /// 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 { + /// Modifiers describing the expected format of a string `Schema`. + public enum StringFormat { + /// A custom string format. + case custom(String) + } + + /// Modifiers describing the expected format of an integer `Schema`. + public enum IntegerFormat { + /// A 32-bit signed integer. + case int32 + /// A 64-bit signed integer. + case int64 + /// A custom integer format. + case custom(String) + } + /// The data type. - let type: DataType + public let type: DataType /// The format of the data. - let format: String? + public let format: String? /// A brief description of the parameter. - let description: String? + public let description: String? /// Indicates if the value may be null. - let nullable: Bool? + public let nullable: Bool? /// Possible values of the element of type ``DataType/string`` with "enum" format. - let enumValues: [String]? + public let enumValues: [String]? /// Schema of the elements of type ``DataType/array``. - let items: Schema? + public let items: Schema? /// Properties of type ``DataType/object``. - let properties: [String: Schema]? + public let properties: [String: Schema]? /// Required properties of type ``DataType/object``. - let requiredProperties: [String]? + public let requiredProperties: [String]? /// Constructs a new `Schema`. /// @@ -59,11 +75,29 @@ public class Schema { /// - 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) { + @available(*, deprecated, message: """ + Use static methods `string(description:format:nullable:)`, `number(description:format:nullable:)`, + etc., instead. + """) + public convenience 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.init( + type: type, + format: format, + description: description, + nullable: nullable ?? false, + enumValues: enumValues, + items: items, + properties: properties, + requiredProperties: requiredProperties + ) + } + + required init(type: DataType, format: String? = nil, description: String? = nil, + nullable: Bool = false, enumValues: [String]? = nil, items: Schema? = nil, + properties: [String: Schema]? = nil, requiredProperties: [String]? = nil) { self.type = type self.format = format self.description = description @@ -73,6 +107,228 @@ public class Schema { self.properties = properties self.requiredProperties = requiredProperties } + + /// Returns a `Schema` representing a string value. + /// + /// This schema instructs the model to produce data of type ``DataType/string``, which is suitable + /// for decoding into a Swift `String` (or `String?`, if `nullable` is set to `true`). + /// + /// > Tip: If a specific set of string values should be generated by the model (for example, + /// > "north", "south", "east", or "west"), use ``enumeration(values:description:nullable:)`` + /// > instead to constrain the generated values. + /// + /// - Parameters: + /// - description: An optional description of what the string should contain or represent; may + /// use Markdown format. + /// - nullable: If `true`, instructs the model that it *may* generate `null` instead of a + /// string; defaults to `false`, enforcing that a string value is generated. + /// - format: An optional modifier describing the expected format of the string. Currently no + /// formats are officially supported for strings but custom values may be specified using + /// ``StringFormat/custom(_:)``, for example `.custom("email")` or `.custom("byte")`; these + /// provide additional hints for how the model should respond but are not guaranteed to be + /// adhered to. + public static func string(description: String? = nil, nullable: Bool = false, + format: StringFormat? = nil) -> Schema { + return self.init( + type: .string, + format: format?.rawValue, + description: description, + nullable: nullable + ) + } + + /// Returns a `Schema` representing an enumeration of string values. + /// + /// This schema instructs the model to produce data of type ``DataType/string`` with the + /// `format` `"enum"`. This data is suitable for decoding into a Swift `String` (or `String?`, + /// if `nullable` is set to `true`), or an `enum` with strings as raw values. + /// + /// **Example:** + /// The values `["north", "south", "east", "west"]` for an enumation of directions. + /// ``` + /// enum Direction: String, Decodable { + /// case north, south, east, west + /// } + /// ``` + /// + /// - Parameters: + /// - values: The list of string values that may be generated by the model. + /// - description: An optional description of what the `values` contain or represent; may use + /// Markdown format. + /// - nullable: If `true`, instructs the model that it *may* generate `null` instead of one of + /// the strings specified in `values`; defaults to `false`, enforcing that one of the string + /// values is generated. + public static func enumeration(values: [String], description: String? = nil, + nullable: Bool = false) -> Schema { + return self.init( + type: .string, + format: "enum", + description: description, + nullable: nullable, + enumValues: values + ) + } + + /// Returns a `Schema` representing a single-precision floating-point number. + /// + /// This schema instructs the model to produce data of type ``DataType/number`` with the + /// `format` `"float"`, which is suitable for decoding into a Swift `Float` (or `Float?`, if + /// `nullable` is set to `true`). + /// + /// > Important: This `Schema` provides a hint to the model that it should generate a + /// > single-precision floating-point number, a `float`, but only guarantees that the value will + /// > be a number. + /// + /// - Parameters: + /// - description: An optional description of what the number should contain or represent; may + /// use Markdown format. + /// - nullable: If `true`, instructs the model that it may generate `null` instead of a number; + /// defaults to `false`, enforcing that a number is generated. + public static func float(description: String? = nil, nullable: Bool = false) -> Schema { + return self.init( + type: .number, + format: "float", + description: description, + nullable: nullable + ) + } + + /// Returns a `Schema` representing a double-precision floating-point number. + /// + /// This schema instructs the model to produce data of type ``DataType/number`` with the + /// `format` `"double"`, which is suitable for decoding into a Swift `Double` (or `Double?`, if + /// `nullable` is set to `true`). + /// + /// > Important: This `Schema` provides a hint to the model that it should generate a + /// > double-precision floating-point number, a `double`, but only guarantees that the value will + /// > be a number. + /// + /// - Parameters: + /// - description: An optional description of what the number should contain or represent; may + /// use Markdown format. + /// - nullable: If `true`, instructs the model that it may return `null` instead of a number; + /// defaults to `false`, enforcing that a number is returned. + public static func double(description: String? = nil, nullable: Bool = false) -> Schema { + return self.init( + type: .number, + format: "double", + description: description, + nullable: nullable + ) + } + + /// Returns a `Schema` representing an integer value. + /// + /// This schema instructs the model to produce data of type ``DataType/integer``, which is + /// suitable for decoding into a Swift `Int` (or `Int?`, if `nullable` is set to `true`) or other + /// integer types (such as `Int32`) based on the expected size of values being generated. + /// + /// > Important: If a `format` of ``IntegerFormat/int32`` or ``IntegerFormat/int64`` is + /// > specified, this provides a hint to the model that it should generate 32-bit or 64-bit + /// > integers but this `Schema` only guarantees that the value will be an integer. Therefore, it + /// > is *possible* that decoding into an `Int32` could overflow even if a `format` of + /// > ``IntegerFormat/int32`` is specified. + /// + /// - Parameters: + /// - description: An optional description of what the integer should contain or represent; may + /// use Markdown format. + /// - nullable: If `true`, instructs the model that it may return `null` instead of an integer; + /// defaults to `false`, enforcing that an integer is returned. + /// - format: An optional modifier describing the expected format of the integer. Currently the + /// formats ``IntegerFormat/int32`` and ``IntegerFormat/int64`` are supported; custom values + /// may be specified using ``IntegerFormat/custom(_:)`` but may be ignored by the model. + public static func integer(description: String? = nil, nullable: Bool = false, + format: IntegerFormat? = nil) -> Schema { + return self.init( + type: .integer, + format: format?.rawValue, + description: description, + nullable: nullable + ) + } + + /// Returns a `Schema` representing a boolean value. + /// + /// This schema instructs the model to produce data of type ``DataType/boolean``, which is + /// suitable for decoding into a Swift `Bool` (or `Bool?`, if `nullable` is set to `true`). + /// + /// - Parameters: + /// - description: An optional description of what the boolean should contain or represent; may + /// use Markdown format. + /// - nullable: If `true`, instructs the model that it may return `null` instead of a boolean; + /// defaults to `false`, enforcing that a boolean is returned. + public static func boolean(description: String? = nil, nullable: Bool = false) -> Schema { + return self.init(type: .boolean, description: description, nullable: nullable) + } + + /// Returns a `Schema` representing an array. + /// + /// This schema instructs the model to produce data of type ``DataType/array``, which has elements + /// of any other ``DataType`` (including nested ``DataType/array``s). This data is suitable for + /// decoding into many Swift collection types, including `Array`, holding elements of types + /// suitable for decoding from the respective `items` type. + /// + /// - Parameters: + /// - items: The `Schema` of the elements that the array will hold. + /// - description: An optional description of what the array should contain or represent; may + /// use Markdown format. + /// - nullable: If `true`, instructs the model that it may return `null` instead of an array; + /// defaults to `false`, enforcing that an array is returned. + public static func array(items: Schema, description: String? = nil, + nullable: Bool = false) -> Schema { + return self.init(type: .array, description: description, nullable: nullable, items: items) + } + + /// Returns a `Schema` representing an object. + /// + /// This schema instructs the model to produce data of type ``DataType/object``, which has keys + /// of type ``DataType/string`` and values of any other ``DataType`` (including nested + /// ``DataType/object``s). This data is suitable for decoding into Swift keyed collection types, + /// including `Dictionary`, or other custom `struct` or `class` types. + /// + /// **Example:** A `City` could be represented with the following object `Schema`. + /// ``` + /// Schema.object(properties: [ + /// "name" : .string(), + /// "population": .integer() + /// ]) + /// ``` + /// The generated data could be decoded into a Swift native type: + /// ``` + /// struct City: Decodable { + /// let name: String + /// let population: Int + /// } + /// ``` + /// + /// - Parameters: + /// - properties: A dictionary containing the object's property names as keys and their + /// respective `Schema`s as values. + /// - optionalProperties: A list of property names that may be be omitted in objects generated + /// by the model; these names must correspond to the keys provided in the `properties` + /// dictionary and may be an empty list. + /// - description: An optional description of what the object should contain or represent; may + /// use Markdown format. + /// - nullable: If `true`, instructs the model that it may return `null` instead of an object; + /// defaults to `false`, enforcing that an object is returned. + public static func object(properties: [String: Schema], optionalProperties: [String] = [], + description: String? = nil, nullable: Bool = false) -> Schema { + var requiredProperties = Set(properties.keys) + for optionalProperty in optionalProperties { + guard properties.keys.contains(optionalProperty) else { + fatalError("Optional property \"\(optionalProperty)\" not defined in object properties.") + } + requiredProperties.remove(optionalProperty) + } + + return self.init( + type: .object, + description: description, + nullable: nullable, + properties: properties, + requiredProperties: requiredProperties.sorted() + ) + } } /// A data type. @@ -114,3 +370,45 @@ extension Schema: Encodable { } extension DataType: Encodable {} + +// MARK: - RawRepresentable Conformance + +extension Schema.IntegerFormat: RawRepresentable { + public init?(rawValue: String) { + switch rawValue { + case "int32": + self = .int32 + case "int64": + self = .int64 + default: + self = .custom(rawValue) + } + } + + public var rawValue: String { + switch self { + case .int32: + return "int32" + case .int64: + return "int64" + case let .custom(format): + return format + } + } +} + +extension Schema.StringFormat: RawRepresentable { + public init?(rawValue: String) { + switch rawValue { + default: + self = .custom(rawValue) + } + } + + public var rawValue: String { + switch self { + case let .custom(format): + return format + } + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerationConfigTests.swift b/FirebaseVertexAI/Tests/Unit/GenerationConfigTests.swift index 35450c03758..43cbfe6dd96 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerationConfigTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerationConfigTests.swift @@ -57,7 +57,7 @@ final class GenerationConfigTests: XCTestCase { maxOutputTokens: maxOutputTokens, stopSequences: stopSequences, responseMIMEType: responseMIMEType, - responseSchema: Schema(type: .array, items: Schema(type: .string)) + responseSchema: .array(items: .string()) ) let jsonData = try encoder.encode(generationConfig) @@ -70,8 +70,10 @@ final class GenerationConfigTests: XCTestCase { "responseMIMEType" : "\(responseMIMEType)", "responseSchema" : { "items" : { + "nullable" : false, "type" : "STRING" }, + "nullable" : false, "type" : "ARRAY" }, "stopSequences" : [ @@ -89,15 +91,11 @@ final class GenerationConfigTests: XCTestCase { let mimeType = "application/json" let generationConfig = GenerationConfig( responseMIMEType: mimeType, - responseSchema: Schema( - type: .object, - properties: [ - "firstName": Schema(type: .string), - "lastName": Schema(type: .string), - "age": Schema(type: .integer), - ], - requiredProperties: ["firstName", "lastName", "age"] - ) + responseSchema: .object(properties: [ + "firstName": .string(), + "lastName": .string(), + "age": .integer(), + ]) ) let jsonData = try encoder.encode(generationConfig) @@ -107,21 +105,25 @@ final class GenerationConfigTests: XCTestCase { { "responseMIMEType" : "\(mimeType)", "responseSchema" : { + "nullable" : false, "properties" : { "age" : { + "nullable" : false, "type" : "INTEGER" }, "firstName" : { + "nullable" : false, "type" : "STRING" }, "lastName" : { + "nullable" : false, "type" : "STRING" } }, "required" : [ + "age", "firstName", - "lastName", - "age" + "lastName" ], "type" : "OBJECT" }