Skip to content

Commit 4ca1413

Browse files
authored
[Vertex AI] Add Citation.publicationDate (#13893)
1 parent b99e3d7 commit 4ca1413

File tree

6 files changed

+401
-2
lines changed

6 files changed

+401
-2
lines changed

FirebaseVertexAI/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
that may be reported when a prompt is blocked. (#13861)
7373
- [added] Added the `PromptFeedback` property `blockReasonMessage` that *may* be
7474
provided alongside the `blockReason`. (#13891)
75+
- [added] Added an optional `publicationDate` property that *may* be provided in
76+
`Citation`. (#13893)
7577

7678
# 11.3.0
7779
- [added] Added `Decodable` conformance for `FunctionResponse`. (#13606)

FirebaseVertexAI/Sources/GenerateContentResponse.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ public struct Citation: Sendable {
141141

142142
/// The license the cited source work is distributed under, if specified.
143143
public let license: String?
144+
145+
/// The publication date of the cited source, if available.
146+
///
147+
/// > Tip: `DateComponents` can be converted to a `Date` using the `date` computed property.
148+
public let publicationDate: DateComponents?
144149
}
145150

146151
/// A value enumerating possible reasons for a model to terminate a content generation request.
@@ -363,28 +368,47 @@ extension Citation: Decodable {
363368
case uri
364369
case title
365370
case license
371+
case publicationDate
366372
}
367373

368374
public init(from decoder: any Decoder) throws {
369375
let container = try decoder.container(keyedBy: CodingKeys.self)
370376
startIndex = try container.decodeIfPresent(Int.self, forKey: .startIndex) ?? 0
371377
endIndex = try container.decode(Int.self, forKey: .endIndex)
378+
372379
if let uri = try container.decodeIfPresent(String.self, forKey: .uri), !uri.isEmpty {
373380
self.uri = uri
374381
} else {
375382
uri = nil
376383
}
384+
377385
if let title = try container.decodeIfPresent(String.self, forKey: .title), !title.isEmpty {
378386
self.title = title
379387
} else {
380388
title = nil
381389
}
390+
382391
if let license = try container.decodeIfPresent(String.self, forKey: .license),
383392
!license.isEmpty {
384393
self.license = license
385394
} else {
386395
license = nil
387396
}
397+
398+
if let publicationProtoDate = try container.decodeIfPresent(
399+
ProtoDate.self,
400+
forKey: .publicationDate
401+
) {
402+
publicationDate = publicationProtoDate.dateComponents
403+
if let publicationDate, !publicationDate.isValidDate {
404+
VertexLog.warning(
405+
code: .decodedInvalidCitationPublicationDate,
406+
"Decoded an invalid citation publication date: \(publicationDate)"
407+
)
408+
}
409+
} else {
410+
publicationDate = nil
411+
}
388412
}
389413
}
390414

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2024 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 Foundation
16+
17+
/// Represents a whole or partial calendar date, such as a birthday.
18+
///
19+
/// The time of day and time zone are either specified elsewhere or are insignificant. The date is
20+
/// relative to the Gregorian Calendar. This can represent one of the following:
21+
/// - A full date, with non-zero year, month, and day values
22+
/// - A month and day value, with a zero year, such as an anniversary
23+
/// - A year on its own, with zero month and day values
24+
/// - A year and month value, with a zero day, such as a credit card expiration date
25+
///
26+
/// This represents a
27+
/// [`google.type.Date`](https://cloud.google.com/vertex-ai/docs/reference/rest/Shared.Types/Date).
28+
struct ProtoDate {
29+
/// Year of the date.
30+
///
31+
/// Must be from 1 to 9999, or 0 to specify a date without a year.
32+
let year: Int?
33+
34+
/// Month of a year.
35+
///
36+
/// Must be from 1 to 12, or 0 to specify a year without a month and day.
37+
let month: Int?
38+
39+
/// Day of a month.
40+
///
41+
/// Must be from 1 to 31 and valid for the year and month, or 0 to specify a year by itself or a
42+
/// year and month where the day isn't significant.
43+
let day: Int?
44+
45+
/// Returns the a `DateComponents` representation of the `ProtoDate`.
46+
///
47+
/// > Note: This uses the Gregorian `Calendar` to match the `google.type.Date` definition.
48+
var dateComponents: DateComponents {
49+
DateComponents(
50+
calendar: Calendar(identifier: .gregorian),
51+
year: year,
52+
month: month,
53+
day: day
54+
)
55+
}
56+
}
57+
58+
// MARK: - Codable Conformance
59+
60+
extension ProtoDate: Decodable {
61+
enum CodingKeys: CodingKey {
62+
case year
63+
case month
64+
case day
65+
}
66+
67+
init(from decoder: any Decoder) throws {
68+
let container = try decoder.container(keyedBy: CodingKeys.self)
69+
if let year = try container.decodeIfPresent(Int.self, forKey: .year), year != 0 {
70+
if year < 0 || year > 9999 {
71+
VertexLog.warning(
72+
code: .decodedInvalidProtoDateYear,
73+
"""
74+
Invalid year: \(year); must be from 1 to 9999, or 0 for a date without a specified year.
75+
"""
76+
)
77+
}
78+
self.year = year
79+
} else {
80+
year = nil
81+
}
82+
83+
if let month = try container.decodeIfPresent(Int.self, forKey: .month), month != 0 {
84+
if month < 0 || month > 12 {
85+
VertexLog.warning(
86+
code: .decodedInvalidProtoDateMonth,
87+
"""
88+
Invalid month: \(month); must be from 1 to 12, or 0 for a year date without a specified \
89+
month and day.
90+
"""
91+
)
92+
}
93+
self.month = month
94+
} else {
95+
month = nil
96+
}
97+
98+
if let day = try container.decodeIfPresent(Int.self, forKey: .day), day != 0 {
99+
if day < 0 || day > 31 {
100+
VertexLog.warning(
101+
code: .decodedInvalidProtoDateDay,
102+
"Invalid day: \(day); must be from 1 to 31, or 0 for a date without a specified day."
103+
)
104+
}
105+
self.day = day
106+
} else {
107+
day = nil
108+
}
109+
110+
guard year != nil || month != nil || day != nil else {
111+
throw DecodingError.dataCorrupted(.init(
112+
codingPath: [CodingKeys.year, CodingKeys.month, CodingKeys.day],
113+
debugDescription: "Invalid date: missing year, month and day"
114+
))
115+
}
116+
}
117+
}

FirebaseVertexAI/Sources/VertexLog.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ enum VertexLog {
5050
case generateContentResponseUnrecognizedHarmProbability = 3005
5151
case generateContentResponseUnrecognizedHarmCategory = 3006
5252
case generateContentResponseUnrecognizedHarmSeverity = 3007
53+
case decodedInvalidProtoDateYear = 3008
54+
case decodedInvalidProtoDateMonth = 3009
55+
case decodedInvalidProtoDateDay = 3010
56+
case decodedInvalidCitationPublicationDate = 3011
5357

5458
// SDK State Errors
5559
case generateContentResponseNoCandidates = 4000

FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ final class GenerativeModelTests: XCTestCase {
134134
forResource: "unary-success-citations",
135135
withExtension: "json"
136136
)
137+
let expectedPublicationDate = DateComponents(
138+
calendar: Calendar(identifier: .gregorian),
139+
year: 2019,
140+
month: 5,
141+
day: 10
142+
)
137143

138144
let response = try await model.generateContent(testPrompt)
139145

@@ -149,8 +155,10 @@ final class GenerativeModelTests: XCTestCase {
149155
XCTAssertEqual(citationSource1.endIndex, 128)
150156
XCTAssertNil(citationSource1.title)
151157
XCTAssertNil(citationSource1.license)
158+
XCTAssertNil(citationSource1.publicationDate)
152159
let citationSource2 = try XCTUnwrap(citationMetadata.citations[1])
153160
XCTAssertEqual(citationSource2.title, "some-citation-2")
161+
XCTAssertEqual(citationSource2.publicationDate, expectedPublicationDate)
154162
XCTAssertEqual(citationSource2.startIndex, 130)
155163
XCTAssertEqual(citationSource2.endIndex, 265)
156164
XCTAssertNil(citationSource2.uri)
@@ -161,6 +169,7 @@ final class GenerativeModelTests: XCTestCase {
161169
XCTAssertEqual(citationSource3.endIndex, 431)
162170
XCTAssertEqual(citationSource3.license, "mit")
163171
XCTAssertNil(citationSource3.title)
172+
XCTAssertNil(citationSource3.publicationDate)
164173
}
165174

166175
func testGenerateContent_success_quoteReply() async throws {
@@ -1052,6 +1061,12 @@ final class GenerativeModelTests: XCTestCase {
10521061
forResource: "streaming-success-citations",
10531062
withExtension: "txt"
10541063
)
1064+
let expectedPublicationDate = DateComponents(
1065+
calendar: Calendar(identifier: .gregorian),
1066+
year: 2014,
1067+
month: 3,
1068+
day: 30
1069+
)
10551070

10561071
let stream = try model.generateContentStream("Hi")
10571072
var citations = [Citation]()
@@ -1072,18 +1087,19 @@ final class GenerativeModelTests: XCTestCase {
10721087
.contains {
10731088
$0.startIndex == 0 && $0.endIndex == 128
10741089
&& $0.uri == "https://www.example.com/some-citation-1" && $0.title == nil
1075-
&& $0.license == nil
1090+
&& $0.license == nil && $0.publicationDate == nil
10761091
})
10771092
XCTAssertTrue(citations
10781093
.contains {
10791094
$0.startIndex == 130 && $0.endIndex == 265 && $0.uri == nil
10801095
&& $0.title == "some-citation-2" && $0.license == nil
1096+
&& $0.publicationDate == expectedPublicationDate
10811097
})
10821098
XCTAssertTrue(citations
10831099
.contains {
10841100
$0.startIndex == 272 && $0.endIndex == 431
10851101
&& $0.uri == "https://www.example.com/some-citation-3" && $0.title == nil
1086-
&& $0.license == "mit"
1102+
&& $0.license == "mit" && $0.publicationDate == nil
10871103
})
10881104
XCTAssertFalse(citations.contains { $0.uri?.isEmpty ?? false })
10891105
XCTAssertFalse(citations.contains { $0.title?.isEmpty ?? false })

0 commit comments

Comments
 (0)