From 8e454a6fbd57b5cf0c3676bab4d847cda2324c3c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 02:26:52 +0000 Subject: [PATCH 1/5] feat(firebase_ai): handle unknown parts when parsing content --- .../firebase_ai/lib/src/content.dart | 86 ++++++++++++------- .../firebase_ai/test/content_test.dart | 21 +++-- 2 files changed, 72 insertions(+), 35 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/content.dart b/packages/firebase_ai/firebase_ai/lib/src/content.dart index ac47ed996069..e82ecc218b24 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/content.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/content.dart @@ -13,7 +13,9 @@ // limitations under the License. import 'dart:convert'; +import 'dart:developer'; import 'dart:typed_data'; + import 'error.dart'; /// The base structured datatype containing multi-part content of a message. @@ -81,37 +83,51 @@ Content parseContent(Object jsonObject) { /// Parse the [Part] from json object. Part parsePart(Object? jsonObject) { - if (jsonObject is Map && jsonObject.containsKey('functionCall')) { - final functionCall = jsonObject['functionCall']; - if (functionCall is Map && - functionCall.containsKey('name') && - functionCall.containsKey('args')) { - return FunctionCall( - functionCall['name'] as String, - functionCall['args'] as Map, - id: functionCall['id'] as String?, - ); - } else { - throw unhandledFormat('functionCall', functionCall); - } + if (jsonObject is! Map) { + log('Unhandled part format: $jsonObject'); + return UnknownPart({ + 'unhandled': jsonObject, + }); } - return switch (jsonObject) { - {'text': final String text} => TextPart(text), - { - 'file_data': { - 'file_uri': final String fileUri, - 'mime_type': final String mimeType + try { + if (jsonObject.containsKey('functionCall')) { + final functionCall = jsonObject['functionCall']; + if (functionCall is Map && + functionCall.containsKey('name') && + functionCall.containsKey('args')) { + return FunctionCall( + functionCall['name'] as String, + functionCall['args'] as Map, + id: functionCall['id'] as String?, + ); + } else { + throw unhandledFormat('functionCall', functionCall); } - } => - FileData(mimeType, fileUri), - { - 'functionResponse': {'name': String _, 'response': Map _} - } => - throw UnimplementedError('FunctionResponse part not yet supported'), - {'inlineData': {'mimeType': String mimeType, 'data': String bytes}} => - InlineDataPart(mimeType, base64Decode(bytes)), - _ => throw unhandledFormat('Part', jsonObject), - }; + } + return switch (jsonObject) { + {'text': final String text} => TextPart(text), + { + 'file_data': { + 'file_uri': final String fileUri, + 'mime_type': final String mimeType + } + } => + FileData(mimeType, fileUri), + { + 'functionResponse': { + 'name': String _, + 'response': Map _ + } + } => + throw UnimplementedError('FunctionResponse part not yet supported'), + {'inlineData': {'mimeType': String mimeType, 'data': String bytes}} => + InlineDataPart(mimeType, base64Decode(bytes)), + _ => throw unhandledFormat('Part', jsonObject), + }; + } on Object catch (e) { + log('unhandled part format: $jsonObject, $e'); + return UnknownPart(jsonObject); + } } /// A datatype containing media that is part of a multi-part [Content] message. @@ -120,6 +136,18 @@ sealed class Part { Object toJson(); } +/// A [Part] that contains unparsable data. +final class UnknownPart implements Part { + // ignore: public_member_api_docs + UnknownPart(this.data); + + /// The unparsed data. + final Map data; + + @override + Object toJson() => data; +} + /// A [Part] with the text content. final class TextPart implements Part { // ignore: public_member_api_docs diff --git a/packages/firebase_ai/firebase_ai/test/content_test.dart b/packages/firebase_ai/firebase_ai/test/content_test.dart index fc2957d89d73..f9c6f8c8424e 100644 --- a/packages/firebase_ai/firebase_ai/test/content_test.dart +++ b/packages/firebase_ai/firebase_ai/test/content_test.dart @@ -199,17 +199,26 @@ void main() { expect(() => parsePart(json), throwsA(isA())); }); - test('throws unhandledFormat for invalid JSON', () { + test('returns UnknownPart for invalid JSON', () { final json = {'invalid': 'data'}; - expect(() => parsePart(json), throwsA(isA())); + final result = parsePart(json); + expect(result, isA()); + final unknownPart = result as UnknownPart; + expect(unknownPart.data, json); }); - test('throws unhandledFormat for null input', () { - expect(() => parsePart(null), throwsA(isA())); + test('returns UnknownPart for null input', () { + final result = parsePart(null); + expect(result, isA()); + final unknownPart = result as UnknownPart; + expect(unknownPart.data, {'unhandled': null}); }); - test('throws unhandledFormat for empty map', () { - expect(() => parsePart({}), throwsA(isA())); + test('returns UnknownPart for empty map', () { + final result = parsePart({}); + expect(result, isA()); + final unknownPart = result as UnknownPart; + expect(unknownPart.data, {}); }); }); } From 206747e9883b03514201b57645fabbe15ebff0e2 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Tue, 15 Jul 2025 20:07:00 -0700 Subject: [PATCH 2/5] tweak the content test --- packages/firebase_ai/firebase_ai/lib/src/content.dart | 7 ------- packages/firebase_ai/firebase_ai/test/content_test.dart | 9 ++++++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/content.dart b/packages/firebase_ai/firebase_ai/lib/src/content.dart index e82ecc218b24..9dc5511f12be 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/content.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/content.dart @@ -113,13 +113,6 @@ Part parsePart(Object? jsonObject) { } } => FileData(mimeType, fileUri), - { - 'functionResponse': { - 'name': String _, - 'response': Map _ - } - } => - throw UnimplementedError('FunctionResponse part not yet supported'), {'inlineData': {'mimeType': String mimeType, 'data': String bytes}} => InlineDataPart(mimeType, base64Decode(bytes)), _ => throw unhandledFormat('Part', jsonObject), diff --git a/packages/firebase_ai/firebase_ai/test/content_test.dart b/packages/firebase_ai/firebase_ai/test/content_test.dart index f9c6f8c8424e..ddbbacfd50c2 100644 --- a/packages/firebase_ai/firebase_ai/test/content_test.dart +++ b/packages/firebase_ai/firebase_ai/test/content_test.dart @@ -192,11 +192,14 @@ void main() { expect(inlineData.bytes, [1, 2, 3]); }); - test('throws UnimplementedError for functionResponse', () { + test('returns UnknownPart for functionResponse', () { final json = { 'functionResponse': {'name': 'test', 'response': {}} }; - expect(() => parsePart(json), throwsA(isA())); + final result = parsePart(json); + expect(result, isA()); + final unknownPart = result as UnknownPart; + expect(unknownPart.data, json); }); test('returns UnknownPart for invalid JSON', () { @@ -218,7 +221,7 @@ void main() { final result = parsePart({}); expect(result, isA()); final unknownPart = result as UnknownPart; - expect(unknownPart.data, {}); + expect(unknownPart.data, {'unhandled': {}}); }); }); } From 6523b1db436229578c0cdf685443d457eec8cb03 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Tue, 15 Jul 2025 20:58:02 -0700 Subject: [PATCH 3/5] remove the extra exception --- .../firebase_ai/lib/src/content.dart | 59 +++++++++---------- .../firebase_ai/test/content_test.dart | 1 - 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/content.dart b/packages/firebase_ai/firebase_ai/lib/src/content.dart index 9dc5511f12be..b2661d05c880 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/content.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/content.dart @@ -89,38 +89,37 @@ Part parsePart(Object? jsonObject) { 'unhandled': jsonObject, }); } - try { - if (jsonObject.containsKey('functionCall')) { - final functionCall = jsonObject['functionCall']; - if (functionCall is Map && - functionCall.containsKey('name') && - functionCall.containsKey('args')) { - return FunctionCall( - functionCall['name'] as String, - functionCall['args'] as Map, - id: functionCall['id'] as String?, - ); - } else { - throw unhandledFormat('functionCall', functionCall); - } + + if (jsonObject.containsKey('functionCall')) { + final functionCall = jsonObject['functionCall']; + if (functionCall is Map && + functionCall.containsKey('name') && + functionCall.containsKey('args')) { + return FunctionCall( + functionCall['name'] as String, + functionCall['args'] as Map, + id: functionCall['id'] as String?, + ); + } else { + throw unhandledFormat('functionCall', functionCall); } - return switch (jsonObject) { - {'text': final String text} => TextPart(text), - { - 'file_data': { - 'file_uri': final String fileUri, - 'mime_type': final String mimeType - } - } => - FileData(mimeType, fileUri), - {'inlineData': {'mimeType': String mimeType, 'data': String bytes}} => - InlineDataPart(mimeType, base64Decode(bytes)), - _ => throw unhandledFormat('Part', jsonObject), - }; - } on Object catch (e) { - log('unhandled part format: $jsonObject, $e'); - return UnknownPart(jsonObject); } + return switch (jsonObject) { + {'text': final String text} => TextPart(text), + { + 'file_data': { + 'file_uri': final String fileUri, + 'mime_type': final String mimeType + } + } => + FileData(mimeType, fileUri), + {'inlineData': {'mimeType': String mimeType, 'data': String bytes}} => + InlineDataPart(mimeType, base64Decode(bytes)), + _ => () { + log('unhandled part format: $jsonObject'); + return UnknownPart(jsonObject); + }(), + }; } /// A datatype containing media that is part of a multi-part [Content] message. diff --git a/packages/firebase_ai/firebase_ai/test/content_test.dart b/packages/firebase_ai/firebase_ai/test/content_test.dart index ddbbacfd50c2..59a68bd6a198 100644 --- a/packages/firebase_ai/firebase_ai/test/content_test.dart +++ b/packages/firebase_ai/firebase_ai/test/content_test.dart @@ -16,7 +16,6 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:firebase_ai/src/content.dart'; -import 'package:firebase_ai/src/error.dart'; import 'package:flutter_test/flutter_test.dart'; // Mock google_ai classes (if needed) From 9c6cf133ddec26fe55709ac5f3e0737107f93580 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Tue, 29 Jul 2025 17:46:28 -0700 Subject: [PATCH 4/5] fix the test error --- packages/firebase_ai/firebase_ai/test/utils/matchers.dart | 3 +++ .../firebase_vertexai/test/utils/matchers.dart | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/firebase_ai/firebase_ai/test/utils/matchers.dart b/packages/firebase_ai/firebase_ai/test/utils/matchers.dart index 39c23188b677..aa085da49475 100644 --- a/packages/firebase_ai/firebase_ai/test/utils/matchers.dart +++ b/packages/firebase_ai/firebase_ai/test/utils/matchers.dart @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. import 'package:firebase_ai/firebase_ai.dart'; +import 'package:firebase_ai/src/content.dart'; import 'package:http/http.dart' as http; import 'package:matcher/matcher.dart'; @@ -33,6 +34,8 @@ Matcher matchesPart(Part part) => switch (part) { isA() .having((p) => p.name, 'name', name) .having((p) => p.response, 'args', response), + UnknownPart(data: final data) => + isA().having((p) => p.data, 'data', data), }; Matcher matchesContent(Content content) => isA() diff --git a/packages/firebase_vertexai/firebase_vertexai/test/utils/matchers.dart b/packages/firebase_vertexai/firebase_vertexai/test/utils/matchers.dart index 28e72e65a4cb..8daf0b12abf8 100644 --- a/packages/firebase_vertexai/firebase_vertexai/test/utils/matchers.dart +++ b/packages/firebase_vertexai/firebase_vertexai/test/utils/matchers.dart @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +import 'package:firebase_ai/src/content.dart'; import 'package:firebase_vertexai/firebase_vertexai.dart'; import 'package:http/http.dart' as http; import 'package:matcher/matcher.dart'; @@ -33,6 +34,8 @@ Matcher matchesPart(Part part) => switch (part) { isA() .having((p) => p.name, 'name', name) .having((p) => p.response, 'args', response), + UnknownPart(data: final data) => + isA().having((p) => p.data, 'data', data), }; Matcher matchesContent(Content content) => isA() From 86ed39eaaccfef0f9e4a86deb5ddc0c25917ebd2 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Tue, 29 Jul 2025 17:58:49 -0700 Subject: [PATCH 5/5] fix test for vertex ai --- .../firebase_vertexai/test/content_test.dart | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/firebase_vertexai/firebase_vertexai/test/content_test.dart b/packages/firebase_vertexai/firebase_vertexai/test/content_test.dart index 21db7a03f852..59a68bd6a198 100644 --- a/packages/firebase_vertexai/firebase_vertexai/test/content_test.dart +++ b/packages/firebase_vertexai/firebase_vertexai/test/content_test.dart @@ -16,8 +16,6 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:firebase_ai/src/content.dart'; -import 'package:firebase_vertexai/firebase_vertexai.dart' - show VertexAISdkException; import 'package:flutter_test/flutter_test.dart'; // Mock google_ai classes (if needed) @@ -193,24 +191,36 @@ void main() { expect(inlineData.bytes, [1, 2, 3]); }); - test('throws UnimplementedError for functionResponse', () { + test('returns UnknownPart for functionResponse', () { final json = { 'functionResponse': {'name': 'test', 'response': {}} }; - expect(() => parsePart(json), throwsA(isA())); + final result = parsePart(json); + expect(result, isA()); + final unknownPart = result as UnknownPart; + expect(unknownPart.data, json); }); - test('throws unhandledFormat for invalid JSON', () { + test('returns UnknownPart for invalid JSON', () { final json = {'invalid': 'data'}; - expect(() => parsePart(json), throwsA(isA())); + final result = parsePart(json); + expect(result, isA()); + final unknownPart = result as UnknownPart; + expect(unknownPart.data, json); }); - test('throws unhandledFormat for null input', () { - expect(() => parsePart(null), throwsA(isA())); + test('returns UnknownPart for null input', () { + final result = parsePart(null); + expect(result, isA()); + final unknownPart = result as UnknownPart; + expect(unknownPart.data, {'unhandled': null}); }); - test('throws unhandledFormat for empty map', () { - expect(() => parsePart({}), throwsA(isA())); + test('returns UnknownPart for empty map', () { + final result = parsePart({}); + expect(result, isA()); + final unknownPart = result as UnknownPart; + expect(unknownPart.data, {'unhandled': {}}); }); }); }