Skip to content

Commit c70f616

Browse files
andrewheardG.Dev.Ssomsak
authored andcommitted
Add public API tests for GenerateContentResponse (google-gemini#133)
1 parent 7ceeba8 commit c70f616

File tree

2 files changed

+121
-190
lines changed

2 files changed

+121
-190
lines changed

Sources/GoogleAI/GenerateContentResponse.swift

Lines changed: 109 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -17,58 +17,24 @@ import Foundation
1717
/// The model's response to a generate content request.
1818
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
1919
public struct GenerateContentResponse {
20-
/// Token usage metadata for processing the generate content request.
21-
public struct UsageMetadata {
22-
/// The number of tokens in the request prompt.
23-
public let promptTokenCount: Int
24-
25-
/// The total number of tokens across the generated response candidates.
26-
public let candidatesTokenCount: Int
27-
28-
/// The total number of tokens in both the request and response.
29-
public let totalTokenCount: Int
30-
}
31-
3220
/// A list of candidate response content, ordered from best to worst.
3321
public let candidates: [CandidateResponse]
3422

3523
/// A value containing the safety ratings for the response, or, if the request was blocked, a
3624
/// reason for blocking the request.
3725
public let promptFeedback: PromptFeedback?
3826

39-
/// Token usage metadata for processing the generate content request.
40-
public let usageMetadata: UsageMetadata?
41-
4227
/// The response's content as text, if it exists.
4328
public var text: String? {
4429
guard let candidate = candidates.first else {
4530
Logging.default.error("Could not get text from a response that had no candidates.")
4631
return nil
4732
}
48-
let textValues: [String] = candidate.content.parts.compactMap { part in
49-
guard case let .text(text) = part else {
50-
return nil
51-
}
52-
return text
53-
}
54-
guard textValues.count > 0 else {
33+
guard let text = candidate.content.parts.first?.text else {
5534
Logging.default.error("Could not get a text part from the first candidate.")
5635
return nil
5736
}
58-
return textValues.joined(separator: " ")
59-
}
60-
61-
/// Returns function calls found in any `Part`s of the first candidate of the response, if any.
62-
public var functionCalls: [FunctionCall] {
63-
guard let candidate = candidates.first else {
64-
return []
65-
}
66-
return candidate.content.parts.compactMap { part in
67-
guard case let .functionCall(functionCall) = part else {
68-
return nil
69-
}
70-
return functionCall
71-
}
37+
return text
7238
}
7339

7440
/// Returns function calls found in any `Part`s of the first candidate of the response, if any.
@@ -85,128 +51,17 @@ public struct GenerateContentResponse {
8551
}
8652

8753
/// Initializer for SwiftUI previews or tests.
88-
public init(candidates: [CandidateResponse], promptFeedback: PromptFeedback? = nil,
89-
usageMetadata: UsageMetadata? = nil) {
54+
public init(candidates: [CandidateResponse], promptFeedback: PromptFeedback? = nil) {
9055
self.candidates = candidates
9156
self.promptFeedback = promptFeedback
92-
self.usageMetadata = usageMetadata
9357
}
9458
}
9559

96-
/// A struct representing a possible reply to a content generation prompt. Each content generation
97-
/// prompt may produce multiple candidate responses.
98-
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
99-
public struct CandidateResponse {
100-
/// The response's content.
101-
public let content: ModelContent
102-
103-
/// The safety rating of the response content.
104-
public let safetyRatings: [SafetyRating]
105-
106-
/// The reason the model stopped generating content, if it exists; for example, if the model
107-
/// generated a predefined stop sequence.
108-
public let finishReason: FinishReason?
109-
110-
/// Cited works in the model's response content, if it exists.
111-
public let citationMetadata: CitationMetadata?
112-
113-
/// Initializer for SwiftUI previews or tests.
114-
public init(content: ModelContent, safetyRatings: [SafetyRating], finishReason: FinishReason?,
115-
citationMetadata: CitationMetadata?) {
116-
self.content = content
117-
self.safetyRatings = safetyRatings
118-
self.finishReason = finishReason
119-
self.citationMetadata = citationMetadata
120-
}
121-
}
122-
123-
/// A collection of source attributions for a piece of content.
124-
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
125-
public struct CitationMetadata {
126-
/// A list of individual cited sources and the parts of the content to which they apply.
127-
public let citationSources: [Citation]
128-
}
129-
130-
/// A struct describing a source attribution.
131-
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
132-
public struct Citation {
133-
/// The inclusive beginning of a sequence in a model response that derives from a cited source.
134-
public let startIndex: Int
135-
136-
/// The exclusive end of a sequence in a model response that derives from a cited source.
137-
public let endIndex: Int
138-
139-
/// A link to the cited source.
140-
public let uri: String
141-
142-
/// The license the cited source work is distributed under, if specified.
143-
public let license: String?
144-
}
145-
146-
/// A value enumerating possible reasons for a model to terminate a content generation request.
147-
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
148-
public enum FinishReason: String {
149-
case unknown = "FINISH_REASON_UNKNOWN"
150-
151-
case unspecified = "FINISH_REASON_UNSPECIFIED"
152-
153-
/// Natural stop point of the model or provided stop sequence.
154-
case stop = "STOP"
155-
156-
/// The maximum number of tokens as specified in the request was reached.
157-
case maxTokens = "MAX_TOKENS"
158-
159-
/// The token generation was stopped because the response was flagged for safety reasons.
160-
/// NOTE: When streaming, the Candidate.content will be empty if content filters blocked the
161-
/// output.
162-
case safety = "SAFETY"
163-
164-
/// The token generation was stopped because the response was flagged for unauthorized citations.
165-
case recitation = "RECITATION"
166-
167-
/// All other reasons that stopped token generation.
168-
case other = "OTHER"
169-
}
170-
171-
/// A metadata struct containing any feedback the model had on the prompt it was provided.
172-
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
173-
public struct PromptFeedback {
174-
/// A type describing possible reasons to block a prompt.
175-
public enum BlockReason: String {
176-
/// The block reason is unknown.
177-
case unknown = "UNKNOWN"
178-
179-
/// The block reason was not specified in the server response.
180-
case unspecified = "BLOCK_REASON_UNSPECIFIED"
181-
182-
/// The prompt was blocked because it was deemed unsafe.
183-
case safety = "SAFETY"
184-
185-
/// All other block reasons.
186-
case other = "OTHER"
187-
}
188-
189-
/// The reason a prompt was blocked, if it was blocked.
190-
public let blockReason: BlockReason?
191-
192-
/// The safety ratings of the prompt.
193-
public let safetyRatings: [SafetyRating]
194-
195-
/// Initializer for SwiftUI previews or tests.
196-
public init(blockReason: BlockReason?, safetyRatings: [SafetyRating]) {
197-
self.blockReason = blockReason
198-
self.safetyRatings = safetyRatings
199-
}
200-
}
201-
202-
// MARK: - Codable Conformances
203-
20460
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
20561
extension GenerateContentResponse: Decodable {
20662
enum CodingKeys: CodingKey {
20763
case candidates
20864
case promptFeedback
209-
case usageMetadata
21065
}
21166

21267
public init(from decoder: Decoder) throws {
@@ -231,24 +86,33 @@ extension GenerateContentResponse: Decodable {
23186
candidates = []
23287
}
23388
promptFeedback = try container.decodeIfPresent(PromptFeedback.self, forKey: .promptFeedback)
234-
usageMetadata = try container.decodeIfPresent(UsageMetadata.self, forKey: .usageMetadata)
23589
}
23690
}
23791

92+
/// A struct representing a possible reply to a content generation prompt. Each content generation
93+
/// prompt may produce multiple candidate responses.
23894
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
239-
extension GenerateContentResponse.UsageMetadata: Decodable {
240-
enum CodingKeys: CodingKey {
241-
case promptTokenCount
242-
case candidatesTokenCount
243-
case totalTokenCount
244-
}
95+
public struct CandidateResponse {
96+
/// The response's content.
97+
public let content: ModelContent
24598

246-
public init(from decoder: any Decoder) throws {
247-
let container = try decoder.container(keyedBy: CodingKeys.self)
248-
promptTokenCount = try container.decodeIfPresent(Int.self, forKey: .promptTokenCount) ?? 0
249-
candidatesTokenCount = try container
250-
.decodeIfPresent(Int.self, forKey: .candidatesTokenCount) ?? 0
251-
totalTokenCount = try container.decodeIfPresent(Int.self, forKey: .totalTokenCount) ?? 0
99+
/// The safety rating of the response content.
100+
public let safetyRatings: [SafetyRating]
101+
102+
/// The reason the model stopped generating content, if it exists; for example, if the model
103+
/// generated a predefined stop sequence.
104+
public let finishReason: FinishReason?
105+
106+
/// Cited works in the model's response content, if it exists.
107+
public let citationMetadata: CitationMetadata?
108+
109+
/// Initializer for SwiftUI previews or tests.
110+
public init(content: ModelContent, safetyRatings: [SafetyRating], finishReason: FinishReason?,
111+
citationMetadata: CitationMetadata?) {
112+
self.content = content
113+
self.safetyRatings = safetyRatings
114+
self.finishReason = finishReason
115+
self.citationMetadata = citationMetadata
252116
}
253117
}
254118

@@ -262,6 +126,8 @@ extension CandidateResponse: Decodable {
262126
case citationMetadata
263127
}
264128

129+
/// Initializes a response from a decoder. Used for decoding server responses; not for public
130+
/// use.
265131
public init(from decoder: Decoder) throws {
266132
let container = try decoder.container(keyedBy: CodingKeys.self)
267133

@@ -299,34 +165,57 @@ extension CandidateResponse: Decodable {
299165
}
300166
}
301167

168+
/// A collection of source attributions for a piece of content.
302169
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
303-
extension CitationMetadata: Decodable {}
170+
public struct CitationMetadata: Decodable {
171+
/// A list of individual cited sources and the parts of the content to which they apply.
172+
public let citationSources: [Citation]
173+
}
304174

175+
/// A struct describing a source attribution.
305176
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
306-
extension Citation: Decodable {
307-
enum CodingKeys: CodingKey {
308-
case startIndex
309-
case endIndex
310-
case uri
311-
case license
312-
}
177+
public struct Citation: Decodable {
178+
/// The inclusive beginning of a sequence in a model response that derives from a cited source.
179+
public let startIndex: Int
313180

314-
public init(from decoder: any Decoder) throws {
315-
let container = try decoder.container(keyedBy: CodingKeys.self)
316-
startIndex = try container.decodeIfPresent(Int.self, forKey: .startIndex) ?? 0
317-
endIndex = try container.decode(Int.self, forKey: .endIndex)
318-
uri = try container.decode(String.self, forKey: .uri)
319-
if let license = try container.decodeIfPresent(String.self, forKey: .license),
320-
!license.isEmpty {
321-
self.license = license
322-
} else {
323-
license = nil
324-
}
325-
}
181+
/// The exclusive end of a sequence in a model response that derives from a cited source.
182+
public let endIndex: Int
183+
184+
/// A link to the cited source.
185+
public let uri: String
186+
187+
/// The license the cited source work is distributed under.
188+
public let license: String
189+
}
190+
191+
/// A value enumerating possible reasons for a model to terminate a content generation request.
192+
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
193+
public enum FinishReason: String {
194+
case unknown = "FINISH_REASON_UNKNOWN"
195+
196+
case unspecified = "FINISH_REASON_UNSPECIFIED"
197+
198+
/// Natural stop point of the model or provided stop sequence.
199+
case stop = "STOP"
200+
201+
/// The maximum number of tokens as specified in the request was reached.
202+
case maxTokens = "MAX_TOKENS"
203+
204+
/// The token generation was stopped because the response was flagged for safety reasons.
205+
/// NOTE: When streaming, the Candidate.content will be empty if content filters blocked the
206+
/// output.
207+
case safety = "SAFETY"
208+
209+
/// The token generation was stopped because the response was flagged for unauthorized citations.
210+
case recitation = "RECITATION"
211+
212+
/// All other reasons that stopped token generation.
213+
case other = "OTHER"
326214
}
327215

328216
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
329217
extension FinishReason: Decodable {
218+
/// Do not explicitly use. Initializer required for Decodable conformance.
330219
public init(from decoder: Decoder) throws {
331220
let value = try decoder.singleValueContainer().decode(String.self)
332221
guard let decodedFinishReason = FinishReason(rawValue: value) else {
@@ -340,18 +229,47 @@ extension FinishReason: Decodable {
340229
}
341230
}
342231

232+
/// A metadata struct containing any feedback the model had on the prompt it was provided.
343233
@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *)
344-
extension PromptFeedback.BlockReason: Decodable {
345-
public init(from decoder: Decoder) throws {
346-
let value = try decoder.singleValueContainer().decode(String.self)
347-
guard let decodedBlockReason = PromptFeedback.BlockReason(rawValue: value) else {
348-
Logging.default
349-
.error("[GoogleGenerativeAI] Unrecognized BlockReason with value \"\(value)\".")
350-
self = .unknown
351-
return
234+
public struct PromptFeedback {
235+
/// A type describing possible reasons to block a prompt.
236+
public enum BlockReason: String, Decodable {
237+
/// The block reason is unknown.
238+
case unknown = "UNKNOWN"
239+
240+
/// The block reason was not specified in the server response.
241+
case unspecified = "BLOCK_REASON_UNSPECIFIED"
242+
243+
/// The prompt was blocked because it was deemed unsafe.
244+
case safety = "SAFETY"
245+
246+
/// All other block reasons.
247+
case other = "OTHER"
248+
249+
/// Do not explicitly use. Initializer required for Decodable conformance.
250+
public init(from decoder: Decoder) throws {
251+
let value = try decoder.singleValueContainer().decode(String.self)
252+
guard let decodedBlockReason = BlockReason(rawValue: value) else {
253+
Logging.default
254+
.error("[GoogleGenerativeAI] Unrecognized BlockReason with value \"\(value)\".")
255+
self = .unknown
256+
return
257+
}
258+
259+
self = decodedBlockReason
352260
}
261+
}
262+
263+
/// The reason a prompt was blocked, if it was blocked.
264+
public let blockReason: BlockReason?
353265

354-
self = decodedBlockReason
266+
/// The safety ratings of the prompt.
267+
public let safetyRatings: [SafetyRating]
268+
269+
/// Initializer for SwiftUI previews or tests.
270+
public init(blockReason: BlockReason?, safetyRatings: [SafetyRating]) {
271+
self.blockReason = blockReason
272+
self.safetyRatings = safetyRatings
355273
}
356274
}
357275

@@ -362,6 +280,7 @@ extension PromptFeedback: Decodable {
362280
case safetyRatings
363281
}
364282

283+
/// Do not explicitly use. Initializer required for Decodable conformance.
365284
public init(from decoder: Decoder) throws {
366285
let container = try decoder.container(keyedBy: CodingKeys.self)
367286
blockReason = try container.decodeIfPresent(

0 commit comments

Comments
 (0)