Skip to content

Commit 7f8bd29

Browse files
committed
Add converters between FirebaseAI and AFM types
1 parent bfb5bf4 commit 7f8bd29

File tree

9 files changed

+261
-25
lines changed

9 files changed

+261
-25
lines changed

FirebaseAI/Sources/GenerationConfig.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public struct GenerationConfig: Sendable {
2828
let topK: Int?
2929

3030
/// The number of response variations to return.
31-
let candidateCount: Int?
31+
var candidateCount: Int?
3232

3333
/// Maximum number of tokens that can be generated in the response.
3434
let maxOutputTokens: Int?
@@ -43,18 +43,18 @@ public struct GenerationConfig: Sendable {
4343
let stopSequences: [String]?
4444

4545
/// Output response MIME type of the generated candidate text.
46-
let responseMIMEType: String?
46+
var responseMIMEType: String?
4747

4848
/// Output schema of the generated candidate text.
49-
let responseSchema: Schema?
49+
var responseSchema: Schema?
5050

5151
/// Output schema of the generated response in [JSON Schema](https://json-schema.org/) format.
5252
///
5353
/// If set, `responseSchema` must be omitted and `responseMIMEType` is required.
54-
let responseJSONSchema: JSONObject?
54+
var responseJSONSchema: JSONObject?
5555

5656
/// Supported modalities of the response.
57-
let responseModalities: [ResponseModality]?
57+
var responseModalities: [ResponseModality]?
5858

5959
/// Configuration for controlling the "thinking" behavior of compatible Gemini models.
6060
let thinkingConfig: ThinkingConfig?

FirebaseAI/Sources/GenerativeModel.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ public final class GenerativeModel: Sendable {
144144
/// - Throws: A ``GenerateContentError`` if the request failed.
145145
public func generateContent(_ content: [ModelContent]) async throws
146146
-> GenerateContentResponse {
147+
return try await generateContent(content, generationConfig: generationConfig)
148+
}
149+
150+
public func generateContent(_ content: [ModelContent],
151+
generationConfig: GenerationConfig?) async throws
152+
-> GenerateContentResponse {
147153
try content.throwIfError()
148154
let response: GenerateContentResponse
149155
let generateContentRequest = GenerateContentRequest(
@@ -357,6 +363,50 @@ public final class GenerativeModel: Sendable {
357363
return try await generativeAIService.loadRequest(request: countTokensRequest)
358364
}
359365

366+
/// Produces a generable object as a response to a prompt.
367+
///
368+
/// - Parameters:
369+
/// - prompt: A prompt for the model to respond to.
370+
/// - type: A type to produce as the response.
371+
/// - Returns: ``GeneratedContent`` containing the fields and values defined in the schema.
372+
@available(iOS 26.0, macOS 26.0, *)
373+
@available(tvOS, unavailable)
374+
@available(watchOS, unavailable)
375+
public final func generateObject<Content>(_ type: Content.Type = Content.self,
376+
parts: any PartsRepresentable...) async throws
377+
-> Response<Content>
378+
where Content: FoundationModels.Generable {
379+
let jsonSchema = try type.generationSchema.asGeminiJSONSchema()
380+
381+
let generationConfig = {
382+
var generationConfig = self.generationConfig ?? GenerationConfig()
383+
if generationConfig.candidateCount != nil {
384+
generationConfig.candidateCount = nil
385+
}
386+
generationConfig.responseMIMEType = "application/json"
387+
if generationConfig.responseSchema != nil {
388+
generationConfig.responseSchema = nil
389+
}
390+
generationConfig.responseJSONSchema = jsonSchema
391+
if generationConfig.responseModalities != nil {
392+
generationConfig.responseModalities = nil
393+
}
394+
395+
return generationConfig
396+
}()
397+
398+
let response = try await generateContent(
399+
[ModelContent(parts: parts)],
400+
generationConfig: generationConfig
401+
)
402+
403+
let generatedContent = try GeneratedContent(json: response.text ?? "")
404+
let content = try Content(generatedContent)
405+
let rawContent = try ModelOutput(generatedContent)
406+
407+
return Response(content: content, rawContent: rawContent)
408+
}
409+
360410
/// Returns a `GenerateContentError` (for public consumption) from an internal error.
361411
///
362412
/// If `error` is already a `GenerateContentError` the error is returned unchanged.
@@ -366,4 +416,18 @@ public final class GenerativeModel: Sendable {
366416
}
367417
return GenerateContentError.internalError(underlying: error)
368418
}
419+
420+
/// A structure that stores the output of a response call.
421+
@available(iOS 26.0, macOS 26.0, *)
422+
@available(tvOS, unavailable)
423+
@available(watchOS, unavailable)
424+
public struct Response<Content> {
425+
/// The response content.
426+
public let content: Content
427+
428+
/// The raw response content.
429+
///
430+
/// When `Content` is `ModelOutput`, this is the same as `content`.
431+
public let rawContent: ModelOutput
432+
}
369433
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#if canImport(FoundationModels)
16+
import Foundation
17+
import FoundationModels
18+
19+
@available(iOS 26.0, macOS 26.0, *)
20+
@available(tvOS, unavailable)
21+
@available(watchOS, unavailable)
22+
extension GenerationSchema {
23+
func asGeminiJSONSchema() throws -> JSONObject {
24+
let jsonData = try JSONEncoder().encode(self)
25+
var jsonSchema = try JSONDecoder().decode(JSONObject.self, from: jsonData)
26+
updatePropertyOrdering(&jsonSchema)
27+
28+
return jsonSchema
29+
}
30+
}
31+
32+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
33+
fileprivate func updatePropertyOrdering(_ schema: inout JSONObject) {
34+
guard let propertyOrdering = schema.removeValue(forKey: "x-order") else {
35+
return
36+
}
37+
guard case let .array(values) = propertyOrdering else {
38+
return
39+
}
40+
guard values.allSatisfy({
41+
guard case .string = $0 else { return false }
42+
return true
43+
}) else {
44+
return
45+
}
46+
47+
schema["propertyOrdering"] = propertyOrdering
48+
}
49+
#endif // canImport(FoundationModels)

FirebaseAI/Sources/Types/Public/Generable/ConvertibleFromModelOutput.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
// limitations under the License.
1414

1515
import Foundation
16+
#if canImport(FoundationModels)
17+
import FoundationModels
18+
#endif // canImport(FoundationModels)
1619

1720
/// A type that can be initialized from model output.
1821
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
@@ -42,3 +45,18 @@ public protocol ConvertibleFromModelOutput: SendableMetatype {
4245
/// - SeeAlso: `@Generable` macro ``Generable(description:)``
4346
init(_ content: ModelOutput) throws
4447
}
48+
49+
@available(iOS 26.0, macOS 26.0, *)
50+
@available(tvOS, unavailable)
51+
@available(watchOS, unavailable)
52+
extension FoundationModels.GeneratedContent: ConvertibleFromModelOutput {}
53+
54+
@available(iOS 26.0, macOS 26.0, *)
55+
@available(tvOS, unavailable)
56+
@available(watchOS, unavailable)
57+
public extension FoundationModels.ConvertibleFromGeneratedContent
58+
where Self: ConvertibleFromModelOutput {
59+
init(_ content: ModelOutput) throws {
60+
try self.init(content.generatedContent)
61+
}
62+
}

FirebaseAI/Sources/Types/Public/Generable/ConvertibleToModelOutput.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
#if canImport(FoundationModels)
16+
import FoundationModels
17+
#endif // canImport(FoundationModels)
18+
1519
/// A type that can be converted to model output.
1620
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
1721
public protocol ConvertibleToModelOutput {
@@ -41,3 +45,49 @@ public protocol ConvertibleToModelOutput {
4145
/// ``ConvertibleFromModelOutput/init(_:)``.
4246
var modelOutput: ModelOutput { get }
4347
}
48+
49+
@available(iOS 26.0, macOS 26.0, *)
50+
@available(tvOS, unavailable)
51+
@available(watchOS, unavailable)
52+
extension FoundationModels.GeneratedContent: ConvertibleToModelOutput {
53+
public var modelOutput: ModelOutput {
54+
switch kind {
55+
case .null:
56+
return ModelOutput(kind: .null)
57+
case let .bool(value):
58+
return ModelOutput(kind: .bool(value))
59+
case let .number(value):
60+
return ModelOutput(kind: .number(value))
61+
case let .string(value):
62+
return ModelOutput(kind: .string(value))
63+
case let .array(values):
64+
return ModelOutput(kind: .array(values.map { $0.modelOutput }))
65+
case let .structure(properties: properties, orderedKeys: orderedKeys):
66+
return ModelOutput(kind: .structure(
67+
properties: properties.mapValues { $0.modelOutput }, orderedKeys: orderedKeys
68+
))
69+
@unknown default:
70+
fatalError("Unsupported GeneratedContent kind: \(kind)")
71+
}
72+
}
73+
}
74+
75+
@available(iOS 26.0, macOS 26.0, *)
76+
@available(tvOS, unavailable)
77+
@available(watchOS, unavailable)
78+
public extension FoundationModels.ConvertibleToGeneratedContent
79+
where Self: ConvertibleToModelOutput {
80+
var generatedContent: GeneratedContent {
81+
modelOutput.generatedContent
82+
}
83+
}
84+
85+
@available(iOS 26.0, macOS 26.0, *)
86+
@available(tvOS, unavailable)
87+
@available(watchOS, unavailable)
88+
public extension ConvertibleToModelOutput
89+
where Self: FoundationModels.ConvertibleToGeneratedContent {
90+
var modelOutput: ModelOutput {
91+
generatedContent.modelOutput
92+
}
93+
}

FirebaseAI/Sources/Types/Public/Generable/Generable.swift renamed to FirebaseAI/Sources/Types/Public/Generable/FirebaseGenerable.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ import Foundation
4040
/// - SeeAlso: `@Generable` macro ``Generable(description:)`` and `@Guide` macro
4141
/// ``Guide(description:)``.
4242
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
43-
public protocol Generable: ConvertibleFromModelOutput, ConvertibleToModelOutput {
43+
public protocol FirebaseGenerable: ConvertibleFromModelOutput, ConvertibleToModelOutput {
4444
/// An instance of the JSON schema.
4545
static var jsonSchema: JSONSchema { get }
4646
}
4747

4848
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
49-
extension Optional where Wrapped: Generable {}
49+
extension Optional where Wrapped: FirebaseGenerable {}
5050

5151
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
5252
extension Optional: ConvertibleToModelOutput where Wrapped: ConvertibleToModelOutput {
@@ -58,7 +58,7 @@ extension Optional: ConvertibleToModelOutput where Wrapped: ConvertibleToModelOu
5858
}
5959

6060
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
61-
extension Bool: Generable {
61+
extension Bool: FirebaseGenerable {
6262
public static var jsonSchema: JSONSchema {
6363
JSONSchema(kind: .boolean, source: "Bool")
6464
}
@@ -76,7 +76,7 @@ extension Bool: Generable {
7676
}
7777

7878
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
79-
extension String: Generable {
79+
extension String: FirebaseGenerable {
8080
public static var jsonSchema: JSONSchema {
8181
JSONSchema(kind: .string, source: "String")
8282
}
@@ -94,7 +94,7 @@ extension String: Generable {
9494
}
9595

9696
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
97-
extension Int: Generable {
97+
extension Int: FirebaseGenerable {
9898
public static var jsonSchema: JSONSchema {
9999
JSONSchema(kind: .integer, source: "Int")
100100
}
@@ -112,7 +112,7 @@ extension Int: Generable {
112112
}
113113

114114
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
115-
extension Float: Generable {
115+
extension Float: FirebaseGenerable {
116116
public static var jsonSchema: JSONSchema {
117117
JSONSchema(kind: .double, source: "Number")
118118
}
@@ -130,7 +130,7 @@ extension Float: Generable {
130130
}
131131

132132
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
133-
extension Double: Generable {
133+
extension Double: FirebaseGenerable {
134134
public static var jsonSchema: JSONSchema {
135135
JSONSchema(kind: .double, source: "Number")
136136
}
@@ -148,7 +148,7 @@ extension Double: Generable {
148148
}
149149

150150
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
151-
extension Decimal: Generable {
151+
extension Decimal: FirebaseGenerable {
152152
public static var jsonSchema: JSONSchema {
153153
JSONSchema(kind: .double, source: "Number")
154154
}
@@ -167,7 +167,7 @@ extension Decimal: Generable {
167167
}
168168

169169
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
170-
extension Array: Generable where Element: Generable {
170+
extension Array: FirebaseGenerable where Element: FirebaseGenerable {
171171
public static var jsonSchema: JSONSchema {
172172
JSONSchema(kind: .array(item: Element.self), source: String(describing: self))
173173
}

FirebaseAI/Sources/Types/Public/Generable/JSONSchema.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public struct JSONSchema: Sendable {
2525
case integer
2626
case double
2727
case boolean
28-
case array(item: Generable.Type)
28+
case array(item: FirebaseGenerable.Type)
2929
case object(name: String, description: String?, properties: [Property])
3030
}
3131

@@ -45,7 +45,7 @@ public struct JSONSchema: Sendable {
4545
let name: String
4646
let description: String?
4747
let isOptional: Bool
48-
let type: Generable.Type
48+
let type: FirebaseGenerable.Type
4949
// TODO: Store `GenerationGuide` values.
5050

5151
/// Create a property that contains a generable type.
@@ -57,7 +57,7 @@ public struct JSONSchema: Sendable {
5757
/// - type: The type this property represents.
5858
/// - guides: A list of guides to apply to this property.
5959
public init<Value>(name: String, description: String? = nil, type: Value.Type,
60-
guides: [GenerationGuide<Value>] = []) where Value: Generable {
60+
guides: [GenerationGuide<Value>] = []) where Value: FirebaseGenerable {
6161
precondition(guides.isEmpty, "GenerationGuide support is not yet implemented.")
6262
self.name = name
6363
self.description = description
@@ -74,7 +74,7 @@ public struct JSONSchema: Sendable {
7474
/// - type: The type this property represents.
7575
/// - guides: A list of guides to apply to this property.
7676
public init<Value>(name: String, description: String? = nil, type: Value?.Type,
77-
guides: [GenerationGuide<Value>] = []) where Value: Generable {
77+
guides: [GenerationGuide<Value>] = []) where Value: FirebaseGenerable {
7878
precondition(guides.isEmpty, "GenerationGuide support is not yet implemented.")
7979
self.name = name
8080
self.description = description
@@ -89,7 +89,7 @@ public struct JSONSchema: Sendable {
8989
/// - type: The type this schema represents.
9090
/// - description: A natural language description of this schema.
9191
/// - properties: An array of properties.
92-
public init(type: any Generable.Type, description: String? = nil,
92+
public init(type: any FirebaseGenerable.Type, description: String? = nil,
9393
properties: [JSONSchema.Property]) {
9494
let name = String(describing: type)
9595
kind = .object(name: name, description: description, properties: properties)
@@ -102,7 +102,8 @@ public struct JSONSchema: Sendable {
102102
/// - type: The type this schema represents.
103103
/// - description: A natural language description of this schema.
104104
/// - anyOf: The allowed choices.
105-
public init(type: any Generable.Type, description: String? = nil, anyOf choices: [String]) {
105+
public init(type: any FirebaseGenerable.Type, description: String? = nil,
106+
anyOf choices: [String]) {
106107
fatalError("`GenerationSchema.init(type:description:anyOf:)` is not implemented.")
107108
}
108109

@@ -112,8 +113,8 @@ public struct JSONSchema: Sendable {
112113
/// - type: The type this schema represents.
113114
/// - description: A natural language description of this schema.
114115
/// - anyOf: The types this schema should be a union of.
115-
public init(type: any Generable.Type, description: String? = nil,
116-
anyOf types: [any Generable.Type]) {
116+
public init(type: any FirebaseGenerable.Type, description: String? = nil,
117+
anyOf types: [any FirebaseGenerable.Type]) {
117118
fatalError("`GenerationSchema.init(type:description:anyOf:)` is not implemented.")
118119
}
119120

0 commit comments

Comments
 (0)