Skip to content

[VertexAI] Add minimum, maximum, title, and propertyOrdering to schema #14711

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions FirebaseVertexAI/Sources/Types/Public/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]?

Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
)
}

Expand All @@ -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
)
}

Expand All @@ -232,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
)
}

Expand Down Expand Up @@ -349,6 +375,8 @@ extension Schema: Encodable {
case items
case minItems
case maxItems
case minimum
case maximum
case properties
case requiredProperties = "required"
}
Expand Down
86 changes: 80 additions & 6 deletions FirebaseVertexAI/Tests/TestApp/Tests/Integration/SchemaTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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)")
}
}
Loading