Skip to content

Commit d83dc64

Browse files
committed
Move FirebaseAIWrapper into subdir and add API tests
1 parent 9c343a6 commit d83dc64

File tree

6 files changed

+297
-10
lines changed

6 files changed

+297
-10
lines changed

.github/workflows/firebaseai.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ permissions:
2727

2828
jobs:
2929
spm:
30+
strategy:
31+
matrix:
32+
target: [FirebaseAILogicUnit, FirebaseAIUnit]
3033
uses: ./.github/workflows/common.yml
3134
with:
32-
target: FirebaseAIUnit
35+
target: ${{ matrix.target }}
3336
setup_command: scripts/update_vertexai_responses.sh
3437

3538
testapp-integration:
@@ -77,9 +80,12 @@ jobs:
7780
retention-days: 2
7881

7982
pod_lib_lint:
83+
strategy:
84+
matrix:
85+
product: [FirebaseAILogic, FirebaseAI]
8086
uses: ./.github/workflows/common_cocoapods.yml
8187
with:
82-
product: FirebaseAI
88+
product: ${{ matrix.product }}
8389
supports_swift6: true
8490
setup_command: scripts/update_vertexai_responses.sh
8591

FirebaseAI.podspec

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Build AI-powered apps and features with the Gemini API using the Firebase AI SDK
3232
s.prefix_header_file = false
3333

3434
s.source_files = [
35-
'FirebaseAI/Wrapper/**/*.swift',
35+
'FirebaseAI/Wrapper/Sources/**/*.swift',
3636
]
3737

3838
s.swift_version = '5.9'
@@ -45,4 +45,17 @@ Build AI-powered apps and features with the Gemini API using the Firebase AI SDK
4545

