From de563324c36fda2e925ca60b05b0e9aad75e2b21 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 25 May 2026 20:38:46 -0400 Subject: [PATCH 1/4] [AI] Add `SystemInstructions` type with `resultBuilder` --- .../SystemLanguageModel+LanguageModel.swift | 4 +- FirebaseAI/Sources/FirebaseAI.swift | 36 +++++- .../Sources/GenerativeModelSession.swift | 6 +- .../ConvertibleToGeneratedContent.swift | 2 +- .../Protocols/Public/LanguageModel.swift | 2 +- .../SystemInstructionsRepresentable.swift | 52 +++++++++ .../Types/Internal/HybridModelSession.swift | 4 +- .../Sources/Types/Public/GeminiModel.swift | 4 +- .../Sources/Types/Public/HybridModel.swift | 2 +- .../StructuredOutput/GeneratedContent.swift | 55 ++++++++-- .../Types/Public/SystemInstructions.swift | 76 +++++++++++++ .../Public/SystemInstructionsBuilder.swift | 62 +++++++++++ FirebaseAI/Tests/Unit/HybridModelTests.swift | 2 +- .../Unit/Types/GeneratedContentTests.swift | 103 ++++++++++++++++++ 14 files changed, 386 insertions(+), 24 deletions(-) create mode 100644 FirebaseAI/Sources/Protocols/Public/SystemInstructionsRepresentable.swift create mode 100644 FirebaseAI/Sources/Types/Public/SystemInstructions.swift create mode 100644 FirebaseAI/Sources/Types/Public/SystemInstructionsBuilder.swift create mode 100644 FirebaseAI/Tests/Unit/Types/GeneratedContentTests.swift diff --git a/FirebaseAI/Sources/Extensions/Public/SystemLanguageModel+LanguageModel.swift b/FirebaseAI/Sources/Extensions/Public/SystemLanguageModel+LanguageModel.swift index 045fdb1c82d..204990516b1 100644 --- a/FirebaseAI/Sources/Extensions/Public/SystemLanguageModel+LanguageModel.swift +++ b/FirebaseAI/Sources/Extensions/Public/SystemLanguageModel+LanguageModel.swift @@ -32,7 +32,7 @@ /// /// > Important: This method is for **internal use only** and may change at any time. public func _startSession(tools: [any ToolRepresentable]?, - instructions: String?) throws -> any _ModelSession { + instructions: SystemInstructions?) throws -> any _ModelSession { switch availability { case .available: break @@ -81,7 +81,7 @@ return LanguageModelSession( model: self.systemLanguageModel, tools: afmTools, - instructions: instructions + instructions: instructions?.toFoundationModels() ) } #endif // canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM diff --git a/FirebaseAI/Sources/FirebaseAI.swift b/FirebaseAI/Sources/FirebaseAI.swift index c9bc6df3447..42bb03f62ea 100644 --- a/FirebaseAI/Sources/FirebaseAI.swift +++ b/FirebaseAI/Sources/FirebaseAI.swift @@ -147,7 +147,8 @@ public final class FirebaseAI: Sendable { /// - instructions: System instructions that direct the model's behavior. public func generativeModelSession(model: any LanguageModelProvider, tools: [any ToolRepresentable]? = nil, - instructions: String? = nil) -> GenerativeModelSession { + instructions: SystemInstructions? = nil) + -> GenerativeModelSession { let tools = tools?.map { $0.toolRepresentation } return GenerativeModelSession( @@ -157,6 +158,25 @@ public final class FirebaseAI: Sendable { ) } + public func generativeModelSession(model: any LanguageModelProvider, + tools: [any ToolRepresentable]? = nil, + @SystemInstructionsBuilder instructions: () throws + -> SystemInstructions) rethrows -> GenerativeModelSession { + let tools = tools?.map { $0.toolRepresentation } + + return try generativeModelSession(model: model, tools: tools as [any ToolRepresentable]?, + instructions: instructions()) + } + + public func generativeModelSession(model: any LanguageModelProvider, + tools: [any ToolRepresentable]? = nil, + instructions: String?) -> GenerativeModelSession { + let tools = tools?.map { $0.toolRepresentation } + + return generativeModelSession(model: model, tools: tools as [any ToolRepresentable]?, + instructions: instructions) + } + #if canImport(FoundationModels) /// **[Public Preview]** Initializes a `GenerativeModelSession` with the given model. /// @@ -180,6 +200,20 @@ public final class FirebaseAI: Sendable { return generativeModelSession(model: model, tools: tools as [any ToolRepresentable]?, instructions: instructions) } + + @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + public func generativeModelSession(model: any LanguageModelProvider, + tools: [any FoundationModels.Tool], + @SystemInstructionsBuilder instructions: () throws + -> SystemInstructions) rethrows + -> GenerativeModelSession { + let tools = tools.map { FirebaseAILogic.Tool.autoFunctionDeclaration($0) } + + return try generativeModelSession(model: model, tools: tools as [any ToolRepresentable]?, + instructions: instructions) + } #endif // canImport(FoundationModels) #endif // compiler(>=6.2.3) diff --git a/FirebaseAI/Sources/GenerativeModelSession.swift b/FirebaseAI/Sources/GenerativeModelSession.swift index 9a26153df3a..950518f87f6 100644 --- a/FirebaseAI/Sources/GenerativeModelSession.swift +++ b/FirebaseAI/Sources/GenerativeModelSession.swift @@ -53,7 +53,7 @@ /// ``` public final class GenerativeModelSession: Sendable { let sessionManager: SessionManager - let instructions: String? + let instructions: SystemInstructions? // The maximum number of automatic back-and-forth turns the session will perform to resolve // function calls. @@ -74,7 +74,7 @@ /// or instances conforming to ``ToolRepresentable`` for automatic function calling. /// - instructions: System instructions that direct the model's behavior. init(model: any LanguageModel, tools: [any ToolRepresentable]? = nil, - instructions: String? = nil) { + instructions: SystemInstructions? = nil) { sessionManager = SessionManager(model: model, tools: tools) self.instructions = instructions } @@ -438,7 +438,7 @@ } } - func getOrStartSession(instructions: String?) throws -> any _ModelSession { + func getOrStartSession(instructions: SystemInstructions?) throws -> any _ModelSession { try _isResponding.withLock { isResponding in if let currentSession = _activeSession { return currentSession diff --git a/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift b/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift index 9059b6b9fb1..21944fe64d0 100644 --- a/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift +++ b/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift @@ -18,7 +18,7 @@ #endif // canImport(FoundationModels) extension FirebaseAI { - protocol ConvertibleToGeneratedContent { + protocol ConvertibleToGeneratedContent: SystemInstructionsRepresentable { var firebaseGeneratedContent: FirebaseAI.GeneratedContent { get } } } diff --git a/FirebaseAI/Sources/Protocols/Public/LanguageModel.swift b/FirebaseAI/Sources/Protocols/Public/LanguageModel.swift index 510595b2a02..44109ee7c2f 100644 --- a/FirebaseAI/Sources/Protocols/Public/LanguageModel.swift +++ b/FirebaseAI/Sources/Protocols/Public/LanguageModel.swift @@ -23,7 +23,7 @@ /// Returns a new session for this model. /// /// > Important: This method is for **internal use only** and may change at any time. - func _startSession(tools: [any ToolRepresentable]?, instructions: String?) throws + func _startSession(tools: [any ToolRepresentable]?, instructions: SystemInstructions?) throws -> any _ModelSession } diff --git a/FirebaseAI/Sources/Protocols/Public/SystemInstructionsRepresentable.swift b/FirebaseAI/Sources/Protocols/Public/SystemInstructionsRepresentable.swift new file mode 100644 index 00000000000..c51763508d5 --- /dev/null +++ b/FirebaseAI/Sources/Protocols/Public/SystemInstructionsRepresentable.swift @@ -0,0 +1,52 @@ +// Copyright 2026 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. + +#if canImport(FoundationModels) + import FoundationModels +#endif // canImport(FoundationModels) + +public protocol SystemInstructionsRepresentable { + @SystemInstructionsBuilder + var systemInstructionsRepresentation: SystemInstructions { get } +} + +extension String: SystemInstructionsRepresentable { + public var systemInstructionsRepresentation: SystemInstructions { + return SystemInstructions(parts: [.text(self)]) + } +} + +extension Array: SystemInstructionsRepresentable where Element: SystemInstructionsRepresentable { + public var systemInstructionsRepresentation: SystemInstructions { + let allParts = flatMap { $0.systemInstructionsRepresentation.parts } + return SystemInstructions(parts: allParts) + } +} + +extension FirebaseAI.GeneratedContent: SystemInstructionsRepresentable { + public var systemInstructionsRepresentation: SystemInstructions { + return SystemInstructions(parts: [.structure(self)]) + } +} + +#if canImport(FoundationModels) + @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + extension FoundationModels.GeneratedContent: SystemInstructionsRepresentable { + public var systemInstructionsRepresentation: SystemInstructions { + SystemInstructions(parts: [.structure(firebaseGeneratedContent)]) + } + } +#endif // canImport(FoundationModels) diff --git a/FirebaseAI/Sources/Types/Internal/HybridModelSession.swift b/FirebaseAI/Sources/Types/Internal/HybridModelSession.swift index 21c08674891..bbcdd626be5 100644 --- a/FirebaseAI/Sources/Types/Internal/HybridModelSession.swift +++ b/FirebaseAI/Sources/Types/Internal/HybridModelSession.swift @@ -19,13 +19,13 @@ private let primaryModel: any LanguageModel private let secondaryModel: any LanguageModel private let tools: [any ToolRepresentable]? - private let instructions: String? + private let instructions: SystemInstructions? typealias SessionState = (primary: (any _ModelSession)?, secondary: (any _ModelSession)?) private let lock: UnfairLock init(primaryModel: any LanguageModel, secondaryModel: any LanguageModel, - tools: [any ToolRepresentable]?, instructions: String?) { + tools: [any ToolRepresentable]?, instructions: SystemInstructions?) { self.primaryModel = primaryModel self.secondaryModel = secondaryModel self.tools = tools diff --git a/FirebaseAI/Sources/Types/Public/GeminiModel.swift b/FirebaseAI/Sources/Types/Public/GeminiModel.swift index 17b798a89b1..5ff20bc9e3c 100644 --- a/FirebaseAI/Sources/Types/Public/GeminiModel.swift +++ b/FirebaseAI/Sources/Types/Public/GeminiModel.swift @@ -62,7 +62,7 @@ import Foundation /// /// > Important: This method is for **internal use only** and may change at any time. public func _startSession(tools: [any ToolRepresentable]?, - instructions: String?) throws -> any _ModelSession { + instructions: SystemInstructions?) throws -> any _ModelSession { let model = GenerativeModel( modelName: modelName, modelResourceName: modelResourceName, @@ -72,7 +72,7 @@ import Foundation safetySettings: safetySettings, tools: tools?.map { $0.toolRepresentation }, // TODO: Add toolConfig - systemInstruction: instructions.map { ModelContent(role: "system", parts: $0) }, + systemInstruction: instructions?.toModelContent(), requestOptions: requestOptions, urlSession: urlSession ) diff --git a/FirebaseAI/Sources/Types/Public/HybridModel.swift b/FirebaseAI/Sources/Types/Public/HybridModel.swift index 56f790d232b..9098f5b77ce 100644 --- a/FirebaseAI/Sources/Types/Public/HybridModel.swift +++ b/FirebaseAI/Sources/Types/Public/HybridModel.swift @@ -45,7 +45,7 @@ /// /// > Important: This method is for **internal use only** and may change at any time. public func _startSession(tools: [any ToolRepresentable]?, - instructions: String?) throws -> any _ModelSession { + instructions: SystemInstructions?) throws -> any _ModelSession { return HybridModelSession( primaryModel: primary, secondaryModel: secondary, diff --git a/FirebaseAI/Sources/Types/Public/StructuredOutput/GeneratedContent.swift b/FirebaseAI/Sources/Types/Public/StructuredOutput/GeneratedContent.swift index 266eceac6fb..d6ab7d22603 100644 --- a/FirebaseAI/Sources/Types/Public/StructuredOutput/GeneratedContent.swift +++ b/FirebaseAI/Sources/Types/Public/StructuredOutput/GeneratedContent.swift @@ -122,17 +122,43 @@ self.isComplete = isComplete } - #if canImport(FoundationModels) - /// The JSON string representation of the generated content. - /// - /// **Public Preview**: This API is a public preview and may be subject to change. - @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - public var jsonString: String { - return generatedContent.jsonString + /// The JSON string representation of the generated content. + /// + /// **Public Preview**: This API is a public preview and may be subject to change. + public var jsonString: String { + #if canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM + if #available(iOS 26.0, macOS 26.0, visionOS 26.0, *) { + return generatedContent.jsonString + } + #endif // canImport(FoundationModels) && IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM + + switch kind { + case .null: + return "null" + case let .bool(value): + return String(value) + case let .number(value): + return String(value) + case let .string(string): + return "\"\(string)\"" + case let .array(values): + return "[\(values.map { $0.jsonString }.joined(separator: ", "))]" + case let .structure(properties, orderedKeys): + let keysToEncode: [String] + if orderedKeys.isEmpty { + keysToEncode = properties.keys.sorted() + } else { + let orderedKeysSet = Set(orderedKeys) + let missingKeys = properties.keys.filter { !orderedKeysSet.contains($0) }.sorted() + keysToEncode = orderedKeys + missingKeys + } + let keyValuePairs = keysToEncode.compactMap { key -> String? in + guard let value = properties[key] else { return nil } + return "\"\(key)\": \(value.jsonString)" + } + return "{\(keyValuePairs.joined(separator: ", "))}" } - #endif // canImport(FoundationModels) + } // TODO: Replace `ConvertibleFromGeneratedContent` with custom protocol @@ -249,6 +275,15 @@ } } + @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + extension FirebaseAI.GeneratedContent: FoundationModels.InstructionsRepresentable { + public var instructionsRepresentation: Instructions { + return Instructions(generatedContent) + } + } + @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) diff --git a/FirebaseAI/Sources/Types/Public/SystemInstructions.swift b/FirebaseAI/Sources/Types/Public/SystemInstructions.swift new file mode 100644 index 00000000000..d0756cf4b44 --- /dev/null +++ b/FirebaseAI/Sources/Types/Public/SystemInstructions.swift @@ -0,0 +1,76 @@ +// Copyright 2026 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. + +#if canImport(FoundationModels) + import FoundationModels +#endif // canImport(FoundationModels) + +public struct SystemInstructions: Sendable { + enum InstructionsPart { + case text(String) + case structure(FirebaseAI.GeneratedContent) + } + + let parts: [InstructionsPart] + + init(parts: [InstructionsPart]) { + self.parts = parts + } + + public init(_ content: some SystemInstructionsRepresentable) { + parts = content.systemInstructionsRepresentation.parts + } + + public init(@SystemInstructionsBuilder _ content: () throws -> SystemInstructions) rethrows { + self = try content() + } + + #if canImport(FoundationModels) + @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + func toFoundationModels() -> FoundationModels.Instructions { + return FoundationModels.Instructions { + for part in parts { + switch part { + case let .text(text): + text + case let .structure(structure): + structure + } + } + } + } + #endif // canImport(FoundationModels) + + func toModelContent() -> ModelContent { + return ModelContent( + role: "system", + parts: parts.map { + switch $0 { + case let .text(string): + return TextPart(string) + case let .structure(generatedContent): + return TextPart(generatedContent.jsonString) + } + } + ) + } +} + +extension SystemInstructions: SystemInstructionsRepresentable { + public var systemInstructionsRepresentation: SystemInstructions { + self + } +} diff --git a/FirebaseAI/Sources/Types/Public/SystemInstructionsBuilder.swift b/FirebaseAI/Sources/Types/Public/SystemInstructionsBuilder.swift new file mode 100644 index 00000000000..fa593b01cbd --- /dev/null +++ b/FirebaseAI/Sources/Types/Public/SystemInstructionsBuilder.swift @@ -0,0 +1,62 @@ +// Copyright 2026 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. + +#if canImport(FoundationModels) + import FoundationModels +#endif // canImport(FoundationModels) + +@resultBuilder +public struct SystemInstructionsBuilder { + public static func buildBlock(_ components: repeat each I) -> SystemInstructions + where repeat each I: SystemInstructionsRepresentable { + var allComponents: [SystemInstructions.InstructionsPart] = [] + repeat allComponents + .append(contentsOf: (each components).systemInstructionsRepresentation.parts) + return SystemInstructions(parts: allComponents) + } + + public static func buildArray(_ instructions: [some SystemInstructionsRepresentable]) + -> SystemInstructions { + let allComponents = instructions.flatMap { $0.systemInstructionsRepresentation.parts } + return SystemInstructions(parts: allComponents) + } + + public static func buildEither(first component: some SystemInstructionsRepresentable) + -> SystemInstructions { + component.systemInstructionsRepresentation + } + + public static func buildEither(second component: some SystemInstructionsRepresentable) + -> SystemInstructions { + component.systemInstructionsRepresentation + } + + public static func buildOptional(_ instructions: SystemInstructions?) -> SystemInstructions { + instructions ?? SystemInstructions(parts: []) + } + + public static func buildLimitedAvailability(_ instructions: some SystemInstructionsRepresentable) + -> SystemInstructions { + instructions.systemInstructionsRepresentation + } + + public static func buildExpression(_ expression: I) -> I + where I: SystemInstructionsRepresentable { + expression + } + + public static func buildExpression(_ expression: SystemInstructions) -> SystemInstructions { + expression + } +} diff --git a/FirebaseAI/Tests/Unit/HybridModelTests.swift b/FirebaseAI/Tests/Unit/HybridModelTests.swift index 0134ea2cf8b..520c1b44612 100644 --- a/FirebaseAI/Tests/Unit/HybridModelTests.swift +++ b/FirebaseAI/Tests/Unit/HybridModelTests.swift @@ -23,7 +23,7 @@ let startSessionHandler: @Sendable () throws -> any _ModelSession func _startSession(tools: [any ToolRepresentable]?, - instructions: String?) throws -> any _ModelSession { + instructions: SystemInstructions?) throws -> any _ModelSession { return try startSessionHandler() } } diff --git a/FirebaseAI/Tests/Unit/Types/GeneratedContentTests.swift b/FirebaseAI/Tests/Unit/Types/GeneratedContentTests.swift new file mode 100644 index 00000000000..603f0e66efe --- /dev/null +++ b/FirebaseAI/Tests/Unit/Types/GeneratedContentTests.swift @@ -0,0 +1,103 @@ +// Copyright 2026 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 FirebaseAILogic +import XCTest + +#if compiler(>=6.2.3) + @testable import FirebaseAILogic + import XCTest + + final class GeneratedContentTests: XCTestCase { + func testJsonString_null() throws { + let content = FirebaseAI.GeneratedContent(kind: .null, isComplete: true) + XCTAssertEqual(content.jsonString, "null") + } + + func testJsonString_bool() throws { + let contentTrue = FirebaseAI.GeneratedContent(kind: .bool(true), isComplete: true) + XCTAssertEqual(contentTrue.jsonString, "true") + + let contentFalse = FirebaseAI.GeneratedContent(kind: .bool(false), isComplete: true) + XCTAssertEqual(contentFalse.jsonString, "false") + } + + func testJsonString_number() throws { + let content = FirebaseAI.GeneratedContent(kind: .number(123.45), isComplete: true) + XCTAssertEqual(content.jsonString, "123.45") + } + + func testJsonString_string() throws { + let content = FirebaseAI.GeneratedContent(kind: .string("test-string"), isComplete: true) + XCTAssertEqual(content.jsonString, "\"test-string\"") + } + + func testJsonString_array() throws { + let content = FirebaseAI.GeneratedContent( + kind: .array([ + FirebaseAI.GeneratedContent(kind: .null, isComplete: true), + FirebaseAI.GeneratedContent(kind: .bool(true), isComplete: true), + FirebaseAI.GeneratedContent(kind: .number(12.5), isComplete: true), + FirebaseAI.GeneratedContent(kind: .string("abc"), isComplete: true), + ]), + isComplete: true + ) + XCTAssertEqual(content.jsonString, "[null, true, 12.5, \"abc\"]") + } + + func testJsonString_structureWithOrderedKeys() throws { + let content = FirebaseAI.GeneratedContent( + kind: .structure( + properties: [ + "z": FirebaseAI.GeneratedContent(kind: .number(1.5), isComplete: true), + "a": FirebaseAI.GeneratedContent(kind: .string("first"), isComplete: true), + "m": FirebaseAI.GeneratedContent(kind: .bool(false), isComplete: true), + ], + orderedKeys: ["m", "a", "z"] + ), + isComplete: true + ) + XCTAssertEqual(content.jsonString, "{\"m\": false, \"a\": \"first\", \"z\": 1.5}") + } + + func testJsonString_structureWithEmptyOrderedKeys() throws { + let content = FirebaseAI.GeneratedContent( + kind: .structure( + properties: [ + "z": FirebaseAI.GeneratedContent(kind: .number(1), isComplete: true), + "a": FirebaseAI.GeneratedContent(kind: .string("first"), isComplete: true), + ], + orderedKeys: [] + ), + isComplete: true + ) + XCTAssertEqual(content.jsonString, "{}") + } + + func testJsonString_structureWithPartialOrderedKeys() throws { + let content = FirebaseAI.GeneratedContent( + kind: .structure( + properties: [ + "z": FirebaseAI.GeneratedContent(kind: .number(1), isComplete: true), + "a": FirebaseAI.GeneratedContent(kind: .string("first"), isComplete: true), + "m": FirebaseAI.GeneratedContent(kind: .bool(false), isComplete: true), + ], + orderedKeys: ["m"] + ), + isComplete: true + ) + XCTAssertEqual(content.jsonString, "{\"m\": false}") + } + } +#endif // compiler(>=6.2.3) From 99490a3d65ac753cc38f00cf194ee8be0f05e2d4 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 28 May 2026 12:06:15 -0400 Subject: [PATCH 2/4] Fix `GeneratedContent.jsonString` to match Foundation Models encoding --- .../Public/StructuredOutput/GeneratedContent.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/FirebaseAI/Sources/Types/Public/StructuredOutput/GeneratedContent.swift b/FirebaseAI/Sources/Types/Public/StructuredOutput/GeneratedContent.swift index d6ab7d22603..683b09531bf 100644 --- a/FirebaseAI/Sources/Types/Public/StructuredOutput/GeneratedContent.swift +++ b/FirebaseAI/Sources/Types/Public/StructuredOutput/GeneratedContent.swift @@ -144,15 +144,7 @@ case let .array(values): return "[\(values.map { $0.jsonString }.joined(separator: ", "))]" case let .structure(properties, orderedKeys): - let keysToEncode: [String] - if orderedKeys.isEmpty { - keysToEncode = properties.keys.sorted() - } else { - let orderedKeysSet = Set(orderedKeys) - let missingKeys = properties.keys.filter { !orderedKeysSet.contains($0) }.sorted() - keysToEncode = orderedKeys + missingKeys - } - let keyValuePairs = keysToEncode.compactMap { key -> String? in + let keyValuePairs = orderedKeys.compactMap { key -> String? in guard let value = properties[key] else { return nil } return "\"\(key)\": \(value.jsonString)" } From 9ec40b22485d01a2fd82d9529e615ebb31158a1a Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 28 May 2026 12:28:35 -0400 Subject: [PATCH 3/4] Rename `SystemInstructions` to `FirebaseAI.Instructions` --- .../SystemLanguageModel+LanguageModel.swift | 2 +- FirebaseAI/Sources/FirebaseAI.swift | 11 +-- .../Sources/GenerativeModelSession.swift | 6 +- .../ConvertibleToGeneratedContent.swift | 2 +- .../Public/InstructionsRepresentable.swift | 55 +++++++++++++ .../Protocols/Public/LanguageModel.swift | 4 +- .../SystemInstructionsRepresentable.swift | 52 ------------- .../Types/Internal/HybridModelSession.swift | 4 +- .../Sources/Types/Public/GeminiModel.swift | 2 +- .../Sources/Types/Public/HybridModel.swift | 2 +- .../Sources/Types/Public/Instructions.swift | 78 +++++++++++++++++++ .../Types/Public/InstructionsBuilder.swift | 76 ++++++++++++++++++ .../Types/Public/SystemInstructions.swift | 76 ------------------ .../Public/SystemInstructionsBuilder.swift | 62 --------------- FirebaseAI/Tests/Unit/HybridModelTests.swift | 2 +- 15 files changed, 227 insertions(+), 207 deletions(-) create mode 100644 FirebaseAI/Sources/Protocols/Public/InstructionsRepresentable.swift delete mode 100644 FirebaseAI/Sources/Protocols/Public/SystemInstructionsRepresentable.swift create mode 100644 FirebaseAI/Sources/Types/Public/Instructions.swift create mode 100644 FirebaseAI/Sources/Types/Public/InstructionsBuilder.swift delete mode 100644 FirebaseAI/Sources/Types/Public/SystemInstructions.swift delete mode 100644 FirebaseAI/Sources/Types/Public/SystemInstructionsBuilder.swift diff --git a/FirebaseAI/Sources/Extensions/Public/SystemLanguageModel+LanguageModel.swift b/FirebaseAI/Sources/Extensions/Public/SystemLanguageModel+LanguageModel.swift index 204990516b1..ba6794350eb 100644 --- a/FirebaseAI/Sources/Extensions/Public/SystemLanguageModel+LanguageModel.swift +++ b/FirebaseAI/Sources/Extensions/Public/SystemLanguageModel+LanguageModel.swift @@ -32,7 +32,7 @@ /// /// > Important: This method is for **internal use only** and may change at any time. public func _startSession(tools: [any ToolRepresentable]?, - instructions: SystemInstructions?) throws -> any _ModelSession { + instructions: FirebaseAI.Instructions?) throws -> any _ModelSession { switch availability { case .available: break diff --git a/FirebaseAI/Sources/FirebaseAI.swift b/FirebaseAI/Sources/FirebaseAI.swift index 42bb03f62ea..407e38d8633 100644 --- a/FirebaseAI/Sources/FirebaseAI.swift +++ b/FirebaseAI/Sources/FirebaseAI.swift @@ -147,7 +147,7 @@ public final class FirebaseAI: Sendable { /// - instructions: System instructions that direct the model's behavior. public func generativeModelSession(model: any LanguageModelProvider, tools: [any ToolRepresentable]? = nil, - instructions: SystemInstructions? = nil) + instructions: FirebaseAI.Instructions? = nil) -> GenerativeModelSession { let tools = tools?.map { $0.toolRepresentation } @@ -160,8 +160,9 @@ public final class FirebaseAI: Sendable { public func generativeModelSession(model: any LanguageModelProvider, tools: [any ToolRepresentable]? = nil, - @SystemInstructionsBuilder instructions: () throws - -> SystemInstructions) rethrows -> GenerativeModelSession { + @FirebaseAI.InstructionsBuilder instructions: () throws + -> FirebaseAI.Instructions) rethrows + -> GenerativeModelSession { let tools = tools?.map { $0.toolRepresentation } return try generativeModelSession(model: model, tools: tools as [any ToolRepresentable]?, @@ -206,8 +207,8 @@ public final class FirebaseAI: Sendable { @available(watchOS, unavailable) public func generativeModelSession(model: any LanguageModelProvider, tools: [any FoundationModels.Tool], - @SystemInstructionsBuilder instructions: () throws - -> SystemInstructions) rethrows + @FirebaseAI.InstructionsBuilder instructions: () throws + -> FirebaseAI.Instructions) rethrows -> GenerativeModelSession { let tools = tools.map { FirebaseAILogic.Tool.autoFunctionDeclaration($0) } diff --git a/FirebaseAI/Sources/GenerativeModelSession.swift b/FirebaseAI/Sources/GenerativeModelSession.swift index 950518f87f6..75da09c605d 100644 --- a/FirebaseAI/Sources/GenerativeModelSession.swift +++ b/FirebaseAI/Sources/GenerativeModelSession.swift @@ -53,7 +53,7 @@ /// ``` public final class GenerativeModelSession: Sendable { let sessionManager: SessionManager - let instructions: SystemInstructions? + let instructions: FirebaseAI.Instructions? // The maximum number of automatic back-and-forth turns the session will perform to resolve // function calls. @@ -74,7 +74,7 @@ /// or instances conforming to ``ToolRepresentable`` for automatic function calling. /// - instructions: System instructions that direct the model's behavior. init(model: any LanguageModel, tools: [any ToolRepresentable]? = nil, - instructions: SystemInstructions? = nil) { + instructions: FirebaseAI.Instructions? = nil) { sessionManager = SessionManager(model: model, tools: tools) self.instructions = instructions } @@ -438,7 +438,7 @@ } } - func getOrStartSession(instructions: SystemInstructions?) throws -> any _ModelSession { + func getOrStartSession(instructions: FirebaseAI.Instructions?) throws -> any _ModelSession { try _isResponding.withLock { isResponding in if let currentSession = _activeSession { return currentSession diff --git a/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift b/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift index 21944fe64d0..baa3c280a4a 100644 --- a/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift +++ b/FirebaseAI/Sources/Protocols/Internal/ConvertibleToGeneratedContent.swift @@ -18,7 +18,7 @@ #endif // canImport(FoundationModels) extension FirebaseAI { - protocol ConvertibleToGeneratedContent: SystemInstructionsRepresentable { + protocol ConvertibleToGeneratedContent: FirebaseAI.InstructionsRepresentable { var firebaseGeneratedContent: FirebaseAI.GeneratedContent { get } } } diff --git a/FirebaseAI/Sources/Protocols/Public/InstructionsRepresentable.swift b/FirebaseAI/Sources/Protocols/Public/InstructionsRepresentable.swift new file mode 100644 index 00000000000..913707b4f20 --- /dev/null +++ b/FirebaseAI/Sources/Protocols/Public/InstructionsRepresentable.swift @@ -0,0 +1,55 @@ +// Copyright 2026 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. + +#if canImport(FoundationModels) + import FoundationModels +#endif // canImport(FoundationModels) + +public extension FirebaseAI { + protocol InstructionsRepresentable { + @FirebaseAI.InstructionsBuilder + var firebaseInstructionsRepresentation: FirebaseAI.Instructions { get } + } +} + +extension String: FirebaseAI.InstructionsRepresentable { + public var firebaseInstructionsRepresentation: FirebaseAI.Instructions { + return FirebaseAI.Instructions(parts: [.text(self)]) + } +} + +extension Array: FirebaseAI.InstructionsRepresentable + where Element: FirebaseAI.InstructionsRepresentable { + public var firebaseInstructionsRepresentation: FirebaseAI.Instructions { + let allParts = flatMap { $0.firebaseInstructionsRepresentation.parts } + return FirebaseAI.Instructions(parts: allParts) + } +} + +extension FirebaseAI.GeneratedContent: FirebaseAI.InstructionsRepresentable { + public var firebaseInstructionsRepresentation: FirebaseAI.Instructions { + return FirebaseAI.Instructions(parts: [.structure(self)]) + } +} + +#if canImport(FoundationModels) + @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + extension FoundationModels.GeneratedContent: FirebaseAI.InstructionsRepresentable { + public var firebaseInstructionsRepresentation: FirebaseAI.Instructions { + FirebaseAI.Instructions(parts: [.structure(firebaseGeneratedContent)]) + } + } +#endif // canImport(FoundationModels) diff --git a/FirebaseAI/Sources/Protocols/Public/LanguageModel.swift b/FirebaseAI/Sources/Protocols/Public/LanguageModel.swift index 44109ee7c2f..9807b2ee24c 100644 --- a/FirebaseAI/Sources/Protocols/Public/LanguageModel.swift +++ b/FirebaseAI/Sources/Protocols/Public/LanguageModel.swift @@ -23,8 +23,8 @@ /// Returns a new session for this model. /// /// > Important: This method is for **internal use only** and may change at any time. - func _startSession(tools: [any ToolRepresentable]?, instructions: SystemInstructions?) throws - -> any _ModelSession + func _startSession(tools: [any ToolRepresentable]?, + instructions: FirebaseAI.Instructions?) throws -> any _ModelSession } // Default implementation to allow a `LanguageModel` to conform to `LanguageModelProvider`. diff --git a/FirebaseAI/Sources/Protocols/Public/SystemInstructionsRepresentable.swift b/FirebaseAI/Sources/Protocols/Public/SystemInstructionsRepresentable.swift deleted file mode 100644 index c51763508d5..00000000000 --- a/FirebaseAI/Sources/Protocols/Public/SystemInstructionsRepresentable.swift +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2026 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. - -#if canImport(FoundationModels) - import FoundationModels -#endif // canImport(FoundationModels) - -public protocol SystemInstructionsRepresentable { - @SystemInstructionsBuilder - var systemInstructionsRepresentation: SystemInstructions { get } -} - -extension String: SystemInstructionsRepresentable { - public var systemInstructionsRepresentation: SystemInstructions { - return SystemInstructions(parts: [.text(self)]) - } -} - -extension Array: SystemInstructionsRepresentable where Element: SystemInstructionsRepresentable { - public var systemInstructionsRepresentation: SystemInstructions { - let allParts = flatMap { $0.systemInstructionsRepresentation.parts } - return SystemInstructions(parts: allParts) - } -} - -extension FirebaseAI.GeneratedContent: SystemInstructionsRepresentable { - public var systemInstructionsRepresentation: SystemInstructions { - return SystemInstructions(parts: [.structure(self)]) - } -} - -#if canImport(FoundationModels) - @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - extension FoundationModels.GeneratedContent: SystemInstructionsRepresentable { - public var systemInstructionsRepresentation: SystemInstructions { - SystemInstructions(parts: [.structure(firebaseGeneratedContent)]) - } - } -#endif // canImport(FoundationModels) diff --git a/FirebaseAI/Sources/Types/Internal/HybridModelSession.swift b/FirebaseAI/Sources/Types/Internal/HybridModelSession.swift index bbcdd626be5..f00b74405b8 100644 --- a/FirebaseAI/Sources/Types/Internal/HybridModelSession.swift +++ b/FirebaseAI/Sources/Types/Internal/HybridModelSession.swift @@ -19,13 +19,13 @@ private let primaryModel: any LanguageModel private let secondaryModel: any LanguageModel private let tools: [any ToolRepresentable]? - private let instructions: SystemInstructions? + private let instructions: FirebaseAI.Instructions? typealias SessionState = (primary: (any _ModelSession)?, secondary: (any _ModelSession)?) private let lock: UnfairLock init(primaryModel: any LanguageModel, secondaryModel: any LanguageModel, - tools: [any ToolRepresentable]?, instructions: SystemInstructions?) { + tools: [any ToolRepresentable]?, instructions: FirebaseAI.Instructions?) { self.primaryModel = primaryModel self.secondaryModel = secondaryModel self.tools = tools diff --git a/FirebaseAI/Sources/Types/Public/GeminiModel.swift b/FirebaseAI/Sources/Types/Public/GeminiModel.swift index 5ff20bc9e3c..5d478c47fcf 100644 --- a/FirebaseAI/Sources/Types/Public/GeminiModel.swift +++ b/FirebaseAI/Sources/Types/Public/GeminiModel.swift @@ -62,7 +62,7 @@ import Foundation /// /// > Important: This method is for **internal use only** and may change at any time. public func _startSession(tools: [any ToolRepresentable]?, - instructions: SystemInstructions?) throws -> any _ModelSession { + instructions: FirebaseAI.Instructions?) throws -> any _ModelSession { let model = GenerativeModel( modelName: modelName, modelResourceName: modelResourceName, diff --git a/FirebaseAI/Sources/Types/Public/HybridModel.swift b/FirebaseAI/Sources/Types/Public/HybridModel.swift index 9098f5b77ce..10bb5276f18 100644 --- a/FirebaseAI/Sources/Types/Public/HybridModel.swift +++ b/FirebaseAI/Sources/Types/Public/HybridModel.swift @@ -45,7 +45,7 @@ /// /// > Important: This method is for **internal use only** and may change at any time. public func _startSession(tools: [any ToolRepresentable]?, - instructions: SystemInstructions?) throws -> any _ModelSession { + instructions: FirebaseAI.Instructions?) throws -> any _ModelSession { return HybridModelSession( primaryModel: primary, secondaryModel: secondary, diff --git a/FirebaseAI/Sources/Types/Public/Instructions.swift b/FirebaseAI/Sources/Types/Public/Instructions.swift new file mode 100644 index 00000000000..49ee82e9236 --- /dev/null +++ b/FirebaseAI/Sources/Types/Public/Instructions.swift @@ -0,0 +1,78 @@ +// Copyright 2026 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. + +#if canImport(FoundationModels) + import FoundationModels +#endif // canImport(FoundationModels) + +public extension FirebaseAI { + struct Instructions: Sendable { + enum InstructionsPart { + case text(String) + case structure(FirebaseAI.GeneratedContent) + } + + let parts: [InstructionsPart] + + init(parts: [InstructionsPart]) { + self.parts = parts + } + + public init(_ content: some FirebaseAI.InstructionsRepresentable) { + parts = content.firebaseInstructionsRepresentation.parts + } + + public init(@FirebaseAI.InstructionsBuilder _ content: () throws -> Instructions) rethrows { + self = try content() + } + + #if canImport(FoundationModels) + @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + func toFoundationModels() -> FoundationModels.Instructions { + return FoundationModels.Instructions { + for part in parts { + switch part { + case let .text(text): + text + case let .structure(structure): + structure + } + } + } + } + #endif // canImport(FoundationModels) + + func toModelContent() -> ModelContent { + return ModelContent( + role: "system", + parts: parts.map { + switch $0 { + case let .text(string): + return TextPart(string) + case let .structure(generatedContent): + return TextPart(generatedContent.jsonString) + } + } + ) + } + } +} + +extension FirebaseAI.Instructions: FirebaseAI.InstructionsRepresentable { + public var firebaseInstructionsRepresentation: FirebaseAI.Instructions { + self + } +} diff --git a/FirebaseAI/Sources/Types/Public/InstructionsBuilder.swift b/FirebaseAI/Sources/Types/Public/InstructionsBuilder.swift new file mode 100644 index 00000000000..0933a946b09 --- /dev/null +++ b/FirebaseAI/Sources/Types/Public/InstructionsBuilder.swift @@ -0,0 +1,76 @@ +// Copyright 2026 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. + +#if canImport(FoundationModels) + import FoundationModels +#endif // canImport(FoundationModels) + +public extension FirebaseAI { + @resultBuilder + struct InstructionsBuilder { + public static func buildBlock(_ components: repeat each I) + -> FirebaseAI.Instructions where repeat each I: FirebaseAI.InstructionsRepresentable { + var allComponents: [FirebaseAI.Instructions.InstructionsPart] = [] + repeat allComponents + .append(contentsOf: (each components).firebaseInstructionsRepresentation.parts) + return FirebaseAI.Instructions(parts: allComponents) + } + + public static func buildArray(_ instructions: [some FirebaseAI.InstructionsRepresentable]) + -> FirebaseAI.Instructions { + let allComponents = instructions.flatMap { $0.firebaseInstructionsRepresentation.parts } + return FirebaseAI.Instructions(parts: allComponents) + } + + public static func buildEither(first component: some FirebaseAI.InstructionsRepresentable) + -> FirebaseAI.Instructions { + component.firebaseInstructionsRepresentation + } + + public static func buildEither(second component: some FirebaseAI.InstructionsRepresentable) + -> FirebaseAI.Instructions { + component.firebaseInstructionsRepresentation + } + + public static func buildOptional(_ instructions: FirebaseAI.Instructions?) + -> FirebaseAI.Instructions { + instructions ?? FirebaseAI.Instructions(parts: []) + } + + public static func buildLimitedAvailability(_ instructions: some FirebaseAI + .InstructionsRepresentable) -> FirebaseAI.Instructions { + instructions.firebaseInstructionsRepresentation + } + + public static func buildExpression(_ expression: I) + -> I where I: FirebaseAI.InstructionsRepresentable { + expression + } + + public static func buildExpression(_ expression: FirebaseAI.Instructions) + -> FirebaseAI.Instructions { + expression + } + + #if canImport(FoundationModels) + @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + public static func buildExpression(_ expression: FoundationModels + .ConvertibleToGeneratedContent) -> FirebaseAI.Instructions { + FirebaseAI.Instructions(expression.generatedContent) + } + #endif // canImport(FoundationModels) + } +} diff --git a/FirebaseAI/Sources/Types/Public/SystemInstructions.swift b/FirebaseAI/Sources/Types/Public/SystemInstructions.swift deleted file mode 100644 index d0756cf4b44..00000000000 --- a/FirebaseAI/Sources/Types/Public/SystemInstructions.swift +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2026 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. - -#if canImport(FoundationModels) - import FoundationModels -#endif // canImport(FoundationModels) - -public struct SystemInstructions: Sendable { - enum InstructionsPart { - case text(String) - case structure(FirebaseAI.GeneratedContent) - } - - let parts: [InstructionsPart] - - init(parts: [InstructionsPart]) { - self.parts = parts - } - - public init(_ content: some SystemInstructionsRepresentable) { - parts = content.systemInstructionsRepresentation.parts - } - - public init(@SystemInstructionsBuilder _ content: () throws -> SystemInstructions) rethrows { - self = try content() - } - - #if canImport(FoundationModels) - @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - func toFoundationModels() -> FoundationModels.Instructions { - return FoundationModels.Instructions { - for part in parts { - switch part { - case let .text(text): - text - case let .structure(structure): - structure - } - } - } - } - #endif // canImport(FoundationModels) - - func toModelContent() -> ModelContent { - return ModelContent( - role: "system", - parts: parts.map { - switch $0 { - case let .text(string): - return TextPart(string) - case let .structure(generatedContent): - return TextPart(generatedContent.jsonString) - } - } - ) - } -} - -extension SystemInstructions: SystemInstructionsRepresentable { - public var systemInstructionsRepresentation: SystemInstructions { - self - } -} diff --git a/FirebaseAI/Sources/Types/Public/SystemInstructionsBuilder.swift b/FirebaseAI/Sources/Types/Public/SystemInstructionsBuilder.swift deleted file mode 100644 index fa593b01cbd..00000000000 --- a/FirebaseAI/Sources/Types/Public/SystemInstructionsBuilder.swift +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2026 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. - -#if canImport(FoundationModels) - import FoundationModels -#endif // canImport(FoundationModels) - -@resultBuilder -public struct SystemInstructionsBuilder { - public static func buildBlock(_ components: repeat each I) -> SystemInstructions - where repeat each I: SystemInstructionsRepresentable { - var allComponents: [SystemInstructions.InstructionsPart] = [] - repeat allComponents - .append(contentsOf: (each components).systemInstructionsRepresentation.parts) - return SystemInstructions(parts: allComponents) - } - - public static func buildArray(_ instructions: [some SystemInstructionsRepresentable]) - -> SystemInstructions { - let allComponents = instructions.flatMap { $0.systemInstructionsRepresentation.parts } - return SystemInstructions(parts: allComponents) - } - - public static func buildEither(first component: some SystemInstructionsRepresentable) - -> SystemInstructions { - component.systemInstructionsRepresentation - } - - public static func buildEither(second component: some SystemInstructionsRepresentable) - -> SystemInstructions { - component.systemInstructionsRepresentation - } - - public static func buildOptional(_ instructions: SystemInstructions?) -> SystemInstructions { - instructions ?? SystemInstructions(parts: []) - } - - public static func buildLimitedAvailability(_ instructions: some SystemInstructionsRepresentable) - -> SystemInstructions { - instructions.systemInstructionsRepresentation - } - - public static func buildExpression(_ expression: I) -> I - where I: SystemInstructionsRepresentable { - expression - } - - public static func buildExpression(_ expression: SystemInstructions) -> SystemInstructions { - expression - } -} diff --git a/FirebaseAI/Tests/Unit/HybridModelTests.swift b/FirebaseAI/Tests/Unit/HybridModelTests.swift index 520c1b44612..43499605488 100644 --- a/FirebaseAI/Tests/Unit/HybridModelTests.swift +++ b/FirebaseAI/Tests/Unit/HybridModelTests.swift @@ -23,7 +23,7 @@ let startSessionHandler: @Sendable () throws -> any _ModelSession func _startSession(tools: [any ToolRepresentable]?, - instructions: SystemInstructions?) throws -> any _ModelSession { + instructions: FirebaseAI.Instructions?) throws -> any _ModelSession { return try startSessionHandler() } } From 37ecadfc3bf9be828c883c54de5f2c623933767c Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 28 May 2026 12:36:48 -0400 Subject: [PATCH 4/4] Fix formatting --- FirebaseAI/Sources/FirebaseAI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAI/Sources/FirebaseAI.swift b/FirebaseAI/Sources/FirebaseAI.swift index 407e38d8633..7c12da03bc3 100644 --- a/FirebaseAI/Sources/FirebaseAI.swift +++ b/FirebaseAI/Sources/FirebaseAI.swift @@ -162,7 +162,7 @@ public final class FirebaseAI: Sendable { tools: [any ToolRepresentable]? = nil, @FirebaseAI.InstructionsBuilder instructions: () throws -> FirebaseAI.Instructions) rethrows - -> GenerativeModelSession { + -> GenerativeModelSession { let tools = tools?.map { $0.toolRepresentation } return try generativeModelSession(model: model, tools: tools as [any ToolRepresentable]?,