diff --git a/packages/firebase_ai/firebase_ai/lib/src/content.dart b/packages/firebase_ai/firebase_ai/lib/src/content.dart index ac47ed996069..b2661d05c880 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,7 +83,14 @@ Content parseContent(Object jsonObject) { /// Parse the [Part] from json object. Part parsePart(Object? jsonObject) { - if (jsonObject is Map && jsonObject.containsKey('functionCall')) { + if (jsonObject is! Map) { + log('Unhandled part format: $jsonObject'); + return UnknownPart({ + 'unhandled': jsonObject, + }); + } + + if (jsonObject.containsKey('functionCall')) { final functionCall = jsonObject['functionCall']; if (functionCall is Map && functionCall.containsKey('name') && @@ -104,13 +113,12 @@ 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), + _ => () { + log('unhandled part format: $jsonObject'); + return UnknownPart(jsonObject); + }(), }; } @@ -120,6 +128,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..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) @@ -192,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': {}}); }); }); } 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/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': {}}); }); }); } 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()