Skip to content

Commit 2b68801

Browse files
paulb777google-labs-jules[bot]gemini-code-assist[bot]andrewheard
authored
Add inlineDataParts accessor for GenerateContentResponse (#14755)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Andrew Heard <[email protected]>
1 parent ba460b0 commit 2b68801

File tree

3 files changed

+125
-0
lines changed

3 files changed

+125
-0
lines changed

FirebaseVertexAI/Sources/GenerateContentResponse.swift

+12
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ public struct GenerateContentResponse: Sendable {
8888
}
8989
}
9090

91+
/// Returns inline data parts found in any `Part`s of the first candidate of the response, if any.
92+
public var inlineDataParts: [InlineDataPart] {
93+
guard let candidate = candidates.first else {
94+
VertexLog.error(code: .generateContentResponseNoCandidates, """
95+
Could not get inline data parts because the response has no candidates. The accessor only \
96+
checks the first candidate.
97+
""")
98+
return []
99+
}
100+
return candidate.content.parts.compactMap { $0 as? InlineDataPart }
101+
}
102+
91103
/// Initializer for SwiftUI previews or tests.
92104
public init(candidates: [Candidate], promptFeedback: PromptFeedback? = nil,
93105
usageMetadata: UsageMetadata? = nil) {

FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

+4
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ struct GenerateContentIntegrationTests {
149149
let candidate = try #require(response.candidates.first)
150150
let inlineDataPart = try #require(candidate.content.parts
151151
.first { $0 is InlineDataPart } as? InlineDataPart)
152+
let inlineDataPartsViaAccessor = response.inlineDataParts
153+
#expect(inlineDataPartsViaAccessor.count == 1)
154+
let inlineDataPartViaAccessor = try #require(inlineDataPartsViaAccessor.first)
155+
#expect(inlineDataPart == inlineDataPartViaAccessor)
152156
#expect(inlineDataPart.mimeType == "image/png")
153157
#expect(inlineDataPart.data.count > 0)
154158
#if canImport(UIKit)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
import FirebaseVertexAI
16+
import XCTest
17+
18+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
19+
final class GenerateContentResponseTests: XCTestCase {
20+
// MARK: - GenerateContentResponse Computed Properties
21+
22+
func testGenerateContentResponse_inlineDataParts_success() throws {
23+
let imageData = Data("sample image data".utf8)
24+
let inlineDataPart = InlineDataPart(data: imageData, mimeType: "image/png")
25+
let textPart = TextPart("This is the text part.")
26+
let modelContent = ModelContent(parts: [textPart, inlineDataPart])
27+
let candidate = Candidate(
28+
content: modelContent,
29+
safetyRatings: [],
30+
finishReason: nil,
31+
citationMetadata: nil
32+
)
33+
let response = GenerateContentResponse(candidates: [candidate])
34+
35+
let inlineParts = response.inlineDataParts
36+
37+
XCTAssertFalse(inlineParts.isEmpty, "inlineDataParts should not be empty.")
38+
XCTAssertEqual(inlineParts.count, 1, "There should be exactly one InlineDataPart.")
39+
let firstInlinePart = try XCTUnwrap(inlineParts.first, "Could not get the first inline part.")
40+
XCTAssertEqual(firstInlinePart.mimeType, inlineDataPart.mimeType, "MimeType should match.")
41+
XCTAssertEqual(firstInlinePart.data, imageData)
42+
XCTAssertEqual(response.text, textPart.text)
43+
XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty.")
44+
}
45+
46+
func testGenerateContentResponse_inlineDataParts_multipleInlineDataParts_success() throws {
47+
let imageData1 = Data("sample image data 1".utf8)
48+
let inlineDataPart1 = InlineDataPart(data: imageData1, mimeType: "image/png")
49+
let imageData2 = Data("sample image data 2".utf8)
50+
let inlineDataPart2 = InlineDataPart(data: imageData2, mimeType: "image/jpeg")
51+
let modelContent = ModelContent(parts: [inlineDataPart1, inlineDataPart2])
52+
let candidate = Candidate(
53+
content: modelContent,
54+
safetyRatings: [],
55+
finishReason: nil,
56+
citationMetadata: nil
57+
)
58+
let response = GenerateContentResponse(candidates: [candidate])
59+
60+
let inlineParts = response.inlineDataParts
61+
62+
XCTAssertFalse(inlineParts.isEmpty, "inlineDataParts should not be empty.")
63+
XCTAssertEqual(inlineParts.count, 2, "There should be exactly two InlineDataParts.")
64+
let firstInlinePart = try XCTUnwrap(inlineParts.first, "Could not get the first inline part.")
65+
XCTAssertEqual(firstInlinePart.mimeType, inlineDataPart1.mimeType, "MimeType should match.")
66+
XCTAssertEqual(firstInlinePart.data, imageData1)
67+
let secondInlinePart = try XCTUnwrap(inlineParts.last, "Could not get the second inline part.")
68+
XCTAssertEqual(secondInlinePart.mimeType, inlineDataPart2.mimeType, "MimeType should match.")
69+
XCTAssertEqual(secondInlinePart.data, imageData2)
70+
XCTAssertNil(response.text)
71+
XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty.")
72+
}
73+
74+
func testGenerateContentResponse_inlineDataParts_noInlineData() throws {
75+
let textPart = TextPart("This is the text part.")
76+
let functionCallPart = FunctionCallPart(name: "testFunc", args: [:])
77+
let modelContent = ModelContent(parts: [textPart, functionCallPart])
78+
let candidate = Candidate(
79+
content: modelContent,
80+
safetyRatings: [],
81+
finishReason: nil,
82+
citationMetadata: nil
83+
)
84+
let response = GenerateContentResponse(candidates: [candidate])
85+
86+
let inlineParts = response.inlineDataParts
87+
88+
XCTAssertTrue(inlineParts.isEmpty, "inlineDataParts should be empty.")
89+
XCTAssertEqual(response.text, "This is the text part.")
90+
XCTAssertEqual(response.functionCalls.count, 1)
91+
XCTAssertEqual(response.functionCalls.first?.name, "testFunc")
92+
}
93+
94+
func testGenerateContentResponse_inlineDataParts_noCandidates() throws {
95+
let response = GenerateContentResponse(candidates: [])
96+
97+
let inlineParts = response.inlineDataParts
98+
99+
XCTAssertTrue(
100+
inlineParts.isEmpty,
101+
"inlineDataParts should be empty when there are no candidates."
102+
)
103+
XCTAssertNil(response.text, "Text should be nil when there are no candidates.")
104+
XCTAssertTrue(
105+
response.functionCalls.isEmpty,
106+
"functionCalls should be empty when there are no candidates."
107+
)
108+
}
109+
}

0 commit comments

Comments
 (0)