Skip to content

Commit 7977505

Browse files
committed
Add URLContextMetadata to Candidate
1 parent 26d3be3 commit 7977505

File tree

6 files changed

+186
-24
lines changed

6 files changed

+186
-24
lines changed

FirebaseAI/Sources/AILog.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ enum AILog {
6363
case generateContentResponseUnrecognizedContentModality = 3012
6464
case decodedUnsupportedImagenPredictionType = 3013
6565
case decodedUnsupportedPartData = 3014
66+
case fallbackValueUsed = 3017
67+
case urlMetadataUnrecognizedURLRetrievalStatus = 3018
6668

6769
// SDK State Errors
6870
case generateContentResponseNoCandidates = 4000
@@ -124,4 +126,32 @@ enum AILog {
124126
static func additionalLoggingEnabled() -> Bool {
125127
return ProcessInfo.processInfo.arguments.contains(enableArgumentKey)
126128
}
129+
130+
/// Returns the unwrapped optional value if non-nil or returns the fallback value and logs.
131+
///
132+
/// This convenience method is intended for use in place of `optionalValue ?? fallbackValue` with
133+
/// the addition of logging on use of the fallback value.
134+
///
135+
/// - Parameters:
136+
/// - optionalValue: The value to unwrap.
137+
/// - fallbackValue: The fallback (default) value to return when `optionalValue` is `nil`.
138+
/// - level: The logging level to use for fallback messages; defaults to
139+
/// `FirebaseLoggerLevel.warning`.
140+
/// - code: The message code to use for fallback messages; defaults to
141+
/// `MessageCode.fallbackValueUsed`.
142+
/// - caller: The name of the unwrapped value; defaults to the name of the computed property or
143+
/// function name from which the unwrapping occurred.
144+
static func safeUnwrap<T>(_ optionalValue: T?,
145+
fallback fallbackValue: T,
146+
level: FirebaseLoggerLevel = .warning,
147+
code: MessageCode = .fallbackValueUsed,
148+
caller: String = #function) -> T {
149+
guard let unwrappedValue = optionalValue else {
150+
AILog.log(level: level, code: code, """
151+
No value specified for '\(caller)' (\(T.self)); using fallback value '\(fallbackValue)'.
152+
""")
153+
return fallbackValue
154+
}
155+
return unwrappedValue
156+
}
127157
}

FirebaseAI/Sources/GenerateContentResponse.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,19 @@ public struct Candidate: Sendable {
154154

155155
public let groundingMetadata: GroundingMetadata?
156156

157+
/// Metadata related to the ``URLContext`` tool.
158+
public let urlContextMetadata: URLContextMetadata?
159+
157160
/// Initializer for SwiftUI previews or tests.
158161
public init(content: ModelContent, safetyRatings: [SafetyRating], finishReason: FinishReason?,
159-
citationMetadata: CitationMetadata?, groundingMetadata: GroundingMetadata? = nil) {
162+
citationMetadata: CitationMetadata?, groundingMetadata: GroundingMetadata? = nil,
163+
urlContextMetadata: URLContextMetadata? = nil) {
160164
self.content = content
161165
self.safetyRatings = safetyRatings
162166
self.finishReason = finishReason
163167
self.citationMetadata = citationMetadata
164168
self.groundingMetadata = groundingMetadata
169+
self.urlContextMetadata = urlContextMetadata
165170
}
166171

167172
// Returns `true` if the candidate contains no information that a developer could use.
@@ -499,6 +504,7 @@ extension Candidate: Decodable {
499504
case finishReason
500505
case citationMetadata
501506
case groundingMetadata
507+
case urlContextMetadata
502508
}
503509

504510
/// Initializes a response from a decoder. Used for decoding server responses; not for public
@@ -540,6 +546,14 @@ extension Candidate: Decodable {
540546
GroundingMetadata.self,
541547
forKey: .groundingMetadata
542548
)
549+
550+
if let urlContextMetadata =
551+
try container.decodeIfPresent(URLContextMetadata.self, forKey: .urlContextMetadata),
552+
!urlContextMetadata.urlMetadata.isEmpty {
553+
self.urlContextMetadata = urlContextMetadata
554+
} else {
555+
urlContextMetadata = nil
556+
}
543557
}
544558
}
545559

FirebaseAI/Sources/Tool.swift

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,6 @@ public struct GoogleSearch: Sendable {
6363
public init() {}
6464
}
6565