4646
s.dependency 'FirebaseAILogic', '12.4.0'
4747
s.dependency 'FirebaseCore', '~> 12.4.0'
48+
49+
s.test_spec 'unit' do |unit_tests|
50+
unit_tests_dir = 'FirebaseAI/Wrapper/Tests/'
51+
unit_tests.scheme = { :code_coverage => true }
52+
unit_tests.platforms = {
53+
:ios => ios_deployment_target,
54+
:osx => osx_deployment_target,
55+
:tvos => tvos_deployment_target
56+
}
57+
unit_tests.source_files = [
58+
unit_tests_dir + '**/*.swift',
59+
]
60+
end
4861
end
File renamed without changes.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright 2023 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+
import FirebaseAI
16+
import FirebaseCore
17+
import XCTest
18+
#if canImport(AppKit)
19+
import AppKit // For NSImage extensions.
20+
#elseif canImport(UIKit)
21+
import UIKit // For UIImage extensions.
22+
#endif
23+
24+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
25+
final class APITests: XCTestCase {
26+
func codeSamples() async throws {
27+
let app = FirebaseApp.app()
28+
let config = GenerationConfig(temperature: 0.2,
29+
topP: 0.1,
30+
topK: 16,
31+
candidateCount: 4,
32+
maxOutputTokens: 256,
33+
stopSequences: ["..."],
34+
responseMIMEType: "text/plain")
35+
let filters = [SafetySetting(harmCategory: .dangerousContent, threshold: .blockOnlyHigh)]
36+
let systemInstruction = ModelContent(
37+
role: "system",
38+
parts: TextPart("Talk like a pirate.")
39+
)
40+
41+
let requestOptions = RequestOptions()
42+
let _ = RequestOptions(timeout: 30.0)
43+
44+
// Instantiate Firebase AI SDK - Default App
45+
let firebaseAI = FirebaseAI.firebaseAI()
46+
let _ = FirebaseAI.firebaseAI(backend: .googleAI())
47+
let _ = FirebaseAI.firebaseAI(backend: .vertexAI())
48+
let _ = FirebaseAI.firebaseAI(backend: .vertexAI(location: "my-location"))
49+
50+
// Instantiate Firebase AI SDK - Custom App
51+
let _ = FirebaseAI.firebaseAI(app: app!)
52+
let _ = FirebaseAI.firebaseAI(app: app!, backend: .googleAI())
53+
let _ = FirebaseAI.firebaseAI(app: app!, backend: .vertexAI())
54+
let _ = FirebaseAI.firebaseAI(app: app!, backend: .vertexAI(location: "my-location"))
55+
56+
// Permutations without optional arguments.
57+
58+
let _ = firebaseAI.generativeModel(modelName: "gemini-2.0-flash")
59+
60+
let _ = firebaseAI.generativeModel(
61+
modelName: "gemini-2.0-flash",
62+
safetySettings: filters
63+
)
64+
65+
let _ = firebaseAI.generativeModel(
66+
modelName: "gemini-2.0-flash",
67+
generationConfig: config
68+
)
69+
70+
let _ = firebaseAI.generativeModel(
71+
modelName: "gemini-2.0-flash",
72+
systemInstruction: systemInstruction
73+
)
74+
75+
// All arguments passed.
76+
let model = firebaseAI.generativeModel(
77+
modelName: "gemini-2.0-flash",
78+
generationConfig: config, // Optional
79+
safetySettings: filters, // Optional
80+
systemInstruction: systemInstruction, // Optional
81+
requestOptions: requestOptions // Optional
82+
)
83+
84+
// Full Typed Usage
85+
let pngData = Data() // ....
86+
let contents = [ModelContent(
87+
role: "user",
88+
parts: [
89+
TextPart("Is it a cat?"),
90+
InlineDataPart(data: pngData, mimeType: "image/png"),
91+
]
92+
)]
93+
94+
do {
95+
let response = try await model.generateContent(contents)
96+
print(response.text ?? "Couldn't get text... check status")
97+
} catch {
98+
print("Error generating content: \(error)")
99+
}
100+
101+
// Content input combinations.
102+
let _ = try await model.generateContent("Constant String")
103+
let str = "String Variable"
104+
let _ = try await model.generateContent(str)
105+
let _ = try await model.generateContent([str])
106+
let _ = try await model.generateContent(str, "abc", "def")
107+
let _ = try await model.generateContent(
108+
str,
109+
FileDataPart(uri: "gs://test-bucket/image.jpg", mimeType: "image/jpeg")
110+
)
111+
#if canImport(UIKit)
112+
_ = try await model.generateContent(UIImage())
113+
_ = try await model.generateContent([UIImage()])
114+
_ = try await model.generateContent([str, UIImage(), TextPart(str)])
115+
_ = try await model.generateContent(str, UIImage(), "def", UIImage())
116+
_ = try await model.generateContent([str, UIImage(), "def", UIImage()])
117+
_ = try await model.generateContent([ModelContent(parts: "def", UIImage()),
118+
ModelContent(parts: "def", UIImage())])
119+
#elseif canImport(AppKit)
120+
_ = try await model.generateContent(NSImage())
121+
_ = try await model.generateContent([NSImage()])
122+
_ = try await model.generateContent(str, NSImage(), "def", NSImage())
123+
_ = try await model.generateContent([str, NSImage(), "def", NSImage()])
124+
#endif
125+
126+
// PartsRepresentable combinations.
127+
let _ = ModelContent(parts: [TextPart(str)])
128+
let _ = ModelContent(role: "model", parts: [TextPart(str)])
129+
let _ = ModelContent(parts: "Constant String")
130+
let _ = ModelContent(parts: str)
131+
let _ = ModelContent(parts: [str])
132+
let _ = ModelContent(parts: [str, InlineDataPart(data: Data(), mimeType: "foo")])
133+
#if canImport(UIKit)
134+
_ = ModelContent(role: "user", parts: UIImage())
135+
_ = ModelContent(role: "user", parts: [UIImage()])
136+
_ = ModelContent(parts: [str, UIImage()])
137+
// Note: without explicitly specifying`: [any PartsRepresentable]` this will fail to compile
138+
// below with "Cannot convert value of type `[Any]` to expected type `[any Part]`.
139+
let representable2: [any PartsRepresentable] = [str, UIImage()]
140+
_ = ModelContent(parts: representable2)
141+
_ = ModelContent(parts: [str, UIImage(), TextPart(str)])
142+
#elseif canImport(AppKit)
143+
_ = ModelContent(role: "user", parts: NSImage())
144+
_ = ModelContent(role: "user", parts: [NSImage()])
145+
_ = ModelContent(parts: [str, NSImage()])
146+
// Note: without explicitly specifying`: [any PartsRepresentable]` this will fail to compile
147+
// below with "Cannot convert value of type `[Any]` to expected type `[any Part]`.
148+
let representable2: [any PartsRepresentable] = [str, NSImage()]
149+
_ = ModelContent(parts: representable2)
150+
_ = ModelContent(parts: [str, NSImage(), TextPart(str)])
151+
#endif
152+
153+
// countTokens API
154+
let _: CountTokensResponse = try await model.countTokens("What color is the Sky?")
155+
#if canImport(UIKit)
156+
let _: CountTokensResponse = try await model.countTokens("What color is the Sky?",
157+
UIImage())
158+
let _: CountTokensResponse = try await model.countTokens([
159+
ModelContent(parts: "What color is the Sky?", UIImage()),
160+
ModelContent(parts: UIImage(), "What color is the Sky?", UIImage()),
161+
])
162+
#endif
163+
164+
// Chat
165+
_ = model.startChat()
166+
_ = model.startChat(history: [ModelContent(parts: "abc")])
167+
}
168+
169+
// Public API tests for GenerateContentResponse.
170+
func generateContentResponseAPI() {
171+
let response = GenerateContentResponse(candidates: [])
172+
173+
let _: [Candidate] = response.candidates
174+
let _: PromptFeedback? = response.promptFeedback
175+
176+
// Usage Metadata
177+
guard let usageMetadata = response.usageMetadata else { fatalError() }
178+
let _: Int = usageMetadata.promptTokenCount
179+
let _: Int = usageMetadata.candidatesTokenCount
180+
let _: Int = usageMetadata.totalTokenCount
181+
182+
// Computed Properties
183+
let _: String? = response.text
184+
let _: [FunctionCallPart] = response.functionCalls
185+
}
186+
}

