Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 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
29 changes: 29 additions & 0 deletions FirebaseAI/Sources/GenerateObjectError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

enum GenerateObjectError: Error, CustomStringConvertible {
case responseTextError(String)
case jsonDecodingError(Error)

var description: String {
switch self {
case let .responseTextError(message):
return message
case let .jsonDecodingError(error):
return "Failed to decode JSON: \(error)"
}
}
}
20 changes: 20 additions & 0 deletions FirebaseAI/Sources/GenerationConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,26 @@ public struct GenerationConfig: Sendable {
self.responseModalities = responseModalities
self.thinkingConfig = thinkingConfig
}

/// Internal initializer to create a new config by overriding specific values of another.
init(from base: GenerationConfig?,
responseMIMEType: String,
responseSchema: Schema) {
self.init(
temperature: base?.temperature,
topP: base?.topP,
topK: base?.topK,
candidateCount: base?.candidateCount,
maxOutputTokens: base?.maxOutputTokens,
presencePenalty: base?.presencePenalty,
frequencyPenalty: base?.frequencyPenalty,
stopSequences: base?.stopSequences,
responseMIMEType: responseMIMEType,
responseSchema: responseSchema,
responseModalities: base?.responseModalities,
thinkingConfig: base?.thinkingConfig
)
}
}

// MARK: - Codable Conformances
Expand Down
2 changes: 1 addition & 1 deletion FirebaseAI/Sources/GenerativeAIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct GenerativeAIService {

let firebaseInfo: FirebaseInfo

private let urlSession: URLSession
let urlSession: URLSession

init(firebaseInfo: FirebaseInfo, urlSession: URLSession) {
self.firebaseInfo = firebaseInfo
Expand Down
56 changes: 56 additions & 0 deletions FirebaseAI/Sources/GenerativeModel+GenerateObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public extension GenerativeModel {
/// Generates a structured object of a specified type that conforms to ``FirebaseGenerable``.
///
/// This method simplifies the process of generating structured data by handling the schema
/// generation, API request, and JSON decoding automatically.
///
/// - Parameters:
/// - type: The `FirebaseGenerable` type to generate.
/// - prompt: The text prompt to send to the model.
/// - Returns: An instance of the requested type, decoded from the model's JSON response.
/// - Throws: A ``GenerateContentError`` if the model fails to generate the content or if
/// the response cannot be decoded into the specified type.
func generateObject<T: FirebaseGenerable>(as type: T.Type,
from prompt: String) async throws -> T {
// Create a new generation config, inheriting previous settings and overriding for JSON output.
let newGenerationConfig = GenerationConfig(
from: generationConfig,
responseMIMEType: "application/json",
responseSchema: T.firebaseGenerationSchema
)

// Create a new model instance with the overridden config.
let model = GenerativeModel(copying: self, generationConfig: newGenerationConfig)
let response = try await model.generateContent(prompt)

guard let text = response.text, let data = text.data(using: .utf8) else {
throw GenerateContentError.internalError(
underlying: GenerateObjectError.responseTextError("Failed to get response text or data.")
)
}

do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw GenerateContentError
.internalError(underlying: GenerateObjectError.jsonDecodingError(error))
}
}
}
18 changes: 18 additions & 0 deletions FirebaseAI/Sources/GenerativeModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ public final class GenerativeModel: Sendable {
AILog.debug(code: .generativeModelInitialized, "Model \(modelResourceName) initialized.")
}

/// Internal initializer to create a new model by copying an existing one with specific overrides.
convenience init(copying other: GenerativeModel,
generationConfig: GenerationConfig? = nil) {
self.init(
modelName: other.modelName,
modelResourceName: other.modelResourceName,
firebaseInfo: other.generativeAIService.firebaseInfo,
apiConfig: other.apiConfig,
generationConfig: generationConfig ?? other.generationConfig,
safetySettings: other.safetySettings,
tools: other.tools,
toolConfig: other.toolConfig,
systemInstruction: other.systemInstruction,
requestOptions: other.requestOptions,
urlSession: other.generativeAIService.urlSession
)
}

/// Generates content from String and/or image inputs, given to the model as a prompt, that are
/// representable as one or more ``Part``s.
///
Expand Down
22 changes: 22 additions & 0 deletions FirebaseAI/Sources/Types/Public/FirebaseGenerable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

/// A type that can be generated by a large language model.
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol FirebaseGenerable: Decodable {
/// A schema describing the structure of the generated type.
static var firebaseGenerationSchema: Schema { get }
}
23 changes: 23 additions & 0 deletions FirebaseAI/Sources/Types/Public/FirebaseGenerableMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

/// A macro that makes a ``Decodable`` type ``FirebaseGenerable``.
@attached(member, names: named(firebaseGenerationSchema))
@attached(extension, conformances: FirebaseGenerable)
public macro FirebaseGenerable() = #externalMacro(
module: "FirebaseAILogicMacro",
type: "FirebaseGenerableMacro"
)
38 changes: 38 additions & 0 deletions FirebaseAI/Sources/Types/Public/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,27 @@ public final class Schema: Sendable {
self.propertyOrdering = propertyOrdering
}

/// Private initializer to create a new schema by copying an existing one with specific overrides.
private convenience init(copying other: Schema, nullable: Bool? = nil) {
self.init(
type: other.dataType,
format: other.format,
description: other.description,
title: other.title,
nullable: nullable ?? other.nullable,
enumValues: other.enumValues,
items: other.items,
minItems: other.minItems,
maxItems: other.maxItems,
minimum: other.minimum,
maximum: other.maximum,
anyOf: other.anyOf,
properties: other.properties,
requiredProperties: other.requiredProperties,
propertyOrdering: other.propertyOrdering
)
}

/// Returns a `Schema` representing a string value.
///
/// This schema instructs the model to produce data of type `"STRING"`, which is suitable for
Expand Down Expand Up @@ -481,3 +502,20 @@ extension Schema: Encodable {
case propertyOrdering
}
}

// MARK: - Helpers

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
public extension Schema {
/// Returns a new schema that is identical to the receiver, but with the `nullable`
/// property set to `true`.
///
/// This is useful for representing optional types. For example, if you have a schema
/// for a `User` object, you can represent an optional `User?` by calling
/// `userSchema.nullable()`.
///
/// - Returns: A new `Schema` instance with `nullable` set to `true`.
func asNullable() -> Schema {
return Schema(copying: self, nullable: true)
}
}
Loading
Loading