66-
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
67-
public struct URLContext: Sendable {
68-
public init() {}
69-
}
70-
7166
/// A helper tool that the model may use when generating responses.
7267
///
7368
/// A `Tool` is a piece of code that enables the system to interact with external systems to perform
@@ -76,27 +71,18 @@ public struct URLContext: Sendable {
7671
public struct Tool: Sendable {
7772
/// A list of `FunctionDeclarations` available to the model.
7873
let functionDeclarations: [FunctionDeclaration]?
74+
7975
/// Specifies the Google Search configuration.
8076
let googleSearch: GoogleSearch?
8177

8278
let urlContext: URLContext?
8379

84-
init(functionDeclarations: [FunctionDeclaration]?) {
80+
init(functionDeclarations: [FunctionDeclaration]? = nil,
81+
googleSearch: GoogleSearch? = nil,
82+
urlContext: URLContext? = nil) {
8583
self.functionDeclarations = functionDeclarations
86-
googleSearch = nil
87-
urlContext = nil
88-
}
89-
90-
init(googleSearch: GoogleSearch) {
9184
self.googleSearch = googleSearch
92-
functionDeclarations = nil
93-
urlContext = nil
94-
}
95-
96-
init(urlContext: URLContext) {
9785
self.urlContext = urlContext
98-
functionDeclarations = nil
99-
googleSearch = nil
10086
}
10187

10288
/// Creates a tool that allows the model to perform function calling.
@@ -142,8 +128,8 @@ public struct Tool: Sendable {
142128
return self.init(googleSearch: googleSearch)
143129
}
144130

145-
public static func urlContext(_ urlContext: URLContext = URLContext()) -> Tool {
146-
return self.init(urlContext: urlContext)
131+
public static func urlContext() -> Tool {
132+
return self.init(urlContext: URLContext())
147133
}
148134
}
149135

@@ -233,8 +219,5 @@ extension FunctionCallingConfig.Mode: Encodable {}
233219
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
234220
extension GoogleSearch: Encodable {}
235221

236-
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
237-
extension URLContext: Encodable {}
238-
239222
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
240223
extension ToolConfig: Encodable {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
16+
public struct URLContext: Sendable, Encodable {
17+
init() {}
18+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
/// Metadata related to the ``URLContext`` tool.
16+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
17+
public struct URLContextMetadata: Sendable, Hashable {
18+
/// List of URL context.
19+
public let urlMetadata: [URLMetadata]
20+
}
21+
22+
// MARK: - Codable Conformances
23+
24+
extension URLContextMetadata: Decodable {
25+
enum CodingKeys: CodingKey {
26+
case urlMetadata
27+
}
28+
29+
public init(from decoder: any Decoder) throws {
30+
let container = try decoder.container(keyedBy: CodingKeys.self)
31+
urlMetadata = try container.decodeIfPresent([URLMetadata].self, forKey: .urlMetadata) ?? []
32+
}
33+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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 Foundation
16+
17+
/// Context of a single URL retrieval.
18+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
19+
public struct URLMetadata: Sendable, Hashable {
20+
/// Status of the URL retrieval.
21+
public struct URLRetrievalStatus: DecodableProtoEnum, Hashable {
22+
enum Kind: String {
23+
case unspecified = "URL_RETRIEVAL_STATUS_UNSPECIFIED"
24+
case success = "URL_RETRIEVAL_STATUS_SUCCESS"
25+
case error = "URL_RETRIEVAL_STATUS_ERROR"
26+
case paywall = "URL_RETRIEVAL_STATUS_PAYWALL"
27+
case unsafe = "URL_RETRIEVAL_STATUS_UNSAFE"
28+
}
29+
30+
/// Internal only - default value.
31+
static let unspecified = URLRetrievalStatus(kind: .unspecified)
32+
33+
/// URL retrieval succeeded.
34+
public static let success = URLRetrievalStatus(kind: .success)
35+
36+
/// URL retrieval failed due to an error.
37+
public static let error = URLRetrievalStatus(kind: .error)
38+
39+
// URL retrieval failed failed because the content is behind paywall.
40+
public static let paywall = URLRetrievalStatus(kind: .paywall)
41+
42+
// URL retrieval failed because the content is unsafe.
43+
public static let unsafe = URLRetrievalStatus(kind: .unsafe)
44+
45+
/// Returns the raw string representation of the `URLRetrievalStatus` value.
46+
public let rawValue: String
47+
48+
static let unrecognizedValueMessageCode =
49+
AILog.MessageCode.urlMetadataUnrecognizedURLRetrievalStatus
50+
}
51+
52+
/// The URL retrieved by the ``URLContext`` tool.
53+
public let retrievedURL: URL?
54+
55+
/// The status of the URL retrieval.
56+
public let retrievalStatus: URLRetrievalStatus
57+
}
58+
59+
// MARK: - Codable Conformances
60+
61+
extension URLMetadata: Decodable {
62+
enum CodingKeys: String, CodingKey {
63+
case retrievedURL = "retrievedUrl"
64+
case retrievalStatus = "urlRetrievalStatus"
65+
}
66+
67+
public init(from decoder: any Decoder) throws {
68+
let container = try decoder.container(keyedBy: CodingKeys.self)
69+
70+
if let retrievedURLString = try container.decodeIfPresent(String.self, forKey: .retrievedURL),
71+
let retrievedURL = URL(string: retrievedURLString) {
72+
self.retrievedURL = retrievedURL
73+
} else {
74+
retrievedURL = nil
75+
}
76+
let retrievalStatus = try container.decodeIfPresent(
77+
URLMetadata.URLRetrievalStatus.self, forKey: .retrievalStatus
78+
)
79+
80+
self.retrievalStatus = AILog.safeUnwrap(
81+
retrievalStatus, fallback: URLMetadata.URLRetrievalStatus(kind: .unspecified)
82+
)
83+
}
84+
}

0 commit comments

Comments
 (0)