Skip to content

Commit ac59c24

Browse files
feat(firebaseai): handle unknown parts when parsing content (#17522)
* feat(firebase_ai): handle unknown parts when parsing content * tweak the content test * remove the extra exception * fix the test error * fix test for vertex ai --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent f6491b9 commit ac59c24

File tree

5 files changed

+72
-25
lines changed

5 files changed

+72
-25
lines changed

packages/firebase_ai/firebase_ai/lib/src/content.dart

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
// limitations under the License.
1414

1515
import 'dart:convert';
16+
import 'dart:developer';
1617
import 'dart:typed_data';
18+
1719
import 'error.dart';
1820

1921
/// The base structured datatype containing multi-part content of a message.
@@ -81,7 +83,14 @@ Content parseContent(Object jsonObject) {
8183

8284
/// Parse the [Part] from json object.
8385
Part parsePart(Object? jsonObject) {
84-
if (jsonObject is Map && jsonObject.containsKey('functionCall')) {
86+
if (jsonObject is! Map<String, Object?>) {
87+
log('Unhandled part format: $jsonObject');
88+
return UnknownPart(<String, Object?>{
89+
'unhandled': jsonObject,
90+
});
91+
}
92+
93+
if (jsonObject.containsKey('functionCall')) {
8594
final functionCall = jsonObject['functionCall'];
8695
if (functionCall is Map &&
8796
functionCall.containsKey('name') &&
@@ -104,13 +113,12 @@ Part parsePart(Object? jsonObject) {
104113
}
105114
} =>
106115
FileData(mimeType, fileUri),
107-
{
108-
'functionResponse': {'name': String _, 'response': Map<String, Object?> _}
109-
} =>
110-
throw UnimplementedError('FunctionResponse part not yet supported'),
111116
{'inlineData': {'mimeType': String mimeType, 'data': String bytes}} =>
112117
InlineDataPart(mimeType, base64Decode(bytes)),
113-
_ => throw unhandledFormat('Part', jsonObject),
118+
_ => () {
119+
log('unhandled part format: $jsonObject');
120+
return UnknownPart(jsonObject);
121+
}(),
114122
};
115123
}
116124

@@ -120,6 +128,18 @@ sealed class Part {
120128
Object toJson();
121129
}
122130

131+
/// A [Part] that contains unparsable data.
132+
final class UnknownPart implements Part {
133+
// ignore: public_member_api_docs
134+
UnknownPart(this.data);
135+
136+
/// The unparsed data.
137+
final Map<String, Object?> data;
138+
139+
@override
140+
Object toJson() => data;
141+
}
142+
123143
/// A [Part] with the text content.
124144
final class TextPart implements Part {
125145
// ignore: public_member_api_docs

packages/firebase_ai/firebase_ai/test/content_test.dart

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import 'dart:convert';
1616
import 'dart:typed_data';
1717

1818
import 'package:firebase_ai/src/content.dart';
19-
import 'package:firebase_ai/src/error.dart';
2019
import 'package:flutter_test/flutter_test.dart';
2120

2221
// Mock google_ai classes (if needed)
@@ -192,24 +191,36 @@ void main() {
192191
expect(inlineData.bytes, [1, 2, 3]);
193192
});
194193

195-
test('throws UnimplementedError for functionResponse', () {
194+
test('returns UnknownPart for functionResponse', () {
196195
final json = {
197196
'functionResponse': {'name': 'test', 'response': {}}
198197
};
199-
expect(() => parsePart(json), throwsA(isA<FirebaseAISdkException>()));
198+
final result = parsePart(json);
199+
expect(result, isA<UnknownPart>());
200+
final unknownPart = result as UnknownPart;
201+
expect(unknownPart.data, json);
200202
});
201203

202-
test('throws unhandledFormat for invalid JSON', () {
204+
test('returns UnknownPart for invalid JSON', () {
203205
final json = {'invalid': 'data'};
204-
expect(() => parsePart(json), throwsA(isA<Exception>()));
206+
final result = parsePart(json);
207+
expect(result, isA<UnknownPart>());
208+
final unknownPart = result as UnknownPart;
209+
expect(unknownPart.data, json);
205210
});
206211

207-
test('throws unhandledFormat for null input', () {
208-
expect(() => parsePart(null), throwsA(isA<Exception>()));
212+
test('returns UnknownPart for null input', () {
213+
final result = parsePart(null);
214+
expect(result, isA<UnknownPart>());
215+
final unknownPart = result as UnknownPart;
216+
expect(unknownPart.data, {'unhandled': null});
209217
});
210218

211-
test('throws unhandledFormat for empty map', () {
212-
expect(() => parsePart({}), throwsA(isA<Exception>()));
219+
test('returns UnknownPart for empty map', () {
220+
final result = parsePart({});
221+
expect(result, isA<UnknownPart>());
222+
final unknownPart = result as UnknownPart;
223+
expect(unknownPart.data, {'unhandled': {}});
213224
});
214225
});
215226
}

packages/firebase_ai/firebase_ai/test/utils/matchers.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414
import 'package:firebase_ai/firebase_ai.dart';
15+
import 'package:firebase_ai/src/content.dart';
1516
import 'package:http/http.dart' as http;
1617
import 'package:matcher/matcher.dart';
1718

@@ -33,6 +34,8 @@ Matcher matchesPart(Part part) => switch (part) {
3334
isA<FunctionResponse>()
3435
.having((p) => p.name, 'name', name)
3536
.having((p) => p.response, 'args', response),
37+
UnknownPart(data: final data) =>
38+
isA<UnknownPart>().having((p) => p.data, 'data', data),
3639
};
3740

3841
Matcher matchesContent(Content content) => isA<Content>()

packages/firebase_vertexai/firebase_vertexai/test/content_test.dart

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import 'dart:convert';
1616
import 'dart:typed_data';
1717

1818
import 'package:firebase_ai/src/content.dart';
19-
import 'package:firebase_vertexai/firebase_vertexai.dart'
20-
show VertexAISdkException;
2119
import 'package:flutter_test/flutter_test.dart';
2220

2321
// Mock google_ai classes (if needed)
@@ -193,24 +191,36 @@ void main() {
193191
expect(inlineData.bytes, [1, 2, 3]);
194192
});
195193

196-
test('throws UnimplementedError for functionResponse', () {
194+
test('returns UnknownPart for functionResponse', () {
197195
final json = {
198196
'functionResponse': {'name': 'test', 'response': {}}
199197
};
200-
expect(() => parsePart(json), throwsA(isA<VertexAISdkException>()));
198+
final result = parsePart(json);
199+
expect(result, isA<UnknownPart>());
200+
final unknownPart = result as UnknownPart;
201+
expect(unknownPart.data, json);
201202
});
202203

203-
test('throws unhandledFormat for invalid JSON', () {
204+
test('returns UnknownPart for invalid JSON', () {
204205
final json = {'invalid': 'data'};
205-
expect(() => parsePart(json), throwsA(isA<Exception>()));
206+
final result = parsePart(json);
207+
expect(result, isA<UnknownPart>());
208+
final unknownPart = result as UnknownPart;
209+
expect(unknownPart.data, json);
206210
});
207211

208-
test('throws unhandledFormat for null input', () {
209-
expect(() => parsePart(null), throwsA(isA<Exception>()));
212+
test('returns UnknownPart for null input', () {
213+
final result = parsePart(null);
214+
expect(result, isA<UnknownPart>());
215+
final unknownPart = result as UnknownPart;
216+
expect(unknownPart.data, {'unhandled': null});
210217
});
211218

212-
test('throws unhandledFormat for empty map', () {
213-
expect(() => parsePart({}), throwsA(isA<Exception>()));
219+
test('returns UnknownPart for empty map', () {
220+
final result = parsePart({});
221+
expect(result, isA<UnknownPart>());
222+
final unknownPart = result as UnknownPart;
223+
expect(unknownPart.data, {'unhandled': {}});
214224
});
215225
});
216226
}

packages/firebase_vertexai/firebase_vertexai/test/utils/matchers.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14+
import 'package:firebase_ai/src/content.dart';
1415
import 'package:firebase_vertexai/firebase_vertexai.dart';
1516
import 'package:http/http.dart' as http;
1617
import 'package:matcher/matcher.dart';
@@ -33,6 +34,8 @@ Matcher matchesPart(Part part) => switch (part) {
3334
isA<FunctionResponse>()
3435
.having((p) => p.name, 'name', name)
3536
.having((p) => p.response, 'args', response),
37+
UnknownPart(data: final data) =>
38+
isA<UnknownPart>().having((p) => p.data, 'data', data),
3639
};
3740

3841
Matcher matchesContent(Content content) => isA<Content>()

0 commit comments

Comments
 (0)