Skip to content

Commit fa0dd71

Browse files
paulb777google-labs-jules[bot]andrewheard
authored
[VertexAI] Add minimum, maximum, title, and propertyOrdering to schema (#14711)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Andrew Heard <andrewheard@google.com>
1 parent 3fab397 commit fa0dd71

File tree

2 files changed

+156
-22
lines changed

2 files changed

+156
-22
lines changed

FirebaseVertexAI/Sources/Types/Public/Schema.swift

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -68,44 +68,77 @@ public final class Schema: Sendable {
6868
/// The format of the data.
6969
public let format: String?
7070

71-
/// A brief description of the parameter.
71+
/// A human-readable explanation of the purpose of the schema or property. While not strictly
72+
/// enforced on the value itself, good descriptions significantly help the model understand the
73+
/// context and generate more relevant and accurate output.
7274
public let description: String?
7375

76+
/// A human-readable name/summary for the schema or a specific property. This helps document the
77+
/// schema's purpose but doesn't typically constrain the generated value. It can subtly guide the
78+
/// model by clarifying the intent of a field.
79+
public let title: String?
80+
7481
/// Indicates if the value may be null.
7582
public let nullable: Bool?
7683

7784
/// Possible values of the element of type "STRING" with "enum" format.
7885
public let enumValues: [String]?
7986

80-
/// Schema of the elements of type `"ARRAY"`.
87+
/// Defines the schema for the elements within the `"ARRAY"`. All items in the generated array
88+
/// must conform to this schema definition. This can be a simple type (like .string) or a complex
89+
/// nested object schema.
8190
public let items: Schema?
8291

83-
/// The minimum number of items (elements) in a schema of type `"ARRAY"`.
92+
/// An integer specifying the minimum number of items the generated `"ARRAY"` must contain.
8493
public let minItems: Int?
8594

86-
/// The maximum number of items (elements) in a schema of type `"ARRAY"`.
95+
/// An integer specifying the maximum number of items the generated `"ARRAY"` must contain.
8796
public let maxItems: Int?
8897

89-
/// Properties of type `"OBJECT"`.
98+
/// The minimum value of a numeric type.
99+
public let minimum: Double?
100+
101+
/// The maximum value of a numeric type.
102+
public let maximum: Double?
103+
104+
/// Defines the members (key-value pairs) expected within an object. It's a dictionary where keys
105+
/// are the property names (strings) and values are nested `Schema` definitions describing each
106+
/// property's type and constraints.
90107
public let properties: [String: Schema]?
91108

92-
/// Required properties of type `"OBJECT"`.
109+
/// An array of strings, where each string is the name of a property defined in the `properties`
110+
/// dictionary that must be present in the generated object. If a property is listed here, the
111+
/// model must include it in the output.
93112
public let requiredProperties: [String]?
94113

114+
/// A specific hint provided to the Gemini model, suggesting the order in which the keys should
115+
/// appear in the generated JSON string. Important: Standard JSON objects are inherently unordered
116+
/// collections of key-value pairs. While the model will try to respect propertyOrdering in its
117+
/// textual JSON output, subsequent parsing into native Swift objects (like Dictionaries or
118+
/// Structs) might not preserve this order. This parameter primarily affects the raw JSON string
119+
/// serialization.
120+
public let propertyOrdering: [String]?
121+
95122
required init(type: DataType, format: String? = nil, description: String? = nil,
123+
title: String? = nil,
96124
nullable: Bool = false, enumValues: [String]? = nil, items: Schema? = nil,
97-
minItems: Int? = nil, maxItems: Int? = nil,
98-
properties: [String: Schema]? = nil, requiredProperties: [String]? = nil) {
125+
minItems: Int? = nil, maxItems: Int? = nil, minimum: Double? = nil,
126+
maximum: Double? = nil, properties: [String: Schema]? = nil,
127+
requiredProperties: [String]? = nil, propertyOrdering: [String]? = nil) {
99128
dataType = type
100129
self.format = format
101130
self.description = description
131+
self.title = title
102132
self.nullable = nullable
103133
self.enumValues = enumValues
104134
self.items = items
105135
self.minItems = minItems
106136
self.maxItems = maxItems
137+
self.minimum = minimum
138+
self.maximum = maximum
107139
self.properties = properties
108140
self.requiredProperties = requiredProperties
141+
self.propertyOrdering = propertyOrdering
109142
}
110143

111144
/// Returns a `Schema` representing a string value.
@@ -184,12 +217,19 @@ public final class Schema: Sendable {
184217
/// use Markdown format.
185218
/// - nullable: If `true`, instructs the model that it may generate `null` instead of a number;
186219
/// defaults to `false`, enforcing that a number is generated.
187-
public static func float(description: String? = nil, nullable: Bool = false) -> Schema {
220+
/// - minimum: If specified, instructs the model that the value should be greater than or
221+
/// equal to the specified minimum.
222+
/// - maximum: If specified, instructs the model that the value should be less than or equal
223+
/// to the specified maximum.
224+
public static func float(description: String? = nil, nullable: Bool = false,
225+
minimum: Float? = nil, maximum: Float? = nil) -> Schema {
188226
return self.init(
189227
type: .number,
190228
format: "float",
191229
description: description,
192-
nullable: nullable
230+
nullable: nullable,
231+
minimum: minimum.map { Double($0) },
232+
maximum: maximum.map { Double($0) }
193233
)
194234
}
195235

@@ -203,11 +243,18 @@ public final class Schema: Sendable {
203243
/// use Markdown format.
204244
/// - nullable: If `true`, instructs the model that it may return `null` instead of a number;
205245
/// defaults to `false`, enforcing that a number is returned.
206-
public static func double(description: String? = nil, nullable: Bool = false) -> Schema {
246+
/// - minimum: If specified, instructs the model that the value should be greater than or
247+
/// equal to the specified minimum.
248+
/// - maximum: If specified, instructs the model that the value should be less than or equal
249+
/// to the specified maximum.
250+
public static func double(description: String? = nil, nullable: Bool = false,
251+
minimum: Double? = nil, maximum: Double? = nil) -> Schema {
207252
return self.init(
208253
type: .number,
209254
description: description,
210-
nullable: nullable
255+
nullable: nullable,
256+
minimum: minimum,
257+
maximum: maximum
211258
)
212259
}
213260

@@ -232,12 +279,15 @@ public final class Schema: Sendable {
232279
/// formats ``IntegerFormat/int32`` and ``IntegerFormat/int64`` are supported; custom values
233280
/// may be specified using ``IntegerFormat/custom(_:)`` but may be ignored by the model.
234281
public static func integer(description: String? = nil, nullable: Bool = false,
235-
format: IntegerFormat? = nil) -> Schema {
282+
format: IntegerFormat? = nil,
283+
minimum: Int? = nil, maximum: Int? = nil) -> Schema {
236284
return self.init(
237285
type: .integer,
238286
format: format?.rawValue,
239287
description: description,
240-
nullable: nullable
288+
nullable: nullable.self,
289+
minimum: minimum.map { Double($0) },
290+
maximum: maximum.map { Double($0) }
241291
)
242292
}
243293

@@ -317,7 +367,9 @@ public final class Schema: Sendable {
317367
/// - nullable: If `true`, instructs the model that it may return `null` instead of an object;
318368
/// defaults to `false`, enforcing that an object is returned.
319369
public static func object(properties: [String: Schema], optionalProperties: [String] = [],
320-
description: String? = nil, nullable: Bool = false) -> Schema {
370+
propertyOrdering: [String]? = nil,
371+
description: String? = nil, title: String? = nil,
372+
nullable: Bool = false) -> Schema {
321373
var requiredProperties = Set(properties.keys)
322374
for optionalProperty in optionalProperties {
323375
guard properties.keys.contains(optionalProperty) else {
@@ -329,9 +381,11 @@ public final class Schema: Sendable {
329381
return self.init(
330382
type: .object,
331383
description: description,
384+
title: title,
332385
nullable: nullable,
333386
properties: properties,
334-
requiredProperties: requiredProperties.sorted()
387+
requiredProperties: requiredProperties.sorted(),
388+
propertyOrdering: propertyOrdering
335389
)
336390
}
337391
}
@@ -344,12 +398,16 @@ extension Schema: Encodable {
344398
case dataType = "type"
345399
case format
346400
case description
401+
case title
347402
case nullable
348403
case enumValues = "enum"
349404
case items
350405
case minItems
351406
case maxItems
407+
case minimum
408+
case maximum
352409
case properties
353410
case requiredProperties = "required"
411+
case propertyOrdering
354412
}
355413
}

FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ struct SchemaTests {
5555
generationConfig: GenerationConfig(
5656
responseMIMEType: "application/json",
5757
responseSchema:
58-
.array(
59-
items: .string(description: "The name of the city"),
60-
description: "A list of city names",
61-
minItems: 3,
62-
maxItems: 5
63-
)
58+
.array(
59+
items: .string(description: "The name of the city"),
60+
description: "A list of city names",
61+
minItems: 3,
62+
maxItems: 5
63+
)
6464
),
6565
safetySettings: safetySettings
6666
)
@@ -72,4 +72,80 @@ struct SchemaTests {
7272
#expect(decodedJSON.count >= 3, "Expected at least 3 cities, but got \(decodedJSON.count)")
7373
#expect(decodedJSON.count <= 5, "Expected at most 5 cities, but got \(decodedJSON.count)")
7474
}
75+
76+
@Test(arguments: InstanceConfig.allConfigsExceptDeveloperV1)
77+
func generateContentSchemaNumberRange(_ config: InstanceConfig) async throws {
78+
let model = VertexAI.componentInstance(config).generativeModel(
79+
modelName: ModelNames.gemini2FlashLite,
80+
generationConfig: GenerationConfig(
81+
responseMIMEType: "application/json",
82+
responseSchema: .integer(
83+
description: "A number",
84+
minimum: 110,
85+
maximum: 120
86+
)
87+
),
88+
safetySettings: safetySettings
89+
)
90+
let prompt = "Give me a number"
91+
let response = try await model.generateContent(prompt)
92+
let text = try #require(response.text).trimmingCharacters(in: .whitespacesAndNewlines)
93+
let jsonData = try #require(text.data(using: .utf8))
94+
let decodedNumber = try JSONDecoder().decode(Double.self, from: jsonData)
95+
#expect(decodedNumber >= 110.0, "Expected a number >= 110, but got \(decodedNumber)")
96+
#expect(decodedNumber <= 120.0, "Expected a number <= 120, but got \(decodedNumber)")
97+
}
98+
99+
@Test(arguments: InstanceConfig.allConfigsExceptDeveloperV1)
100+
func generateContentSchemaNumberRangeMultiType(_ config: InstanceConfig) async throws {
101+
struct ProductInfo: Codable {
102+
let productName: String
103+
let rating: Int // Will correspond to .integer in schema
104+
let price: Double // Will correspond to .double in schema
105+
let salePrice: Float // Will correspond to .float in schema
106+
}
107+
let model = VertexAI.componentInstance(config).generativeModel(
108+
modelName: ModelNames.gemini2FlashLite,
109+
generationConfig: GenerationConfig(
110+
responseMIMEType: "application/json",
111+
responseSchema: .object(
112+
properties: [
113+
"productName": .string(description: "The name of the product"),
114+
"price": .double(
115+
description: "A price",
116+
minimum: 10.00,
117+
maximum: 120.00
118+
),
119+
"salePrice": .float(
120+
description: "A sale price",
121+
minimum: 5.00,
122+
maximum: 90.00
123+
),
124+
"rating": .integer(
125+
description: "A rating",
126+
minimum: 1,
127+
maximum: 5
128+
),
129+
],
130+
propertyOrdering: ["salePrice", "rating", "price", "productName"],
131+
title: "ProductInfo"
132+
),
133+
),
134+
safetySettings: safetySettings
135+
)
136+
let prompt = "Describe a premium wireless headphone, including a user rating and price."
137+
let response = try await model.generateContent(prompt)
138+
let text = try #require(response.text).trimmingCharacters(in: .whitespacesAndNewlines)
139+
let jsonData = try #require(text.data(using: .utf8))
140+
let decodedProduct = try JSONDecoder().decode(ProductInfo.self, from: jsonData)
141+
let price = decodedProduct.price
142+
let salePrice = decodedProduct.salePrice
143+
let rating = decodedProduct.rating
144+
#expect(price >= 10.0, "Expected a price >= 10.00, but got \(price)")
145+
#expect(price <= 120.0, "Expected a price <= 120.00, but got \(price)")
146+
#expect(salePrice >= 5.0, "Expected a salePrice >= 5.00, but got \(salePrice)")
147+
#expect(salePrice <= 90.0, "Expected a salePrice <= 90.00, but got \(salePrice)")
148+
#expect(rating >= 1, "Expected a rating >= 1, but got \(rating)")
149+
#expect(rating <= 5, "Expected a rating <= 5, but got \(rating)")
150+
}
75151
}

0 commit comments

Comments
 (0)