Skip to content

Commit 4ecc372

Browse files
feat: Add inlineDataParts property to GenerateContentResponse
Adds a new computed property `inlineDataParts` to `GenerateContentResponse` to provide easier access to inline data parts (like images) within the model's response. This property iterates through the parts of the first candidate and filters for `InlineDataPart` instances. Error handling is included for cases with no candidates or no inline data parts. Unit tests have been added in `GenerativeModelTests.swift` to validate the functionality of the new property, covering scenarios with and without inline data, and responses with no candidates.
1 parent 912d8c0 commit 4ecc372

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

FirebaseVertexAI/Sources/GenerateContentResponse.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,32 @@ 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(
95+
code: .generateContentResponseNoCandidates,
96+
"Could not get inline data parts from a response that had no candidates."
97+
)
98+
return []
99+
}
100+
let inlineData: [InlineDataPart] = candidate.content.parts.compactMap { part in
101+
switch part {
102+
case let inlineDataPart as InlineDataPart:
103+
return inlineDataPart
104+
default:
105+
return nil
106+
}
107+
}
108+
if inlineData.isEmpty {
109+
VertexLog.warning(
110+
code: .generateContentResponseNoInlineData,
111+
"Could not find any inline data parts in the first candidate."
112+
)
113+
}
114+
return inlineData
115+
}
116+
91117
/// Initializer for SwiftUI previews or tests.
92118
public init(candidates: [Candidate], promptFeedback: PromptFeedback? = nil,
93119
usageMetadata: UsageMetadata? = nil) {

FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,6 +1538,92 @@ final class GenerativeModelTests: XCTestCase {
15381538
XCTAssertEqual(response.totalTokens, 6)
15391539
}
15401540

1541+
// MARK: - GenerateContentResponse Computed Properties
1542+
1543+
func testGenerateContentResponse_inlineDataParts_success() throws {
1544+
// 1. Create mock parts
1545+
let imageData = Data("sample image data".utf8) // Placeholder data
1546+
let inlineDataPart = InlineDataPart(mimeType: "image/png", data: imageData)
1547+
let textPart = TextPart("This is the text part.")
1548+
1549+
// 2. Create ModelContent
1550+
let modelContent = ModelContent(parts: [textPart, inlineDataPart]) // Mixed parts
1551+
1552+
// 3. Create Candidate
1553+
let candidate = Candidate(
1554+
content: modelContent,
1555+
safetyRatings: [], // Assuming negligible for this test
1556+
finishReason: .stop,
1557+
citationMetadata: nil
1558+
)
1559+
1560+
// 4. Create GenerateContentResponse
1561+
let response = GenerateContentResponse(candidates: [candidate])
1562+
1563+
// 5. Assertions for inlineDataParts
1564+
let inlineParts = response.inlineDataParts
1565+
XCTAssertFalse(inlineParts.isEmpty, "inlineDataParts should not be empty.")
1566+
XCTAssertEqual(inlineParts.count, 1, "There should be exactly one InlineDataPart.")
1567+
1568+
let firstInlinePart = try XCTUnwrap(inlineParts.first, "Could not get the first inline part.")
1569+
XCTAssertEqual(firstInlinePart.mimeType, "image/png", "MimeType should match.")
1570+
XCTAssertFalse(firstInlinePart.data.isEmpty, "Inline data should not be empty.")
1571+
XCTAssertEqual(firstInlinePart.data, imageData) // Verify data content
1572+
1573+
// 6. Assertion for text (ensure other properties still work)
1574+
XCTAssertEqual(response.text, "This is the text part.")
1575+
1576+
// 7. Assertion for function calls (ensure it's empty)
1577+
XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty.")
1578+
}
1579+
1580+
func testGenerateContentResponse_inlineDataParts_noInlineData() throws {
1581+
// 1. Create mock parts (only text)
1582+
let textPart = TextPart("This is the text part.")
1583+
let funcCallPart = FunctionCallPart(name: "testFunc", args: nil) // Add another part type
1584+
1585+
// 2. Create ModelContent
1586+
let modelContent = ModelContent(parts: [textPart, funcCallPart])
1587+
1588+
// 3. Create Candidate
1589+
let candidate = Candidate(
1590+
content: modelContent,
1591+
safetyRatings: [],
1592+
finishReason: .stop,
1593+
citationMetadata: nil
1594+
)
1595+
1596+
// 4. Create GenerateContentResponse
1597+
let response = GenerateContentResponse(candidates: [candidate])
1598+
1599+
// 5. Assertions for inlineDataParts
1600+
let inlineParts = response.inlineDataParts
1601+
XCTAssertTrue(inlineParts.isEmpty, "inlineDataParts should be empty.")
1602+
1603+
// 6. Assertion for text
1604+
XCTAssertEqual(response.text, "This is the text part.")
1605+
1606+
// 7. Assertion for function calls
1607+
XCTAssertEqual(response.functionCalls.count, 1)
1608+
XCTAssertEqual(response.functionCalls.first?.name, "testFunc")
1609+
}
1610+
1611+
func testGenerateContentResponse_inlineDataParts_noCandidates() throws {
1612+
// 1. Create GenerateContentResponse with no candidates
1613+
let response = GenerateContentResponse(candidates: [])
1614+
1615+
// 2. Assertions for inlineDataParts
1616+
let inlineParts = response.inlineDataParts
1617+
XCTAssertTrue(inlineParts.isEmpty, "inlineDataParts should be empty when there are no candidates.")
1618+
1619+
// 3. Assertion for text
1620+
XCTAssertNil(response.text, "Text should be nil when there are no candidates.")
1621+
1622+
// 4. Assertion for function calls
1623+
XCTAssertTrue(response.functionCalls.isEmpty, "functionCalls should be empty when there are no candidates.")
1624+
}
1625+
1626+
15411627
// MARK: - Helpers
15421628

15431629
private func testFirebaseInfo(appCheck: AppCheckInterop? = nil,

0 commit comments

Comments
 (0)