From e0af72e1e8a310280c178e4a5778099088efbb6f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 20:52:47 +0000 Subject: [PATCH 1/8] Add minimum and maximum properties to Schema class. This change adds `minimum` and `maximum` properties to the `Schema` class in `FirebaseVertexAI/Sources/Types/Public/Schema.swift`. It also adds a new test case `generateContentSchemaNumberRange` to `FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift` to verify the behavior of the new properties for number schemas. The test checks if the generated number falls within the specified range. --- .../Sources/Types/Public/Schema.swift | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/FirebaseVertexAI/Sources/Types/Public/Schema.swift b/FirebaseVertexAI/Sources/Types/Public/Schema.swift index 1fb2886ed14..6e28b3ac598 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Schema.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Schema.swift @@ -86,6 +86,12 @@ public final class Schema: Sendable { /// The maximum number of items (elements) in a schema of type `"ARRAY"`. public let maxItems: Int? + /// The minimum value of a numeric type. + public let minimum: Double? + + /// The maximum value of a numeric type. + public let maximum: Double? + /// Properties of type `"OBJECT"`. public let properties: [String: Schema]? @@ -94,8 +100,9 @@ public final class Schema: Sendable { required init(type: DataType, format: String? = nil, description: String? = nil, nullable: Bool = false, enumValues: [String]? = nil, items: Schema? = nil, - minItems: Int? = nil, maxItems: Int? = nil, - properties: [String: Schema]? = nil, requiredProperties: [String]? = nil) { + minItems: Int? = nil, maxItems: Int? = nil, minimum: Double? = nil, + maximum: Double? = nil, properties: [String: Schema]? = nil, + requiredProperties: [String]? = nil) { dataType = type self.format = format self.description = description @@ -104,6 +111,8 @@ public final class Schema: Sendable { self.items = items self.minItems = minItems self.maxItems = maxItems + self.minimum = minimum + self.maximum = maximum self.properties = properties self.requiredProperties = requiredProperties } @@ -184,12 +193,19 @@ public final class Schema: Sendable { /// 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 { + /// - minimum: If specified, instructs the model that the value should be greater than or + /// equal to the specified minimum. + /// - maximum: If specified, instructs the model that the value should be less than or equal + /// to the specified maximum. + public static func float(description: String? = nil, nullable: Bool = false, + minimum: Double? = nil, maximum: Double? = nil) -> Schema { return self.init( type: .number, format: "float", description: description, - nullable: nullable + nullable: nullable, + minimum: minimum, + maximum: maximum ) } @@ -203,11 +219,18 @@ public final class Schema: Sendable { /// 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 { + /// - minimum: If specified, instructs the model that the value should be greater than or + /// equal to the specified minimum. + /// - maximum: If specified, instructs the model that the value should be less than or equal + /// to the specified maximum. + public static func double(description: String? = nil, nullable: Bool = false, + minimum: Double? = nil, maximum: Double? = nil) -> Schema { return self.init( type: .number, description: description, - nullable: nullable + nullable: nullable, + minimum: minimum, + maximum: maximum ) } @@ -349,6 +372,8 @@ extension Schema: Encodable { case items case minItems case maxItems + case minimum + case maximum case properties case requiredProperties = "required" } From b00d07b0b8190c7ee322acb49c87f0b966170aab Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 11 Apr 2025 15:00:36 -0700 Subject: [PATCH 2/8] int support and additional test --- .../Sources/Types/Public/Schema.swift | 11 ++- .../Tests/Integration/SchemaTests.swift | 86 +++++++++++++++++-- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/FirebaseVertexAI/Sources/Types/Public/Schema.swift b/FirebaseVertexAI/Sources/Types/Public/Schema.swift index 6e28b3ac598..f80ab77e5d7 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Schema.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Schema.swift @@ -198,7 +198,7 @@ public final class Schema: Sendable { /// - maximum: If specified, instructs the model that the value should be less than or equal /// to the specified maximum. public static func float(description: String? = nil, nullable: Bool = false, - minimum: Double? = nil, maximum: Double? = nil) -> Schema { + minimum: Double? = nil, maximum: Double? = nil) -> Schema { return self.init( type: .number, format: "float", @@ -224,7 +224,7 @@ public final class Schema: Sendable { /// - maximum: If specified, instructs the model that the value should be less than or equal /// to the specified maximum. public static func double(description: String? = nil, nullable: Bool = false, - minimum: Double? = nil, maximum: Double? = nil) -> Schema { + minimum: Double? = nil, maximum: Double? = nil) -> Schema { return self.init( type: .number, description: description, @@ -255,12 +255,15 @@ public final class Schema: Sendable { /// 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 { + format: IntegerFormat? = nil, + minimum: Int? = nil, maximum: Int? = nil) -> Schema { return self.init( type: .integer, format: format?.rawValue, description: description, - nullable: nullable + nullable: nullable.self, + minimum: minimum != nil ? Double(minimum!) : nil, + maximum: maximum != nil ? Double(maximum!) : nil ) } diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift index 35eb6442d12..38392a3b0b3 100644 --- a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift @@ -55,12 +55,12 @@ struct SchemaTests { generationConfig: GenerationConfig( responseMIMEType: "application/json", responseSchema: - .array( - items: .string(description: "The name of the city"), - description: "A list of city names", - minItems: 3, - maxItems: 5 - ) + .array( + items: .string(description: "The name of the city"), + description: "A list of city names", + minItems: 3, + maxItems: 5 + ) ), safetySettings: safetySettings ) @@ -72,4 +72,78 @@ struct SchemaTests { #expect(decodedJSON.count >= 3, "Expected at least 3 cities, but got \(decodedJSON.count)") #expect(decodedJSON.count <= 5, "Expected at most 5 cities, but got \(decodedJSON.count)") } + + @Test(arguments: InstanceConfig.allConfigsExceptDeveloperV1) + func generateContentSchemaNumberRange(_ config: InstanceConfig) async throws { + let model = VertexAI.componentInstance(config).generativeModel( + modelName: ModelNames.gemini2FlashLite, + generationConfig: GenerationConfig( + responseMIMEType: "application/json", + responseSchema: .double( + description: "A number", + minimum: 110.0, + maximum: 120.0 + ) + ), + safetySettings: safetySettings + ) + let prompt = "Give me a number" + let response = try await model.generateContent(prompt) + let text = try #require(response.text).trimmingCharacters(in: .whitespacesAndNewlines) + let jsonData = try #require(text.data(using: .utf8)) + let decodedNumber = try JSONDecoder().decode(Double.self, from: jsonData) + #expect(decodedNumber >= 110.0, "Expected a number >= 110, but got \(decodedNumber)") + #expect(decodedNumber <= 120.0, "Expected a number <= 120, but got \(decodedNumber)") + } + + @Test(arguments: InstanceConfig.allConfigsExceptDeveloperV1) + func generateContentSchemaNumberRangeMultiType(_ config: InstanceConfig) async throws { + struct ProductInfo: Codable { + let productName: String + let rating: Int // Will correspond to .integer in schema + let price: Double // Will correspond to .double in schema + let salePrice: Float // Will correspond to .float in schema + } + let model = VertexAI.componentInstance(config).generativeModel( + modelName: ModelNames.gemini2FlashLite, + generationConfig: GenerationConfig( + responseMIMEType: "application/json", + responseSchema: .object( + properties: [ + "productName": .string(description: "The name of the product"), + "price": .double( + description: "A price", + minimum: 10.00, + maximum: 120.00 + ), + "salePrice": .float( + description: "A sale price", + minimum: 5.00, + maximum: 90.00 + ), + "rating": .integer( + description: "A rating", + minimum: 1, + maximum: 5 + ), + ], + ), + ), + safetySettings: safetySettings + ) + let prompt = "Describe a premium wireless headphone, including a user rating and price." + let response = try await model.generateContent(prompt) + let text = try #require(response.text).trimmingCharacters(in: .whitespacesAndNewlines) + let jsonData = try #require(text.data(using: .utf8)) + let decodedProduct = try JSONDecoder().decode(ProductInfo.self, from: jsonData) + let price = decodedProduct.price + let salePrice = decodedProduct.salePrice + let rating = decodedProduct.rating + #expect(price >= 10.0, "Expected a price >= 10.00, but got \(price)") + #expect(price <= 120.0, "Expected a price <= 120.00, but got \(price)") + #expect(salePrice >= 5.0, "Expected a salePrice >= 5.00, but got \(salePrice)") + #expect(salePrice <= 90.0, "Expected a salePrice <= 90.00, but got \(salePrice)") + #expect(rating >= 1, "Expected a rating >= 1, but got \(rating)") + #expect(rating <= 5, "Expected a rating <= 5, but got \(rating)") + } } From 0f3705d2f8dd2b50a6ebd51db146636e3f96b9aa Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 11 Apr 2025 16:16:23 -0700 Subject: [PATCH 3/8] Add title and propertyOrdering --- .../Sources/Types/Public/Schema.swift | 29 ++++++++++++++++--- .../Tests/Integration/SchemaTests.swift | 2 ++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/FirebaseVertexAI/Sources/Types/Public/Schema.swift b/FirebaseVertexAI/Sources/Types/Public/Schema.swift index f80ab77e5d7..95df1148f12 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Schema.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Schema.swift @@ -71,6 +71,11 @@ public final class Schema: Sendable { /// A brief description of the parameter. public let description: String? + /// A human-readable name/summary for the schema or a specific property. This helps document the + /// schema's purpose but doesn't typically constrain the generated value. It can subtly guide the + /// model by clarifying the intent of a field. + public let title: String? + /// Indicates if the value may be null. public let nullable: Bool? @@ -98,14 +103,23 @@ public final class Schema: Sendable { /// Required properties of type `"OBJECT"`. public let requiredProperties: [String]? - required init(type: DataType, format: String? = nil, description: String? = nil, + /// A specific hint provided to the Gemini model, suggesting the order in which the keys should + /// appear in the generated JSON string. Important: Standard JSON objects are inherently unordered + /// collections of key-value pairs. While the model will try to respect propertyOrdering in its + /// textual JSON output, subsequent parsing into native Swift objects (like Dictionaries or + /// Structs) might not preserve this order. This parameter primarily affects the raw JSON string + /// serialization. + public let propertyOrdering: [String]? + + required init(type: DataType, format: String? = nil, description: String? = nil, title: String? = nil, nullable: Bool = false, enumValues: [String]? = nil, items: Schema? = nil, minItems: Int? = nil, maxItems: Int? = nil, minimum: Double? = nil, maximum: Double? = nil, properties: [String: Schema]? = nil, - requiredProperties: [String]? = nil) { + requiredProperties: [String]? = nil, propertyOrdering: [String]? = nil) { dataType = type self.format = format self.description = description + self.title = title self.nullable = nullable self.enumValues = enumValues self.items = items @@ -115,6 +129,7 @@ public final class Schema: Sendable { self.maximum = maximum self.properties = properties self.requiredProperties = requiredProperties + self.propertyOrdering = propertyOrdering } /// Returns a `Schema` representing a string value. @@ -343,7 +358,9 @@ public final class Schema: Sendable { /// - 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 { + propertyOrdering: [String]? = nil, + description: String? = nil, title: String? = nil, + nullable: Bool = false) -> Schema { var requiredProperties = Set(properties.keys) for optionalProperty in optionalProperties { guard properties.keys.contains(optionalProperty) else { @@ -355,9 +372,11 @@ public final class Schema: Sendable { return self.init( type: .object, description: description, + title: title, nullable: nullable, properties: properties, - requiredProperties: requiredProperties.sorted() + requiredProperties: requiredProperties.sorted(), + propertyOrdering: propertyOrdering ) } } @@ -370,6 +389,7 @@ extension Schema: Encodable { case dataType = "type" case format case description + case title case nullable case enumValues = "enum" case items @@ -379,5 +399,6 @@ extension Schema: Encodable { case maximum case properties case requiredProperties = "required" + case propertyOrdering } } diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift index 38392a3b0b3..b796feeb57a 100644 --- a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift @@ -127,6 +127,8 @@ struct SchemaTests { maximum: 5 ), ], + propertyOrdering: ["salePrice", "rating", "price", "productName"], + title: "ProductInfo" ), ), safetySettings: safetySettings From 174cdaff90184df22c511a65c54ec7101beda216 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 11 Apr 2025 16:43:18 -0700 Subject: [PATCH 4/8] improve documentation --- .../Sources/Types/Public/Schema.swift | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/FirebaseVertexAI/Sources/Types/Public/Schema.swift b/FirebaseVertexAI/Sources/Types/Public/Schema.swift index 95df1148f12..2f634912eda 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Schema.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Schema.swift @@ -68,7 +68,9 @@ public final class Schema: Sendable { /// The format of the data. public let format: String? - /// A brief description of the parameter. + /// A human-readable explanation of the purpose of the schema or property. While not strictly + /// enforced on the value itself, good descriptions significantly help the model understand the + /// context and generate more relevant and accurate output. public let description: String? /// A human-readable name/summary for the schema or a specific property. This helps document the @@ -82,13 +84,15 @@ public final class Schema: Sendable { /// Possible values of the element of type "STRING" with "enum" format. public let enumValues: [String]? - /// Schema of the elements of type `"ARRAY"`. + /// Defines the schema for the elements within the `"ARRAY"`. All items in the generated array + /// must conform to this schema definition. This can be a simple type (like .string) or a complex + /// nested object schema. public let items: Schema? - /// The minimum number of items (elements) in a schema of type `"ARRAY"`. + /// An integer specifying the minimum number of items the generated `"ARRAY"` must contain. public let minItems: Int? - /// The maximum number of items (elements) in a schema of type `"ARRAY"`. + /// An integer specifying the maximum number of items the generated `"ARRAY"` must contain. public let maxItems: Int? /// The minimum value of a numeric type. @@ -97,10 +101,14 @@ public final class Schema: Sendable { /// The maximum value of a numeric type. public let maximum: Double? - /// Properties of type `"OBJECT"`. + /// Defines the members (key-value pairs) expected within an object. It's a dictionary where keys + /// are the property names (strings) and values are nested `Schema` definitions describing each + /// property's type and constraints. public let properties: [String: Schema]? - /// Required properties of type `"OBJECT"`. + /// An array of strings, where each string is the name of a property defined in the `properties` + /// dictionary that must be present in the generated object. If a property is listed here, the + /// model must include it in the output. public let requiredProperties: [String]? /// A specific hint provided to the Gemini model, suggesting the order in which the keys should @@ -111,7 +119,8 @@ public final class Schema: Sendable { /// serialization. public let propertyOrdering: [String]? - required init(type: DataType, format: String? = nil, description: String? = nil, title: String? = nil, + required init(type: DataType, format: String? = nil, description: String? = nil, + title: String? = nil, nullable: Bool = false, enumValues: [String]? = nil, items: Schema? = nil, minItems: Int? = nil, maxItems: Int? = nil, minimum: Double? = nil, maximum: Double? = nil, properties: [String: Schema]? = nil, From 3c9df98e6fabdaab2cfc640b23da4adc259d4ca7 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Sat, 12 Apr 2025 07:29:40 -0700 Subject: [PATCH 5/8] Change first test to integer to make it more robust --- .../Tests/TestApp/Tests/Integration/SchemaTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift index b796feeb57a..923ee1a238e 100644 --- a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift @@ -79,10 +79,10 @@ struct SchemaTests { modelName: ModelNames.gemini2FlashLite, generationConfig: GenerationConfig( responseMIMEType: "application/json", - responseSchema: .double( + responseSchema: .integer( description: "A number", - minimum: 110.0, - maximum: 120.0 + minimum: 110, + maximum: 120 ) ), safetySettings: safetySettings From 8275955a4330c580bd5db98320f2427a47bff761 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 14 Apr 2025 15:16:40 -0700 Subject: [PATCH 6/8] Update FirebaseVertexAI/Sources/Types/Public/Schema.swift Co-authored-by: Andrew Heard --- FirebaseVertexAI/Sources/Types/Public/Schema.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseVertexAI/Sources/Types/Public/Schema.swift b/FirebaseVertexAI/Sources/Types/Public/Schema.swift index 2f634912eda..3dad902fd99 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Schema.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Schema.swift @@ -222,7 +222,7 @@ public final class Schema: Sendable { /// - maximum: If specified, instructs the model that the value should be less than or equal /// to the specified maximum. public static func float(description: String? = nil, nullable: Bool = false, - minimum: Double? = nil, maximum: Double? = nil) -> Schema { + minimum: Float? = nil, maximum: Float? = nil) -> Schema { return self.init( type: .number, format: "float", From 67e6bddb1d4c28d8072d416ac368137dae855990 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 14 Apr 2025 15:16:58 -0700 Subject: [PATCH 7/8] Update FirebaseVertexAI/Sources/Types/Public/Schema.swift Co-authored-by: Andrew Heard --- FirebaseVertexAI/Sources/Types/Public/Schema.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseVertexAI/Sources/Types/Public/Schema.swift b/FirebaseVertexAI/Sources/Types/Public/Schema.swift index 3dad902fd99..09eafd7ac77 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Schema.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Schema.swift @@ -286,8 +286,8 @@ public final class Schema: Sendable { format: format?.rawValue, description: description, nullable: nullable.self, - minimum: minimum != nil ? Double(minimum!) : nil, - maximum: maximum != nil ? Double(maximum!) : nil + minimum: minimum.map { Double($0) }, + maximum: maximum.map { Double($0) } ) } From 8f54b36366bcce51d86e127811e43403b30fa5af Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 14 Apr 2025 15:17:14 -0700 Subject: [PATCH 8/8] Update FirebaseVertexAI/Sources/Types/Public/Schema.swift Co-authored-by: Andrew Heard --- FirebaseVertexAI/Sources/Types/Public/Schema.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseVertexAI/Sources/Types/Public/Schema.swift b/FirebaseVertexAI/Sources/Types/Public/Schema.swift index 09eafd7ac77..ee4d10107ee 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Schema.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Schema.swift @@ -228,8 +228,8 @@ public final class Schema: Sendable { format: "float", description: description, nullable: nullable, - minimum: minimum, - maximum: maximum + minimum: minimum.map { Double($0) }, + maximum: maximum.map { Double($0) } ) }