Package.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,15 +196,10 @@ let package = Package(
196196
],
197197
path: "FirebaseAI/Sources"
198198
),
199-
.target(
200-
name: "FirebaseAI",
201-
dependencies: ["FirebaseAILogic"],
202-
path: "FirebaseAI/Wrapper"
203-
),
204199
.testTarget(
205-
name: "FirebaseAIUnit",
200+
name: "FirebaseAILogicUnit",
206201
dependencies: [
207-
"FirebaseAI",
202+
"FirebaseAILogic",
208203
"FirebaseStorage",
209204
],
210205
path: "FirebaseAI/Tests/Unit",
@@ -216,6 +211,16 @@ let package = Package(
216211
.headerSearchPath("../../../"),
217212
]
218213
),
214+
.target(
215+
name: "FirebaseAI",
216+
dependencies: ["FirebaseAILogic"],
217+
path: "FirebaseAI/Wrapper/Sources"
218+
),
219+
.testTarget(
220+
name: "FirebaseAIUnit",
221+
dependencies: ["FirebaseAI"],
222+
path: "FirebaseAI/Wrapper/Tests",
223+
),
219224

220225
// MARK: - Firebase Core
221226

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1200"
4+
version = "1.3">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForTesting = "YES"
11+
buildForRunning = "YES"
12+
buildForProfiling = "NO"
13+
buildForArchiving = "NO"
14+
buildForAnalyzing = "YES">
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "FirebaseAILogicUnit"
18+
BuildableName = "FirebaseAILogicUnit"
19+
BlueprintName = "FirebaseAILogicUnit"
20+
ReferencedContainer = "container:">
21+
</BuildableReference>
22+
</BuildActionEntry>
23+
</BuildActionEntries>
24+
</BuildAction>
25+
<TestAction
26+
buildConfiguration = "Debug"
27+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29+
shouldUseLaunchSchemeArgsEnv = "YES">
30+
<Testables>
31+
<TestableReference
32+
skipped = "NO">
33+
<BuildableReference
34+
BuildableIdentifier = "primary"
35+
BlueprintIdentifier = "FirebaseAILogicUnit"
36+
BuildableName = "FirebaseAILogicUnit"
37+
BlueprintName = "FirebaseAILogicUnit"
38+
ReferencedContainer = "container:">
39+
</BuildableReference>
40+
</TestableReference>
41+
</Testables>
42+
</TestAction>
43+
<LaunchAction
44+
buildConfiguration = "Debug"
45+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
46+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
47+
launchStyle = "0"
48+
useCustomWorkingDirectory = "NO"
49+
ignoresPersistentStateOnLaunch = "NO"
50+
debugDocumentVersioning = "YES"
51+
debugServiceExtension = "internal"
52+
allowLocationSimulation = "YES">
53+
</LaunchAction>
54+
<ProfileAction
55+
buildConfiguration = "Release"
56+
shouldUseLaunchSchemeArgsEnv = "YES"
57+
savedToolIdentifier = ""
58+
useCustomWorkingDirectory = "NO"
59+
debugDocumentVersioning = "YES">
60+
<MacroExpansion>
61+
<BuildableReference
62+
BuildableIdentifier = "primary"
63+
BlueprintIdentifier = "FirebaseAILogicUnit"
64+
BuildableName = "FirebaseAILogicUnit"
65+
BlueprintName = "FirebaseAILogicUnit"
66+
ReferencedContainer = "container:">
67+
</BuildableReference>
68+
</MacroExpansion>
69+
</ProfileAction>
70+
<AnalyzeAction
71+
buildConfiguration = "Debug">
72+
</AnalyzeAction>
73+
<ArchiveAction
74+
buildConfiguration = "Release"
75+
revealArchiveInOrganizer = "YES">
76+
</ArchiveAction>
77+
</Scheme>

0 commit comments

Comments
 (0)