From b49f458352f9e5d42bfcd0fb3515dadcda662f4a Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 17:54:05 -0800 Subject: [PATCH 01/34] - --- packages/genai_primitives/example/main.dart | 12 +- .../lib/genai_primitives.dart | 2 +- .../lib/src/chat_message.dart | 141 ------------------ packages/genai_primitives/lib/src/mesage.dart | 83 +++++++++++ .../lib/src/message_parts.dart | 128 +++++++++------- .../test/genai_primitives_test.dart | 18 +-- 6 files changed, 175 insertions(+), 209 deletions(-) delete mode 100644 packages/genai_primitives/lib/src/chat_message.dart create mode 100644 packages/genai_primitives/lib/src/mesage.dart diff --git a/packages/genai_primitives/example/main.dart b/packages/genai_primitives/example/main.dart index b17da41a4..4d657f029 100644 --- a/packages/genai_primitives/example/main.dart +++ b/packages/genai_primitives/example/main.dart @@ -33,15 +33,15 @@ void main() { print(const JsonEncoder.withIndent(' ').convert(getWeatherTool.toJson())); // 2. Create a conversation history - final history = [ + final history = [ // System message - ChatMessage.system( + Message.system( 'You are a helpful weather assistant. ' 'Use the get_weather tool when needed.', ), // User message asking for weather - ChatMessage.user('What is the weather in London?'), + Message.user('What is the weather in London?'), ]; print('\n[Initial Conversation]'); @@ -50,7 +50,7 @@ void main() { } // 3. Simulate Model Response with Tool Call - final modelResponse = ChatMessage.model( + final modelResponse = Message.model( '', // Empty text for tool call parts: [ const TextPart('Thinking: User wants weather for London...'), @@ -71,7 +71,7 @@ void main() { } // 4. Simulate Tool Execution & Result - final toolResult = ChatMessage.user( + final toolResult = Message.user( '', // User role is typically used for tool results in many APIs parts: [ const ToolPart.result( @@ -88,7 +88,7 @@ void main() { // 5. Simulate Final Model Response with Data (e.g. an image generated or // returned) - final finalResponse = ChatMessage.model( + final finalResponse = Message.model( 'Here is a chart of the weather trend:', parts: [ DataPart( diff --git a/packages/genai_primitives/lib/genai_primitives.dart b/packages/genai_primitives/lib/genai_primitives.dart index aa212c4cd..5a65855db 100644 --- a/packages/genai_primitives/lib/genai_primitives.dart +++ b/packages/genai_primitives/lib/genai_primitives.dart @@ -5,6 +5,6 @@ /// A set of primitives for working with generative AI. library; -export 'src/chat_message.dart'; +export 'src/mesage.dart'; export 'src/message_parts.dart'; export 'src/tool_definition.dart'; diff --git a/packages/genai_primitives/lib/src/chat_message.dart b/packages/genai_primitives/lib/src/chat_message.dart deleted file mode 100644 index 70afc7678..000000000 --- a/packages/genai_primitives/lib/src/chat_message.dart +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:meta/meta.dart'; - -import 'message_parts.dart'; -import 'utils.dart'; - -/// A message in a conversation between a user and a model. -@immutable -class ChatMessage { - /// Creates a new message. - const ChatMessage({ - required this.role, - required this.parts, - this.metadata = const {}, - }); - - /// Creates a message from a JSON-compatible map. - factory ChatMessage.fromJson(Map json) => ChatMessage( - role: ChatMessageRole.values.byName(json['role'] as String), - parts: (json['parts'] as List) - .map((p) => Part.fromJson(p as Map)) - .toList(), - metadata: (json['metadata'] as Map?) ?? const {}, - ); - - /// Creates a system message. - factory ChatMessage.system( - String text, { - List parts = const [], - Map? metadata, - }) => ChatMessage( - role: ChatMessageRole.system, - parts: [TextPart(text), ...parts], - metadata: metadata ?? const {}, - ); - - /// Creates a user message with text. - factory ChatMessage.user( - String text, { - List parts = const [], - Map? metadata, - }) => ChatMessage( - role: ChatMessageRole.user, - parts: [TextPart(text), ...parts], - metadata: metadata ?? const {}, - ); - - /// Creates a model message with text. - factory ChatMessage.model( - String text, { - List parts = const [], - Map? metadata, - }) => ChatMessage( - role: ChatMessageRole.model, - parts: [TextPart(text), ...parts], - metadata: metadata ?? const {}, - ); - - /// The role of the message author. - final ChatMessageRole role; - - /// The content parts of the message. - final List parts; - - /// Optional metadata associated with this message. - /// Can include information like suppressed content, warnings, etc. - final Map metadata; - - /// Gets the text content of the message by concatenating all text parts. - String get text => parts.whereType().map((p) => p.text).join(); - - /// Checks if this message contains any tool calls. - bool get hasToolCalls => - parts.whereType().any((p) => p.kind == ToolPartKind.call); - - /// Gets all tool calls in this message. - List get toolCalls => parts - .whereType() - .where((p) => p.kind == ToolPartKind.call) - .toList(); - - /// Checks if this message contains any tool results. - bool get hasToolResults => - parts.whereType().any((p) => p.kind == ToolPartKind.result); - - /// Gets all tool results in this message. - List get toolResults => parts - .whereType() - .where((p) => p.kind == ToolPartKind.result) - .toList(); - - /// Converts the message to a JSON-compatible map. - Map toJson() => { - 'role': role.name, - 'parts': parts.map((p) => p.toJson()).toList(), - 'metadata': metadata, - }; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - return other is ChatMessage && - other.role == role && - listEquals(other.parts, parts) && - mapEquals(other.metadata, metadata); - } - - @override - int get hashCode => Object.hash( - role, - Object.hashAll(parts), - Object.hashAll(metadata.entries), - ); - - @override - String toString() => - 'Message(role: $role, parts: $parts, metadata: $metadata)'; -} - -/// The role of a message author. -/// -/// The role indicates the source of the message or the intended perspective. -/// For example, a system message is sent to the model to set context, -/// a user message is sent to the model, and a model message is a response -/// to the user. -enum ChatMessageRole { - /// A message from the system that sets context or instructions for the model. - /// - /// System messages are typically sent to the model to define its behavior - /// or persona ("system prompt"). They are not usually shown to the end user. - system, - - /// A message from the end user to the model ("user prompt"). - user, - - /// A message from the model to the user ("model response"). - model, -} diff --git a/packages/genai_primitives/lib/src/mesage.dart b/packages/genai_primitives/lib/src/mesage.dart new file mode 100644 index 000000000..d3b40ef42 --- /dev/null +++ b/packages/genai_primitives/lib/src/mesage.dart @@ -0,0 +1,83 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import 'message_parts.dart'; +import 'utils.dart'; + +final class _Json { + static const parts = 'parts'; + static const metadata = 'metadata'; +} + +/// A message in a conversation between a user and a model. +@immutable +class Message { + /// Creates a new message. + const Message({ + String text = '', + required this.parts, + this.metadata = const {}, + }); + + /// Creates a message from a JSON-compatible map. + factory Message.fromJson(Map json) => Message( + parts: (json[_Json.parts] as List) + .map((p) => Part.fromJson(p as Map)) + .toList(), + metadata: (json[_Json.metadata] as Map?) ?? const {}, + ); + + /// The content parts of the message. + final List parts; + + /// Optional metadata associated with this message. + /// Can include information like suppressed content, warnings, etc. + final Map metadata; + + /// Gets the text content of the message by concatenating all text parts. + String get text => parts.whereType().map((p) => p.text).join(); + + /// Checks if this message contains any tool calls. + bool get hasToolCalls => + parts.whereType().any((p) => p.kind == ToolPartKind.call); + + /// Gets all tool calls in this message. + List get toolCalls => parts + .whereType() + .where((p) => p.kind == ToolPartKind.call) + .toList(); + + /// Checks if this message contains any tool results. + bool get hasToolResults => + parts.whereType().any((p) => p.kind == ToolPartKind.result); + + /// Gets all tool results in this message. + List get toolResults => parts + .whereType() + .where((p) => p.kind == ToolPartKind.result) + .toList(); + + /// Converts the message to a JSON-compatible map. + Map toJson() => { + _Json.parts: parts.map((p) => p.toJson()).toList(), + _Json.metadata: metadata, + }; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Message && + listEquals(other.parts, parts) && + mapEquals(other.metadata, metadata); + } + + @override + int get hashCode => + Object.hash(Object.hashAll(parts), Object.hashAll(metadata.entries)); + + @override + String toString() => 'Message(parts: $parts, metadata: $metadata)'; +} diff --git a/packages/genai_primitives/lib/src/message_parts.dart b/packages/genai_primitives/lib/src/message_parts.dart index 0077cb8de..392650e33 100644 --- a/packages/genai_primitives/lib/src/message_parts.dart +++ b/packages/genai_primitives/lib/src/message_parts.dart @@ -14,6 +14,25 @@ import 'package:path/path.dart' as p; import 'utils.dart'; +final class _Json { + static const type = 'type'; + static const content = 'content'; + static const mimeType = 'mimeType'; + static const name = 'name'; + static const bytes = 'bytes'; + static const url = 'url'; + static const id = 'id'; + static const arguments = 'arguments'; + static const result = 'result'; +} + +final class _Part { + static const text = 'TextPart'; + static const data = 'DataPart'; + static const link = 'LinkPart'; + static const tool = 'ToolPart'; +} + /// Base class for message content parts. @immutable abstract class Part { @@ -21,45 +40,50 @@ abstract class Part { const Part(); /// Creates a part from a JSON-compatible map. - factory Part.fromJson(Map json) => switch (json['type']) { - 'TextPart' => TextPart(json['content'] as String), - 'DataPart' => () { - final content = json['content'] as Map; - final dataUri = content['bytes'] as String; - final Uri uri = Uri.parse(dataUri); - return DataPart( - uri.data!.contentAsBytes(), - mimeType: content['mimeType'] as String, - name: content['name'] as String?, - ); - }(), - 'LinkPart' => () { - final content = json['content'] as Map; - return LinkPart( - Uri.parse(content['url'] as String), - mimeType: content['mimeType'] as String?, - name: content['name'] as String?, - ); - }(), - 'ToolPart' => () { - final content = json['content'] as Map; - // Check if it's a call or result based on presence of arguments or result - if (content.containsKey('arguments')) { - return ToolPart.call( - callId: content['id'] as String, - toolName: content['name'] as String, - arguments: content['arguments'] as Map? ?? {}, + factory Part.fromJson(Map json) { + final Object? type = json[_Json.type]; + + return switch (type) { + _Part.text => TextPart(json[_Json.content] as String), + _Part.data => () { + final content = json[_Json.content] as Map; + final dataUri = content[_Json.bytes] as String; + final Uri uri = Uri.parse(dataUri); + return DataPart( + uri.data!.contentAsBytes(), + mimeType: content[_Json.mimeType] as String, + name: content[_Json.name] as String?, ); - } else { - return ToolPart.result( - callId: content['id'] as String, - toolName: content['name'] as String, - result: content['result'], + }(), + _Part.link => () { + final content = json[_Json.content] as Map; + return LinkPart( + Uri.parse(content[_Json.url] as String), + mimeType: content[_Json.mimeType] as String?, + name: content[_Json.name] as String?, ); - } - }(), - _ => throw UnimplementedError('Unknown part type: ${json['type']}'), - }; + }(), + _Part.tool => () { + final content = json[_Json.content] as Map; + // Check if it's a call or result based on presence of + // arguments or result + if (content.containsKey(_Json.arguments)) { + return ToolPart.call( + callId: content[_Json.id] as String, + toolName: content[_Json.name] as String, + arguments: content[_Json.arguments] as Map? ?? {}, + ); + } else { + return ToolPart.result( + callId: content[_Json.id] as String, + toolName: content[_Json.name] as String, + result: content[_Json.result], + ); + } + }(), + _ => throw UnimplementedError('Unknown part type: $type'), + }; + } /// The default MIME type for binary data. static const defaultMimeType = 'application/octet-stream'; @@ -91,38 +115,38 @@ abstract class Part { final Object content; switch (this) { case final TextPart p: - typeName = 'TextPart'; + typeName = _Part.text; content = p.text; break; case final DataPart p: - typeName = 'DataPart'; + typeName = _Part.data; content = { - if (p.name != null) 'name': p.name, - 'mimeType': p.mimeType, - 'bytes': 'data:${p.mimeType};base64,${base64Encode(p.bytes)}', + if (p.name != null) _Json.name: p.name, + _Json.mimeType: p.mimeType, + _Json.bytes: 'data:${p.mimeType};base64,${base64Encode(p.bytes)}', }; break; case final LinkPart p: - typeName = 'LinkPart'; + typeName = _Part.link; content = { - if (p.name != null) 'name': p.name, - if (p.mimeType != null) 'mimeType': p.mimeType, - 'url': p.url.toString(), + if (p.name != null) _Json.name: p.name, + if (p.mimeType != null) _Json.mimeType: p.mimeType, + _Json.url: p.url.toString(), }; break; case final ToolPart p: - typeName = 'ToolPart'; + typeName = _Part.tool; content = { - 'id': p.callId, - 'name': p.toolName, - if (p.arguments != null) 'arguments': p.arguments, - if (p.result != null) 'result': p.result, + _Json.id: p.callId, + _Json.name: p.toolName, + if (p.arguments != null) _Json.arguments: p.arguments, + if (p.result != null) _Json.result: p.result, }; break; default: throw UnimplementedError('Unknown part type: $runtimeType'); } - return {'type': typeName, 'content': content}; + return {_Json.type: typeName, _Json.content: content}; } } diff --git a/packages/genai_primitives/test/genai_primitives_test.dart b/packages/genai_primitives/test/genai_primitives_test.dart index f813a7112..26bbd9c23 100644 --- a/packages/genai_primitives/test/genai_primitives_test.dart +++ b/packages/genai_primitives/test/genai_primitives_test.dart @@ -200,15 +200,15 @@ void main() { group('ChatMessage', () { test('factories', () { - final system = ChatMessage.system('instructions'); + final system = Message.system('instructions'); expect(system.role, equals(ChatMessageRole.system)); expect(system.text, equals('instructions')); - final user = ChatMessage.user('hello'); + final user = Message.user('hello'); expect(user.role, equals(ChatMessageRole.user)); expect(user.text, equals('hello')); - final model = ChatMessage.model('hi'); + final model = Message.model('hi'); expect(model.role, equals(ChatMessageRole.model)); expect(model.text, equals('hi')); }); @@ -225,7 +225,7 @@ void main() { result: 'ok', ); - final msg1 = ChatMessage( + final msg1 = Message( role: ChatMessageRole.model, parts: [const TextPart('Hi'), toolCall], ); @@ -235,7 +235,7 @@ void main() { expect(msg1.toolResults, isEmpty); expect(msg1.text, equals('Hi')); - final msg2 = ChatMessage(role: ChatMessageRole.user, parts: [toolResult]); + final msg2 = Message(role: ChatMessageRole.user, parts: [toolResult]); expect(msg2.hasToolCalls, isFalse); expect(msg2.hasToolResults, isTrue); expect(msg2.toolCalls, isEmpty); @@ -243,24 +243,24 @@ void main() { }); test('metadata', () { - final msg = ChatMessage.user('hi', metadata: {'key': 'value'}); + final msg = Message.user('hi', metadata: {'key': 'value'}); expect(msg.metadata['key'], equals('value')); final Map json = msg.toJson(); expect(json['metadata'], equals({'key': 'value'})); - final reconstructed = ChatMessage.fromJson(json); + final reconstructed = Message.fromJson(json); expect(reconstructed.metadata, equals({'key': 'value'})); }); test('JSON serialization', () { - final msg = ChatMessage.model('response'); + final msg = Message.model('response'); final Map json = msg.toJson(); expect(json['role'], equals('model')); expect((json['parts'] as List).length, equals(1)); - final reconstructed = ChatMessage.fromJson(json); + final reconstructed = Message.fromJson(json); expect(reconstructed, equals(msg)); }); }); From 62e769da8e7195f46160142e48b20ea00a125524 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 17:58:39 -0800 Subject: [PATCH 02/34] - --- packages/genai_primitives/lib/src/mesage.dart | 6 ++++-- packages/genai_primitives/test/genai_primitives_test.dart | 7 +++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/genai_primitives/lib/src/mesage.dart b/packages/genai_primitives/lib/src/mesage.dart index d3b40ef42..df75a9144 100644 --- a/packages/genai_primitives/lib/src/mesage.dart +++ b/packages/genai_primitives/lib/src/mesage.dart @@ -18,9 +18,11 @@ class Message { /// Creates a new message. const Message({ String text = '', - required this.parts, + this.parts = const [], this.metadata = const {}, - }); + }) { + + }; /// Creates a message from a JSON-compatible map. factory Message.fromJson(Map json) => Message( diff --git a/packages/genai_primitives/test/genai_primitives_test.dart b/packages/genai_primitives/test/genai_primitives_test.dart index 26bbd9c23..ed5c0b577 100644 --- a/packages/genai_primitives/test/genai_primitives_test.dart +++ b/packages/genai_primitives/test/genai_primitives_test.dart @@ -198,11 +198,10 @@ void main() { }); }); - group('ChatMessage', () { + group('Message', () { test('factories', () { - final system = Message.system('instructions'); - expect(system.role, equals(ChatMessageRole.system)); - expect(system.text, equals('instructions')); + final message = Message('instructions'); + expect(message.text, equals('instructions')); final user = Message.user('hello'); expect(user.role, equals(ChatMessageRole.user)); From 5b70ebca8a692db9a33e092a7016df95d1bb1d74 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 18:16:09 -0800 Subject: [PATCH 03/34] Update mesage.dart --- packages/genai_primitives/lib/src/mesage.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/genai_primitives/lib/src/mesage.dart b/packages/genai_primitives/lib/src/mesage.dart index df75a9144..ef95721fc 100644 --- a/packages/genai_primitives/lib/src/mesage.dart +++ b/packages/genai_primitives/lib/src/mesage.dart @@ -16,13 +16,12 @@ final class _Json { @immutable class Message { /// Creates a new message. - const Message({ - String text = '', - this.parts = const [], + const Message(String text,{ + Iterable parts = const [], this.metadata = const {}, }) { - }; + } /// Creates a message from a JSON-compatible map. factory Message.fromJson(Map json) => Message( From 47f4ef592b378e097bab4523f7404fa141d6249e Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 18:18:25 -0800 Subject: [PATCH 04/34] Update mesage.dart --- packages/genai_primitives/lib/src/mesage.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/genai_primitives/lib/src/mesage.dart b/packages/genai_primitives/lib/src/mesage.dart index ef95721fc..2c1ec0077 100644 --- a/packages/genai_primitives/lib/src/mesage.dart +++ b/packages/genai_primitives/lib/src/mesage.dart @@ -16,12 +16,10 @@ final class _Json { @immutable class Message { /// Creates a new message. - const Message(String text,{ - Iterable parts = const [], - this.metadata = const {}, - }) { + const Message({this.parts = const [], this.metadata = const {}}); - } + /// Creates a new message with a single text part. + Message.text(String text) : this(parts: [TextPart(text)]); /// Creates a message from a JSON-compatible map. factory Message.fromJson(Map json) => Message( From ed024adeb522da1ddb3add2cfb876ade531933f9 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 18:27:41 -0800 Subject: [PATCH 05/34] - --- .../lib/src/{mesage.dart => message.dart} | 0 packages/genai_primitives/lib/src/message_parts.dart | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename packages/genai_primitives/lib/src/{mesage.dart => message.dart} (100%) diff --git a/packages/genai_primitives/lib/src/mesage.dart b/packages/genai_primitives/lib/src/message.dart similarity index 100% rename from packages/genai_primitives/lib/src/mesage.dart rename to packages/genai_primitives/lib/src/message.dart diff --git a/packages/genai_primitives/lib/src/message_parts.dart b/packages/genai_primitives/lib/src/message_parts.dart index 392650e33..58a315713 100644 --- a/packages/genai_primitives/lib/src/message_parts.dart +++ b/packages/genai_primitives/lib/src/message_parts.dart @@ -27,10 +27,10 @@ final class _Json { } final class _Part { - static const text = 'TextPart'; - static const data = 'DataPart'; - static const link = 'LinkPart'; - static const tool = 'ToolPart'; + static const text = 'Text'; + static const data = 'Data'; + static const link = 'Link'; + static const tool = 'Tool'; } /// Base class for message content parts. From 0f372568a35ca68841fcb0269a23d36405850b6d Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 18:27:43 -0800 Subject: [PATCH 06/34] Update genai_primitives.dart --- packages/genai_primitives/lib/genai_primitives.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/genai_primitives/lib/genai_primitives.dart b/packages/genai_primitives/lib/genai_primitives.dart index 5a65855db..ad849eeae 100644 --- a/packages/genai_primitives/lib/genai_primitives.dart +++ b/packages/genai_primitives/lib/genai_primitives.dart @@ -5,6 +5,6 @@ /// A set of primitives for working with generative AI. library; -export 'src/mesage.dart'; +export 'src/message.dart'; export 'src/message_parts.dart'; export 'src/tool_definition.dart'; From 918e4d9d3b6def9cb9e3e644cbc7a456a929508d Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 18:31:52 -0800 Subject: [PATCH 07/34] Update genai_primitives_test.dart --- .../test/genai_primitives_test.dart | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/genai_primitives/test/genai_primitives_test.dart b/packages/genai_primitives/test/genai_primitives_test.dart index ed5c0b577..ab8e1a874 100644 --- a/packages/genai_primitives/test/genai_primitives_test.dart +++ b/packages/genai_primitives/test/genai_primitives_test.dart @@ -30,7 +30,7 @@ void main() { test('JSON serialization', () { const part = TextPart('hello'); final Map json = part.toJson(); - expect(json, equals({'type': 'TextPart', 'content': 'hello'})); + expect(json, equals({'type': 'Text', 'content': 'hello'})); final reconstructed = Part.fromJson(json); expect(reconstructed, isA()); @@ -62,7 +62,7 @@ void main() { final part = DataPart(bytes, mimeType: 'image/png', name: 'test.png'); final Map json = part.toJson(); - expect(json['type'], equals('DataPart')); + expect(json['type'], equals('Data')); final content = json['content'] as Map; expect(content['mimeType'], equals('image/png')); expect(content['name'], equals('test.png')); @@ -101,7 +101,7 @@ void main() { final part = LinkPart(uri, mimeType: 'image/png', name: 'image'); final Map json = part.toJson(); - expect(json['type'], equals('LinkPart')); + expect(json['type'], equals('Link')); final content = json['content'] as Map; expect(content['url'], equals(uri.toString())); expect(content['mimeType'], equals('image/png')); @@ -139,8 +139,7 @@ void main() { arguments: {'city': 'London'}, ); final Map json = part.toJson(); - - expect(json['type'], equals('ToolPart')); + expect(json['type'], equals('Tool')); final content = json['content'] as Map; expect(content['id'], equals('call_1')); expect(content['name'], equals('get_weather')); @@ -180,8 +179,7 @@ void main() { result: {'temp': 20}, ); final Map json = part.toJson(); - - expect(json['type'], equals('ToolPart')); + expect(json['type'], equals('Tool')); final content = json['content'] as Map; expect(content['id'], equals('call_1')); expect(content['name'], equals('get_weather')); @@ -199,17 +197,14 @@ void main() { }); group('Message', () { + test('default constructor', () { + final fromParts = const Message(parts: [TextPart('hello')]); + expect(fromParts.text, equals('hello')); + }); + test('factories', () { - final message = Message('instructions'); + final message = Message.text('instructions'); expect(message.text, equals('instructions')); - - final user = Message.user('hello'); - expect(user.role, equals(ChatMessageRole.user)); - expect(user.text, equals('hello')); - - final model = Message.model('hi'); - expect(model.role, equals(ChatMessageRole.model)); - expect(model.text, equals('hi')); }); test('helpers', () { @@ -224,17 +219,14 @@ void main() { result: 'ok', ); - final msg1 = Message( - role: ChatMessageRole.model, - parts: [const TextPart('Hi'), toolCall], - ); + final msg1 = Message(parts: [const TextPart('Hi'), toolCall]); expect(msg1.hasToolCalls, isTrue); expect(msg1.hasToolResults, isFalse); expect(msg1.toolCalls, hasLength(1)); expect(msg1.toolResults, isEmpty); expect(msg1.text, equals('Hi')); - final msg2 = Message(role: ChatMessageRole.user, parts: [toolResult]); + final msg2 = Message(parts: [toolResult]); expect(msg2.hasToolCalls, isFalse); expect(msg2.hasToolResults, isTrue); expect(msg2.toolCalls, isEmpty); @@ -242,7 +234,10 @@ void main() { }); test('metadata', () { - final msg = Message.user('hi', metadata: {'key': 'value'}); + final msg = const Message( + parts: [TextPart('hi')], + metadata: {'key': 'value'}, + ); expect(msg.metadata['key'], equals('value')); final Map json = msg.toJson(); @@ -253,10 +248,9 @@ void main() { }); test('JSON serialization', () { - final msg = Message.model('response'); + final msg = Message.text('response'); final Map json = msg.toJson(); - expect(json['role'], equals('model')); expect((json['parts'] as List).length, equals(1)); final reconstructed = Message.fromJson(json); From daaa464afe114d66d0fcb64d1db055f36229204f Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 18:33:48 -0800 Subject: [PATCH 08/34] - --- packages/genai_primitives/example/main.dart | 2 +- packages/genai_primitives/lib/src/message.dart | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/genai_primitives/example/main.dart b/packages/genai_primitives/example/main.dart index 4d657f029..3ef37ccfc 100644 --- a/packages/genai_primitives/example/main.dart +++ b/packages/genai_primitives/example/main.dart @@ -50,7 +50,7 @@ void main() { } // 3. Simulate Model Response with Tool Call - final modelResponse = Message.model( + final modelResponse = Message( '', // Empty text for tool call parts: [ const TextPart('Thinking: User wants weather for London...'), diff --git a/packages/genai_primitives/lib/src/message.dart b/packages/genai_primitives/lib/src/message.dart index 2c1ec0077..e89c47258 100644 --- a/packages/genai_primitives/lib/src/message.dart +++ b/packages/genai_primitives/lib/src/message.dart @@ -19,7 +19,11 @@ class Message { const Message({this.parts = const [], this.metadata = const {}}); /// Creates a new message with a single text part. - Message.text(String text) : this(parts: [TextPart(text)]); + Message.text( + String text, { + List parts = const [], + Map metadata = const {}, + }) : this(parts: [TextPart(text), ...parts], metadata: metadata); /// Creates a message from a JSON-compatible map. factory Message.fromJson(Map json) => Message( From 0452bea5e65e15b68f8aedf231cad6465f58f537 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 18:54:06 -0800 Subject: [PATCH 09/34] - --- packages/genai_primitives/example/main.dart | 108 +++++++++++------- .../genai_primitives/lib/src/message.dart | 10 +- 2 files changed, 72 insertions(+), 46 deletions(-) diff --git a/packages/genai_primitives/example/main.dart b/packages/genai_primitives/example/main.dart index 3ef37ccfc..1ac39a7f4 100644 --- a/packages/genai_primitives/example/main.dart +++ b/packages/genai_primitives/example/main.dart @@ -8,6 +8,22 @@ import 'dart:typed_data'; import 'package:genai_primitives/genai_primitives.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; +enum Role { system, user, model } + +class ChatMessage { + final Role role; + final Message content; + + ChatMessage({required this.role, required this.content}); + + ChatMessage.system(Message content) + : this(role: Role.system, content: content); + + ChatMessage.user(Message content) : this(role: Role.user, content: content); + + ChatMessage.model(Message content) : this(role: Role.model, content: content); +} + void main() { print('--- GenAI Primitives Example ---'); @@ -33,77 +49,87 @@ void main() { print(const JsonEncoder.withIndent(' ').convert(getWeatherTool.toJson())); // 2. Create a conversation history - final history = [ + final history = [ // System message - Message.system( - 'You are a helpful weather assistant. ' - 'Use the get_weather tool when needed.', + ChatMessage.system( + Message( + 'You are a helpful weather assistant. ' + 'Use the get_weather tool when needed.', + ), ), // User message asking for weather - Message.user('What is the weather in London?'), + ChatMessage.user(Message('What is the weather in London?')), ]; print('\n[Initial Conversation]'); for (final msg in history) { - print('${msg.role.name}: ${msg.text}'); + print('${msg.role.name}: ${msg.content.text}'); } // 3. Simulate Model Response with Tool Call - final modelResponse = Message( - '', // Empty text for tool call - parts: [ - const TextPart('Thinking: User wants weather for London...'), - const ToolPart.call( - callId: 'call_123', - toolName: 'get_weather', - arguments: {'location': 'London', 'unit': 'celsius'}, - ), - ], + final modelResponse = ChatMessage.model( + Message( + '', // Empty text for tool call + parts: [ + const TextPart('Thinking: User wants weather for London...'), + const ToolPart.call( + callId: 'call_123', + toolName: 'get_weather', + arguments: {'location': 'London', 'unit': 'celsius'}, + ), + ], + ), ); history.add(modelResponse); print('\n[Model Response with Tool Call]'); - if (modelResponse.hasToolCalls) { - for (final ToolPart call in modelResponse.toolCalls) { + if (modelResponse.content.hasToolCalls) { + for (final ToolPart call in modelResponse.content.toolCalls) { print('Tool Call: ${call.toolName}(${call.arguments})'); } } // 4. Simulate Tool Execution & Result - final toolResult = Message.user( - '', // User role is typically used for tool results in many APIs - parts: [ - const ToolPart.result( - callId: 'call_123', - toolName: 'get_weather', - result: {'temperature': 15, 'condition': 'Cloudy'}, - ), - ], + final toolResult = ChatMessage.user( + Message( + '', // User role is typically used for tool results in many APIs + parts: [ + const ToolPart.result( + callId: 'call_123', + toolName: 'get_weather', + result: {'temperature': 15, 'condition': 'Cloudy'}, + ), + ], + ), ); history.add(toolResult); print('\n[Tool Result]'); - print('Result: ${toolResult.toolResults.first.result}'); + print('Result: ${toolResult.content.toolResults.first.result}'); // 5. Simulate Final Model Response with Data (e.g. an image generated or // returned) - final finalResponse = Message.model( - 'Here is a chart of the weather trend:', - parts: [ - DataPart( - Uint8List.fromList([0x89, 0x50, 0x4E, 0x47]), // Fake PNG header - mimeType: 'image/png', - name: 'weather_chart.png', - ), - ], + final finalResponse = ChatMessage.model( + Message( + 'Here is a chart of the weather trend:', + parts: [ + DataPart( + Uint8List.fromList([0x89, 0x50, 0x4E, 0x47]), // Fake PNG header + mimeType: 'image/png', + name: 'weather_chart.png', + ), + ], + ), ); history.add(finalResponse); print('\n[Final Model Response with Data]'); - print('Text: ${finalResponse.text}'); - if (finalResponse.parts.any((p) => p is DataPart)) { - final DataPart dataPart = finalResponse.parts.whereType().first; + print('Text: ${finalResponse.content.text}'); + if (finalResponse.content.parts.any((p) => p is DataPart)) { + final DataPart dataPart = finalResponse.content.parts + .whereType() + .first; print( 'Attachment: ${dataPart.name} ' '(${dataPart.mimeType}, ${dataPart.bytes.length} bytes)', @@ -115,6 +141,6 @@ void main() { print( const JsonEncoder.withIndent( ' ', - ).convert(history.map((m) => m.toJson()).toList()), + ).convert(history.map((m) => m.content.toJson()).toList()), ); } diff --git a/packages/genai_primitives/lib/src/message.dart b/packages/genai_primitives/lib/src/message.dart index e89c47258..8eeff62e2 100644 --- a/packages/genai_primitives/lib/src/message.dart +++ b/packages/genai_primitives/lib/src/message.dart @@ -16,17 +16,17 @@ final class _Json { @immutable class Message { /// Creates a new message. - const Message({this.parts = const [], this.metadata = const {}}); + const Message.fromParts({this.parts = const [], this.metadata = const {}}); - /// Creates a new message with a single text part. - Message.text( + /// Creates a new message. + Message( String text, { List parts = const [], Map metadata = const {}, - }) : this(parts: [TextPart(text), ...parts], metadata: metadata); + }) : this.fromParts(parts: [TextPart(text), ...parts], metadata: metadata); /// Creates a message from a JSON-compatible map. - factory Message.fromJson(Map json) => Message( + factory Message.fromJson(Map json) => Message.fromParts( parts: (json[_Json.parts] as List) .map((p) => Part.fromJson(p as Map)) .toList(), From 87cc5ddbbe9d587d14865f245ec898f0e84e8fda Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 18:56:27 -0800 Subject: [PATCH 10/34] - --- packages/genai_primitives/lib/src/message.dart | 2 +- .../test/genai_primitives_test.dart | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/genai_primitives/lib/src/message.dart b/packages/genai_primitives/lib/src/message.dart index 8eeff62e2..d40bb1dd5 100644 --- a/packages/genai_primitives/lib/src/message.dart +++ b/packages/genai_primitives/lib/src/message.dart @@ -18,7 +18,7 @@ class Message { /// Creates a new message. const Message.fromParts({this.parts = const [], this.metadata = const {}}); - /// Creates a new message. + /// Creates a new message, taking a text string as separate parameter. Message( String text, { List parts = const [], diff --git a/packages/genai_primitives/test/genai_primitives_test.dart b/packages/genai_primitives/test/genai_primitives_test.dart index ab8e1a874..e687c418e 100644 --- a/packages/genai_primitives/test/genai_primitives_test.dart +++ b/packages/genai_primitives/test/genai_primitives_test.dart @@ -197,13 +197,13 @@ void main() { }); group('Message', () { - test('default constructor', () { - final fromParts = const Message(parts: [TextPart('hello')]); + test('fromParts', () { + final fromParts = const Message.fromParts(parts: [TextPart('hello')]); expect(fromParts.text, equals('hello')); }); - test('factories', () { - final message = Message.text('instructions'); + test('default constructor', () { + final message = Message('instructions'); expect(message.text, equals('instructions')); }); @@ -219,14 +219,14 @@ void main() { result: 'ok', ); - final msg1 = Message(parts: [const TextPart('Hi'), toolCall]); + final msg1 = Message.fromParts(parts: [const TextPart('Hi'), toolCall]); expect(msg1.hasToolCalls, isTrue); expect(msg1.hasToolResults, isFalse); expect(msg1.toolCalls, hasLength(1)); expect(msg1.toolResults, isEmpty); expect(msg1.text, equals('Hi')); - final msg2 = Message(parts: [toolResult]); + final msg2 = Message.fromParts(parts: [toolResult]); expect(msg2.hasToolCalls, isFalse); expect(msg2.hasToolResults, isTrue); expect(msg2.toolCalls, isEmpty); @@ -234,7 +234,7 @@ void main() { }); test('metadata', () { - final msg = const Message( + final msg = const Message.fromParts( parts: [TextPart('hi')], metadata: {'key': 'value'}, ); @@ -248,7 +248,7 @@ void main() { }); test('JSON serialization', () { - final msg = Message.text('response'); + final msg = Message('response'); final Map json = msg.toJson(); expect((json['parts'] as List).length, equals(1)); From 724ba2b94fa4f7a2aac352bb34e462be2ae12494 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 19:00:01 -0800 Subject: [PATCH 11/34] - --- packages/genai_primitives/example/main.dart | 4 ++++ packages/genai_primitives/test/example_test.dart | 1 + 2 files changed, 5 insertions(+) create mode 100644 packages/genai_primitives/test/example_test.dart diff --git a/packages/genai_primitives/example/main.dart b/packages/genai_primitives/example/main.dart index 1ac39a7f4..254dfff3e 100644 --- a/packages/genai_primitives/example/main.dart +++ b/packages/genai_primitives/example/main.dart @@ -24,6 +24,10 @@ class ChatMessage { ChatMessage.model(Message content) : this(role: Role.model, content: content); } +void runExample() { + print('--- GenAI Primitives Example ---'); +} + void main() { print('--- GenAI Primitives Example ---'); diff --git a/packages/genai_primitives/test/example_test.dart b/packages/genai_primitives/test/example_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/genai_primitives/test/example_test.dart @@ -0,0 +1 @@ + From 2ea87b58373c9f14ae39c9633a20561254801265 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 19:07:18 -0800 Subject: [PATCH 12/34] - --- packages/genai_primitives/example/main.dart | 8 +- .../genai_primitives/test/example_test.dart | 17 ++ .../test/genai_primitives_test.dart | 188 +++++++++++++++++- 3 files changed, 208 insertions(+), 5 deletions(-) diff --git a/packages/genai_primitives/example/main.dart b/packages/genai_primitives/example/main.dart index 254dfff3e..c1c727153 100644 --- a/packages/genai_primitives/example/main.dart +++ b/packages/genai_primitives/example/main.dart @@ -26,10 +26,6 @@ class ChatMessage { void runExample() { print('--- GenAI Primitives Example ---'); -} - -void main() { - print('--- GenAI Primitives Example ---'); // 1. Define a Tool final ToolDefinition getWeatherTool = ToolDefinition( @@ -148,3 +144,7 @@ void main() { ).convert(history.map((m) => m.content.toJson()).toList()), ); } + +void main() { + runExample(); +} diff --git a/packages/genai_primitives/test/example_test.dart b/packages/genai_primitives/test/example_test.dart index 8b1378917..ef3b1fa8d 100644 --- a/packages/genai_primitives/test/example_test.dart +++ b/packages/genai_primitives/test/example_test.dart @@ -1 +1,18 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:typed_data'; + +import 'package:cross_file/cross_file.dart'; +import 'package:genai_primitives/genai_primitives.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; +import 'package:test/test.dart'; + +import '../example/main.dart'; + +void main() { + test('runExample', () { + runExample(); + }); +} diff --git a/packages/genai_primitives/test/genai_primitives_test.dart b/packages/genai_primitives/test/genai_primitives_test.dart index e687c418e..e9b882c9a 100644 --- a/packages/genai_primitives/test/genai_primitives_test.dart +++ b/packages/genai_primitives/test/genai_primitives_test.dart @@ -4,11 +4,63 @@ import 'dart:typed_data'; +import 'package:cross_file/cross_file.dart'; import 'package:genai_primitives/genai_primitives.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; import 'package:test/test.dart'; void main() { + group('Part', () { + test('mimeType helper', () { + // Test with extensions (may be environment dependent for text/plain) + expect( + Part.mimeType('test.png'), + anyOf(equals('image/png'), equals('application/octet-stream')), + ); + + // Test with header bytes (sniffing should be environment independent) + final pngHeader = Uint8List.fromList([ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + ]); + expect( + Part.mimeType('unknown', headerBytes: pngHeader), + equals('image/png'), + ); + + final pdfHeader = Uint8List.fromList([0x25, 0x50, 0x44, 0x46]); + expect( + Part.mimeType('file', headerBytes: pdfHeader), + equals('application/pdf'), + ); + }); + + test('nameFromMimeType helper', () { + expect(Part.nameFromMimeType('image/png'), equals('image.png')); + expect(Part.nameFromMimeType('application/pdf'), equals('file.pdf')); + expect(Part.nameFromMimeType('unknown/type'), equals('file.bin')); + }); + + test('extensionFromMimeType helper', () { + expect(Part.extensionFromMimeType('image/png'), equals('png')); + expect(Part.extensionFromMimeType('application/pdf'), equals('pdf')); + expect(Part.extensionFromMimeType('unknown/type'), isNull); + }); + + test('fromJson throws on unknown type', () { + expect( + () => Part.fromJson({'type': 'Unknown', 'content': ''}), + throwsUnimplementedError, + ); + }); + }); + group('MessagePart', () { group('TextPart', () { test('creation', () { @@ -75,6 +127,49 @@ void main() { expect(dataPart.name, equals('test.png')); expect(dataPart.bytes, equals(bytes)); }); + + test('fromFile creation', () async { + final bytes = Uint8List.fromList([ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + ]); + final file = XFile.fromData( + bytes, + mimeType: 'image/png', + name: 'my_file.png', + ); + + final part = await DataPart.fromFile(file); + expect(part.bytes, equals(bytes)); + expect(part.mimeType, equals('image/png')); + // XFile.fromData might not preserve the name in some test environments + expect(part.name, anyOf(equals('my_file.png'), equals('image.png'))); + }); + + test('fromFile with unknown MIME type detection', () async { + // PNG header + final bytes = Uint8List.fromList([ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + ]); + final file = XFile.fromData(bytes, name: 'temp_file.png'); + + final part = await DataPart.fromFile(file); + expect(part.mimeType, equals('image/png')); + expect(part.name, anyOf(equals('temp_file.png'), equals('image.png'))); + }); }); group('LinkPart', () { @@ -156,6 +251,32 @@ void main() { expect(toolPart.callId, equals('call_1')); expect(toolPart.arguments, equals({'city': 'London'})); }); + + test('toString', () { + const part = ToolPart.call( + callId: 'c1', + toolName: 't1', + arguments: {'a': 1}, + ); + expect(part.toString(), contains('ToolPart.call')); + expect(part.toString(), contains('c1')); + }); + + test('argumentsRaw', () { + const part1 = ToolPart.call( + callId: 'c1', + toolName: 't1', + arguments: {}, + ); + expect(part1.argumentsRaw, equals('{}')); + + const part2 = ToolPart.call( + callId: 'c2', + toolName: 't2', + arguments: {'a': 1}, + ); + expect(part2.argumentsRaw, equals('{"a":1}')); + }); }); group('Result', () { @@ -170,6 +291,17 @@ void main() { expect(part.toolName, equals('get_weather')); expect(part.result, equals({'temp': 20})); expect(part.arguments, isNull); + expect(part.argumentsRaw, equals('')); + }); + + test('toString', () { + const part = ToolPart.result( + callId: 'c1', + toolName: 't1', + result: 'ok', + ); + expect(part.toString(), contains('ToolPart.result')); + expect(part.toString(), contains('c1')); }); test('JSON serialization', () { @@ -256,6 +388,60 @@ void main() { final reconstructed = Message.fromJson(json); expect(reconstructed, equals(msg)); }); + + test('equality and hashCode', () { + const msg1 = Message.fromParts( + parts: [TextPart('hi')], + metadata: {'k': 'v'}, + ); + const msg2 = Message.fromParts( + parts: [TextPart('hi')], + metadata: {'k': 'v'}, + ); + const msg3 = Message.fromParts(parts: [TextPart('hello')]); + const msg4 = Message.fromParts( + parts: [TextPart('hi')], + metadata: {'k': 'other'}, + ); + + expect(msg1, equals(msg2)); + expect(msg1.hashCode, equals(msg2.hashCode)); + expect(msg1, isNot(equals(msg3))); + expect(msg1, isNot(equals(msg4))); + }); + + test('text concatenation', () { + final msg = Message.fromParts( + parts: [ + const TextPart('Part 1. '), + const ToolPart.call(callId: '1', toolName: 't', arguments: {}), + const TextPart('Part 2.'), + ], + ); + expect(msg.text, equals('Part 1. Part 2.')); + }); + + test('toString', () { + final msg = Message('hi'); + expect(msg.toString(), contains('Message')); + expect(msg.toString(), contains('parts: [TextPart(hi)]')); + }); + }); + + group('MessagePartHelpers', () { + test('extension methods', () { + final parts = [ + const TextPart('Hello'), + const ToolPart.call(callId: 'c1', toolName: 't1', arguments: {}), + const ToolPart.result(callId: 'c2', toolName: 't2', result: 'r'), + ]; + + expect(parts.text, equals('Hello')); + expect(parts.toolCalls, hasLength(1)); + expect(parts.toolCalls.first.callId, equals('c1')); + expect(parts.toolResults, hasLength(1)); + expect(parts.toolResults.first.callId, equals('c2')); + }); }); group('ToolDefinition', () { @@ -274,7 +460,7 @@ void main() { expect(json['inputSchema'], isNotNull); // Since we don't have a fromJson in ToolDefinition (yet?), we just test - // serialization If we needed it, we would add it. For now, testing that + // serialization. If we needed it, we would add it. For now, testing that // it produces expected map structure. final schemaMap = json['inputSchema'] as Map; expect(schemaMap['type'], equals('object')); From 5887cc49fae3040036ad5ab6885a7ff2233b6c49 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 21:27:56 -0800 Subject: [PATCH 13/34] - --- packages/genai_primitives/example/main.dart | 26 ++++++++----------- .../genai_primitives/test/example_test.dart | 14 +++++----- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/genai_primitives/example/main.dart b/packages/genai_primitives/example/main.dart index c1c727153..a17fabcc3 100644 --- a/packages/genai_primitives/example/main.dart +++ b/packages/genai_primitives/example/main.dart @@ -24,8 +24,8 @@ class ChatMessage { ChatMessage.model(Message content) : this(role: Role.model, content: content); } -void runExample() { - print('--- GenAI Primitives Example ---'); +void main({void Function(Object? object) output = print}) { + output('--- GenAI Primitives Example ---'); // 1. Define a Tool final ToolDefinition getWeatherTool = ToolDefinition( @@ -45,8 +45,8 @@ void runExample() { ), ); - print('\n[Tool Definition]'); - print(const JsonEncoder.withIndent(' ').convert(getWeatherTool.toJson())); + output('\n[Tool Definition]'); + output(const JsonEncoder.withIndent(' ').convert(getWeatherTool.toJson())); // 2. Create a conversation history final history = [ @@ -105,8 +105,8 @@ void runExample() { ); history.add(toolResult); - print('\n[Tool Result]'); - print('Result: ${toolResult.content.toolResults.first.result}'); + output('\n[Tool Result]'); + output('Result: ${toolResult.content.toolResults.first.result}'); // 5. Simulate Final Model Response with Data (e.g. an image generated or // returned) @@ -124,27 +124,23 @@ void runExample() { ); history.add(finalResponse); - print('\n[Final Model Response with Data]'); - print('Text: ${finalResponse.content.text}'); + output('\n[Final Model Response with Data]'); + output('Text: ${finalResponse.content.text}'); if (finalResponse.content.parts.any((p) => p is DataPart)) { final DataPart dataPart = finalResponse.content.parts .whereType() .first; - print( + output( 'Attachment: ${dataPart.name} ' '(${dataPart.mimeType}, ${dataPart.bytes.length} bytes)', ); } // 6. Demonstrate JSON serialization of the whole history - print('\n[Full History JSON]'); - print( + output('\n[Full History JSON]'); + output( const JsonEncoder.withIndent( ' ', ).convert(history.map((m) => m.content.toJson()).toList()), ); } - -void main() { - runExample(); -} diff --git a/packages/genai_primitives/test/example_test.dart b/packages/genai_primitives/test/example_test.dart index ef3b1fa8d..55b431a6f 100644 --- a/packages/genai_primitives/test/example_test.dart +++ b/packages/genai_primitives/test/example_test.dart @@ -2,17 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:typed_data'; - -import 'package:cross_file/cross_file.dart'; -import 'package:genai_primitives/genai_primitives.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; import 'package:test/test.dart'; -import '../example/main.dart'; +import '../example/main.dart' as example; void main() { test('runExample', () { - runExample(); + final List output = []; + void expectOutput(Object? object) { + output.add(object.toString()); + } + + example.main(output: expectOutput); }); } From f6af0837e59adc9488d0428fcb02ace003061f78 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 21:46:50 -0800 Subject: [PATCH 14/34] - --- packages/genai_primitives/example/main.dart | 8 +- .../genai_primitives/test/example_test.dart | 136 +++++++++++++++++- 2 files changed, 135 insertions(+), 9 deletions(-) diff --git a/packages/genai_primitives/example/main.dart b/packages/genai_primitives/example/main.dart index a17fabcc3..e3b750d1d 100644 --- a/packages/genai_primitives/example/main.dart +++ b/packages/genai_primitives/example/main.dart @@ -62,9 +62,9 @@ void main({void Function(Object? object) output = print}) { ChatMessage.user(Message('What is the weather in London?')), ]; - print('\n[Initial Conversation]'); + output('\n[Initial Conversation]'); for (final msg in history) { - print('${msg.role.name}: ${msg.content.text}'); + output('${msg.role.name}: ${msg.content.text}'); } // 3. Simulate Model Response with Tool Call @@ -83,10 +83,10 @@ void main({void Function(Object? object) output = print}) { ); history.add(modelResponse); - print('\n[Model Response with Tool Call]'); + output('\n[Model Response with Tool Call]'); if (modelResponse.content.hasToolCalls) { for (final ToolPart call in modelResponse.content.toolCalls) { - print('Tool Call: ${call.toolName}(${call.arguments})'); + output('Tool Call: ${call.toolName}(${call.arguments})'); } } diff --git a/packages/genai_primitives/test/example_test.dart b/packages/genai_primitives/test/example_test.dart index 55b431a6f..05d65c724 100644 --- a/packages/genai_primitives/test/example_test.dart +++ b/packages/genai_primitives/test/example_test.dart @@ -8,11 +8,137 @@ import '../example/main.dart' as example; void main() { test('runExample', () { - final List output = []; - void expectOutput(Object? object) { - output.add(object.toString()); - } + final output = StringBuffer(); + example.main(output: (object) => output.writeln(object.toString())); - example.main(output: expectOutput); + // If the test fails update expected output, and check diff for this file. + expect(output.toString(), _expectedOutput); }); } + +const _expectedOutput = ''' +--- GenAI Primitives Example --- + +[Tool Definition] +{ + "name": "get_weather", + "description": "Get the current weather for a location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "description": "The unit of temperature", + "enum": [ + "celsius", + "fahrenheit" + ] + } + }, + "required": [ + "location" + ] + } +} + +[Initial Conversation] +system: You are a helpful weather assistant. Use the get_weather tool when needed. +user: What is the weather in London? + +[Model Response with Tool Call] +Tool Call: get_weather({location: London, unit: celsius}) + +[Tool Result] +Result: {temperature: 15, condition: Cloudy} + +[Final Model Response with Data] +Text: Here is a chart of the weather trend: +Attachment: weather_chart.png (image/png, 4 bytes) + +[Full History JSON] +[ + { + "parts": [ + { + "type": "Text", + "content": "You are a helpful weather assistant. Use the get_weather tool when needed." + } + ], + "metadata": {} + }, + { + "parts": [ + { + "type": "Text", + "content": "What is the weather in London?" + } + ], + "metadata": {} + }, + { + "parts": [ + { + "type": "Text", + "content": "" + }, + { + "type": "Text", + "content": "Thinking: User wants weather for London..." + }, + { + "type": "Tool", + "content": { + "id": "call_123", + "name": "get_weather", + "arguments": { + "location": "London", + "unit": "celsius" + } + } + } + ], + "metadata": {} + }, + { + "parts": [ + { + "type": "Text", + "content": "" + }, + { + "type": "Tool", + "content": { + "id": "call_123", + "name": "get_weather", + "result": { + "temperature": 15, + "condition": "Cloudy" + } + } + } + ], + "metadata": {} + }, + { + "parts": [ + { + "type": "Text", + "content": "Here is a chart of the weather trend:" + }, + { + "type": "Data", + "content": { + "name": "weather_chart.png", + "mimeType": "image/png", + "bytes": "data:image/png;base64,iVBORw==" + } + } + ], + "metadata": {} + } +] +'''; From f259308cf5bcc193b4715924ae5ef310e48907fd Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 21:54:39 -0800 Subject: [PATCH 15/34] Update genai_primitives_test.dart --- .../genai_primitives/test/genai_primitives_test.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/genai_primitives/test/genai_primitives_test.dart b/packages/genai_primitives/test/genai_primitives_test.dart index e9b882c9a..c657880fa 100644 --- a/packages/genai_primitives/test/genai_primitives_test.dart +++ b/packages/genai_primitives/test/genai_primitives_test.dart @@ -145,7 +145,7 @@ void main() { name: 'my_file.png', ); - final part = await DataPart.fromFile(file); + final DataPart part = await DataPart.fromFile(file); expect(part.bytes, equals(bytes)); expect(part.mimeType, equals('image/png')); // XFile.fromData might not preserve the name in some test environments @@ -166,7 +166,7 @@ void main() { ]); final file = XFile.fromData(bytes, name: 'temp_file.png'); - final part = await DataPart.fromFile(file); + final DataPart part = await DataPart.fromFile(file); expect(part.mimeType, equals('image/png')); expect(part.name, anyOf(equals('temp_file.png'), equals('image.png'))); }); @@ -411,11 +411,11 @@ void main() { }); test('text concatenation', () { - final msg = Message.fromParts( + final msg = const Message.fromParts( parts: [ - const TextPart('Part 1. '), - const ToolPart.call(callId: '1', toolName: 't', arguments: {}), - const TextPart('Part 2.'), + TextPart('Part 1. '), + ToolPart.call(callId: '1', toolName: 't', arguments: {}), + TextPart('Part 2.'), ], ); expect(msg.text, equals('Part 1. Part 2.')); From 5110939ec85051635a452bdaede15d20d2542e23 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 21:58:40 -0800 Subject: [PATCH 16/34] - --- packages/genai_primitives/lib/src/message.dart | 3 +-- packages/genai_primitives/lib/src/utils.dart | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/genai_primitives/lib/src/message.dart b/packages/genai_primitives/lib/src/message.dart index d40bb1dd5..c66844f03 100644 --- a/packages/genai_primitives/lib/src/message.dart +++ b/packages/genai_primitives/lib/src/message.dart @@ -78,8 +78,7 @@ class Message { } @override - int get hashCode => - Object.hash(Object.hashAll(parts), Object.hashAll(metadata.entries)); + int get hashCode => Object.hash(Object.hashAll(parts), mapHashCode(metadata)); @override String toString() => 'Message(parts: $parts, metadata: $metadata)'; diff --git a/packages/genai_primitives/lib/src/utils.dart b/packages/genai_primitives/lib/src/utils.dart index 49c97998c..103c45c16 100644 --- a/packages/genai_primitives/lib/src/utils.dart +++ b/packages/genai_primitives/lib/src/utils.dart @@ -20,3 +20,12 @@ bool mapEquals(Map? a, Map? b) { } return true; } + +int mapHashCode(Map? map) { + if (map == null) return 0; + var hash = 0; + for (final MapEntry entry in map.entries) { + hash ^= Object.hash(entry.key, entry.value); + } + return hash; +} From 31e666a1e73d255a9a8bf9a4857690402c5c2f8f Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 23 Dec 2025 22:01:25 -0800 Subject: [PATCH 17/34] Update message_parts.dart --- packages/genai_primitives/lib/src/message_parts.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/genai_primitives/lib/src/message_parts.dart b/packages/genai_primitives/lib/src/message_parts.dart index 58a315713..a0f7eed0c 100644 --- a/packages/genai_primitives/lib/src/message_parts.dart +++ b/packages/genai_primitives/lib/src/message_parts.dart @@ -94,7 +94,7 @@ abstract class Part { /// Gets the name for a MIME type. static String nameFromMimeType(String mimeType) { - final String ext = extensionFromMimeType(mimeType) ?? '.bin'; + final String ext = extensionFromMimeType(mimeType) ?? 'bin'; return mimeType.startsWith('image/') ? 'image.$ext' : 'file.$ext'; } From 6a245e860561ee833b10510020995840840d967d Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Fri, 26 Dec 2025 13:51:21 -0800 Subject: [PATCH 18/34] - --- packages/genai_primitives/example/main.dart | 10 ++++++---- pubspec.lock | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/genai_primitives/example/main.dart b/packages/genai_primitives/example/main.dart index e3b750d1d..0436ce1dd 100644 --- a/packages/genai_primitives/example/main.dart +++ b/packages/genai_primitives/example/main.dart @@ -14,14 +14,16 @@ class ChatMessage { final Role role; final Message content; - ChatMessage({required this.role, required this.content}); + const ChatMessage({required this.role, required this.content}); - ChatMessage.system(Message content) + const ChatMessage.system(Message content) : this(role: Role.system, content: content); - ChatMessage.user(Message content) : this(role: Role.user, content: content); + const ChatMessage.user(Message content) + : this(role: Role.user, content: content); - ChatMessage.model(Message content) : this(role: Role.model, content: content); + const ChatMessage.model(Message content) + : this(role: Role.model, content: content); } void main({void Function(Object? object) output = print}) { diff --git a/pubspec.lock b/pubspec.lock index 6d541136a..8f7efff02 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: From 52eb3a7a1dfe28f71a3bb821c16506a56fb690b3 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Fri, 26 Dec 2025 13:57:05 -0800 Subject: [PATCH 19/34] Update message_parts.dart --- .../lib/src/message_parts.dart | 84 ++++++++++--------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/packages/genai_primitives/lib/src/message_parts.dart b/packages/genai_primitives/lib/src/message_parts.dart index a0f7eed0c..497a0a5ad 100644 --- a/packages/genai_primitives/lib/src/message_parts.dart +++ b/packages/genai_primitives/lib/src/message_parts.dart @@ -43,46 +43,52 @@ abstract class Part { factory Part.fromJson(Map json) { final Object? type = json[_Json.type]; - return switch (type) { - _Part.text => TextPart(json[_Json.content] as String), - _Part.data => () { - final content = json[_Json.content] as Map; - final dataUri = content[_Json.bytes] as String; - final Uri uri = Uri.parse(dataUri); - return DataPart( - uri.data!.contentAsBytes(), - mimeType: content[_Json.mimeType] as String, - name: content[_Json.name] as String?, - ); - }(), - _Part.link => () { - final content = json[_Json.content] as Map; - return LinkPart( - Uri.parse(content[_Json.url] as String), - mimeType: content[_Json.mimeType] as String?, - name: content[_Json.name] as String?, - ); - }(), - _Part.tool => () { - final content = json[_Json.content] as Map; - // Check if it's a call or result based on presence of - // arguments or result - if (content.containsKey(_Json.arguments)) { - return ToolPart.call( - callId: content[_Json.id] as String, - toolName: content[_Json.name] as String, - arguments: content[_Json.arguments] as Map? ?? {}, + switch (type) { + case _Part.text: + return TextPart(json[_Json.content] as String); + case _Part.data: + { + final content = json[_Json.content] as Map; + final dataUri = content[_Json.bytes] as String; + final Uri uri = Uri.parse(dataUri); + return DataPart( + uri.data!.contentAsBytes(), + mimeType: content[_Json.mimeType] as String, + name: content[_Json.name] as String?, ); - } else { - return ToolPart.result( - callId: content[_Json.id] as String, - toolName: content[_Json.name] as String, - result: content[_Json.result], + } + case _Part.link: + { + final content = json[_Json.content] as Map; + return LinkPart( + Uri.parse(content[_Json.url] as String), + mimeType: content[_Json.mimeType] as String?, + name: content[_Json.name] as String?, ); } - }(), - _ => throw UnimplementedError('Unknown part type: $type'), - }; + case _Part.tool: + { + final content = json[_Json.content] as Map; + // Check if it's a call or result based on presence of + // arguments or result + if (content.containsKey(_Json.arguments)) { + return ToolPart.call( + callId: content[_Json.id] as String, + toolName: content[_Json.name] as String, + arguments: + content[_Json.arguments] as Map? ?? {}, + ); + } else { + return ToolPart.result( + callId: content[_Json.id] as String, + toolName: content[_Json.name] as String, + result: content[_Json.result], + ); + } + } + default: + throw UnimplementedError('Unknown part type: $type'); + } } /// The default MIME type for binary data. @@ -300,9 +306,7 @@ class ToolPart extends Part { final dynamic result; /// The arguments as a JSON string. - String get argumentsRaw => arguments != null - ? (arguments!.isEmpty ? '{}' : jsonEncode(arguments)) - : ''; + String get argumentsRaw => arguments == null ? '' : jsonEncode(arguments); @override bool operator ==(Object other) { From af0d06bda1a7c77879f205bcb56e16687ba7cbff Mon Sep 17 00:00:00 2001 From: Andrew Kolos Date: Sun, 4 Jan 2026 19:23:58 -0500 Subject: [PATCH 20/34] ci: add workflow_dispatch to enable manual CI triggers (#633) --- .github/workflows/flutter_packages.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter_packages.yaml b/.github/workflows/flutter_packages.yaml index be8fd6871..64cefd2aa 100644 --- a/.github/workflows/flutter_packages.yaml +++ b/.github/workflows/flutter_packages.yaml @@ -7,6 +7,7 @@ name: Flutter GenUI CI on: + workflow_dispatch: push: branches: - main From 4ebab33c76e502b0f39615bfb70c5f5a056c1534 Mon Sep 17 00:00:00 2001 From: jacobsimionato Date: Tue, 6 Jan 2026 04:49:03 +1030 Subject: [PATCH 21/34] Add A2UI support section to README (#648) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index a5047fff9..8044c4be2 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,12 @@ graph TD genui_google_generative_ai --> genui ``` +## A2UI Support + +The Flutter Gen UI SDK uses the [A2UI protocol](https://a2ui.org) to represent UI content internally. The [genui_a2ui](packages/genui_a2ui/) package allows it to act as a renderer for UIs generated by an A2UI backend agent, similar to the [other A2UI renderers](https://github.com/google/A2UI/tree/main/renderers) which are maintained within the A2UI repository. + +The Flutter Gen UI SDK currently supports A2UI v0.8. + ## Getting started See the [genui getting started guide](packages/genui/README.md#getting-started-with-genui). From 0d4e92d3fc607caf15f7b0b227c22cfcb63796a0 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 5 Jan 2026 12:04:28 -0800 Subject: [PATCH 22/34] Update dartantic_ai dependency (#649) --- packages/genui_dartantic/CHANGELOG.md | 3 +- packages/genui_dartantic/pubspec.yaml | 2 +- pubspec.lock | 54 +++++++++++++++------------ pubspec.yaml | 5 --- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/packages/genui_dartantic/CHANGELOG.md b/packages/genui_dartantic/CHANGELOG.md index fa193e769..c794715a6 100644 --- a/packages/genui_dartantic/CHANGELOG.md +++ b/packages/genui_dartantic/CHANGELOG.md @@ -2,7 +2,8 @@ ## 0.6.1 -- **Feature**: Re-introduced package to monorepo with `DartanticContentGenerator` (#583, #624). +- Updated `pubspec.yaml` to use the latest version of `dartantic_ai` (2.2.0) +- Re-introduced package to monorepo with `DartanticContentGenerator` (#583, #624). ## 0.6.0 diff --git a/packages/genui_dartantic/pubspec.yaml b/packages/genui_dartantic/pubspec.yaml index 2ed372a04..07119e606 100644 --- a/packages/genui_dartantic/pubspec.yaml +++ b/packages/genui_dartantic/pubspec.yaml @@ -16,7 +16,7 @@ environment: flutter: ">=3.35.7 <4.0.0" dependencies: - dartantic_ai: ">=2.0.3 <2.1.0" # TODO(#637): Pinned due to a breakage in latest (2.1.1) due to a downstream breaking change in mistral_ai 0.1.1+. + dartantic_ai: ^2.2.0 flutter: sdk: flutter genui: ^0.6.0 diff --git a/pubspec.lock b/pubspec.lock index 8f7efff02..cdcf6c752 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.11.1" collection: dependency: transitive description: @@ -245,10 +245,10 @@ packages: dependency: transitive description: name: dartantic_ai - sha256: a3d89d1c3d639dee220cdaab7a9793f7b0eaa6e9b1f1749a65776be2a9baeb70 + sha256: dd1ec8203e8af44ecb549fb21a638676afb0ab5003c8894c1f31c5ac4a1d83a9 url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.2.0" dartantic_interface: dependency: transitive description: @@ -455,10 +455,10 @@ packages: dependency: transitive description: name: flutter_markdown_plus - sha256: a3335b1047d4cbdcd20819cf69d9f2ac0e334ae13420104fb6035da1b404a0fa + sha256: "039177906850278e8fb1cd364115ee0a46281135932fa8ecea8455522166d2de" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.7" flutter_math_fork: dependency: transitive description: @@ -735,6 +735,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" json_annotation: dependency: transitive description: @@ -811,26 +819,26 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" mcp_dart: dependency: transitive description: name: mcp_dart - sha256: "436566d733fd1b9cfaeda148756596cd3e77b755f75df2d576128b55bdbc61e0" + sha256: c44fe3b5cfcc0010a3ef6875ccfc5ec3bcfce158ddab0f7f898cef9d55eea9d9 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" meta: dependency: transitive description: @@ -848,13 +856,13 @@ packages: source: hosted version: "2.0.0" mistralai_dart: - dependency: "direct overridden" + dependency: transitive description: name: mistralai_dart - sha256: "479b1a26a4613d1fcf28df27c5c27f9fa6052291a12cfaf26867a349a15dda20" + sha256: "46e2679228468d3a3a7bbcda35e3e5bc53e9c0fe51de51ae31cdfc817dc1756d" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.1+1" mockito: dependency: transitive description: @@ -907,10 +915,10 @@ packages: dependency: transitive description: name: openai_dart - sha256: "0c392263f5aeadf93c9bef0ce9f4781f4ce45de4e4b84858d5508148dfbfd637" + sha256: "037605a210cb3b1d8ac72b11a4ace26f25ee9267aaf981d2af1d7f0524adcbf5" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.2" package_config: dependency: transitive description: @@ -1224,26 +1232,26 @@ packages: dependency: transitive description: name: test - sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.28.0" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4 + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.14" + version: "0.6.12" tuple: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6b650f2b3..029ee15de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,8 +30,3 @@ workspace: flutter: uses-material-design: true - -# Pin mistralai_dart to 0.1.1 (avoid 0.1.1+1 which has breaking API changes). -# dartantic_ai 2.1.1 is not compatible with mistralai_dart 0.1.1+1. -dependency_overrides: - mistralai_dart: 0.1.1 From 910f4c577ac0141f3d9e7eb205f967c9dd5db69c Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 5 Jan 2026 12:11:10 -0800 Subject: [PATCH 23/34] Post publish commit to add sections to CHANGELOGs (#650) --- packages/genui/CHANGELOG.md | 2 ++ packages/genui_a2ui/CHANGELOG.md | 2 ++ packages/genui_dartantic/CHANGELOG.md | 2 ++ packages/genui_firebase_ai/CHANGELOG.md | 2 ++ .../genui_google_generative_ai/CHANGELOG.md | 2 ++ pubspec.lock | 28 +++++++------------ 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/genui/CHANGELOG.md b/packages/genui/CHANGELOG.md index b8f2cb642..1600e16cf 100644 --- a/packages/genui/CHANGELOG.md +++ b/packages/genui/CHANGELOG.md @@ -1,5 +1,7 @@ # `genui` Changelog +## 0.6.2 (in progress) + ## 0.6.1 - **Fix**: Corrected `DateTimeInput` catalog item JSON key mapping (#622). diff --git a/packages/genui_a2ui/CHANGELOG.md b/packages/genui_a2ui/CHANGELOG.md index b65efc8ad..3a19af820 100644 --- a/packages/genui_a2ui/CHANGELOG.md +++ b/packages/genui_a2ui/CHANGELOG.md @@ -1,5 +1,7 @@ # `genui_a2ui` Changelog +## 0.6.2 (in progress) + ## 0.6.1 - **Refactor**: Switched to using a local implementation of the A2A client library, removing the dependency on `package:a2a` (#627). diff --git a/packages/genui_dartantic/CHANGELOG.md b/packages/genui_dartantic/CHANGELOG.md index c794715a6..21fb86187 100644 --- a/packages/genui_dartantic/CHANGELOG.md +++ b/packages/genui_dartantic/CHANGELOG.md @@ -1,5 +1,7 @@ # `genui_dartantic` Changelog +## 0.6.2 (in progress) + ## 0.6.1 - Updated `pubspec.yaml` to use the latest version of `dartantic_ai` (2.2.0) diff --git a/packages/genui_firebase_ai/CHANGELOG.md b/packages/genui_firebase_ai/CHANGELOG.md index db99ffcbb..b7e2887ae 100644 --- a/packages/genui_firebase_ai/CHANGELOG.md +++ b/packages/genui_firebase_ai/CHANGELOG.md @@ -1,5 +1,7 @@ # `genui_firebase_ai` Changelog +## 0.6.2 (in progress) + ## 0.6.1 ## 0.6.0 diff --git a/packages/genui_google_generative_ai/CHANGELOG.md b/packages/genui_google_generative_ai/CHANGELOG.md index 91362c7fd..1658d1db8 100644 --- a/packages/genui_google_generative_ai/CHANGELOG.md +++ b/packages/genui_google_generative_ai/CHANGELOG.md @@ -1,5 +1,7 @@ # `genui_google_generative_ai` Changelog +## 0.6.2 (in progress) + ## 0.6.1 - **Fix**: Ensure bytes are not null when creating Blob in content converter. diff --git a/pubspec.lock b/pubspec.lock index cdcf6c752..6d5bc3d59 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -735,14 +735,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: transitive description: @@ -819,18 +811,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" mcp_dart: dependency: transitive description: @@ -1232,26 +1224,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.28.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.8" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4 url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.14" tuple: dependency: transitive description: From 4bf48a47287c0346f31629aaba5e43b5493cf487 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Tue, 6 Jan 2026 14:32:54 -0600 Subject: [PATCH 24/34] [docs] Suggest using `flutter pub add` for adding dependencies (#645) --- .../docs/create_a_custom_catalogitem.md | 11 +++---- packages/genui/.guides/setup.md | 33 ++++++++----------- packages/genui/README.md | 29 ++++++---------- packages/genui_a2ui/README.md | 15 +++------ packages/genui_google_generative_ai/README.md | 7 +++- 5 files changed, 39 insertions(+), 56 deletions(-) diff --git a/packages/genui/.guides/docs/create_a_custom_catalogitem.md b/packages/genui/.guides/docs/create_a_custom_catalogitem.md index 66629ec96..aa56e4443 100644 --- a/packages/genui/.guides/docs/create_a_custom_catalogitem.md +++ b/packages/genui/.guides/docs/create_a_custom_catalogitem.md @@ -8,14 +8,13 @@ description: | Follow these steps to create your own, custom widgets and make them available to the agent for generation. -## 1. Import `json_schema_builder` +## 1. Depend on `json_schema_builder` -Add the `json_schema_builder` package as a dependency in `pubspec.yaml`. Use the -same commit reference as the one for `genui`. +Use `flutter pub add` to add `json_schema_builder` as a dependency in +your `pubspec.yaml` file: -```yaml -dependencies: - json_schema_builder: ^0.1.3 +```bash +flutter pub add json_schema_builder ``` ## 2. Create the new widget's schema diff --git a/packages/genui/.guides/setup.md b/packages/genui/.guides/setup.md index 83cdecc82..4cc0bced6 100644 --- a/packages/genui/.guides/setup.md +++ b/packages/genui/.guides/setup.md @@ -40,14 +40,11 @@ Logic, follow these instructions: [Firebase's Flutter Setup guide](https://firebase.google.com/docs/flutter/setup) to add Firebase to your app. Run `flutterfire configure` to configure your app. -4. In `pubspec.yaml`, add `genui` and `genui_firebase_ai` to the - `dependencies` section. As of this writing, it's best to use pub's git - dependency to refer directly to this project's source. - - ```yaml - dependencies: - genui: ^0.5.1 - genui_firebase_ai: ^0.5.1 +4. Use `flutter pub add` to add the `genui` and `genui_firebase_ai` packages as + dependencies in your `pubspec.yaml` file: + + ```bash + flutter pub add genui genui_firebase_ai ``` 5. In your app's `main` method, ensure that the widget bindings are initialized, @@ -66,13 +63,11 @@ Logic, follow these instructions: To use `genui` with a generic agent provider that supports the A2UI protocol, use the `genui_a2ui` package. -1. In `pubspec.yaml`, add `genui` and `genui_a2ui` to the `dependencies` - section. +1. Use `flutter pub add` to add the `genui` and `genui_a2ui` packages as + dependencies in your `pubspec.yaml` file: - ```yaml - dependencies: - genui: ^0.5.1 - genui_a2ui: ^0.5.1 + ```bash + flutter pub add genui genui_a2ui ``` 2. Use the `A2uiContentGenerator` to connect to your agent provider. @@ -82,13 +77,11 @@ use the `genui_a2ui` package. To use `genui` with the Google Generative AI API, use the `genui_google_generative_ai` package. -1. In `pubspec.yaml`, add `genui` and `genui_google_generative_ai` to the - `dependencies` section. +1. Use `flutter pub add` to add the `genui` and `genui_google_generative_ai` packages as + dependencies in your `pubspec.yaml` file: - ```yaml - dependencies: - genui: ^0.5.1 - genui_google_generative_ai: ^0.5.1 + ```bash + flutter pub add genui genui_google_generative_ai ``` 2. Use the `GoogleGenerativeAiContentGenerator` to connect to the Google diff --git a/packages/genui/README.md b/packages/genui/README.md index d3639c66d..9cb013f2d 100644 --- a/packages/genui/README.md +++ b/packages/genui/README.md @@ -97,14 +97,11 @@ Logic, follow these instructions: 3. Follow the first three steps in [Firebase's Flutter Setup guide](https://firebase.google.com/docs/flutter/setup) to add Firebase to your app. -4. In `pubspec.yaml`, add `genui` and `genui_firebase_ai` to the - `dependencies` section. - - ```yaml - dependencies: - # ... - genui: 0.5.0 - genui_firebase_ai: 0.5.0 +4. Use `flutter pub add` to add the `genui` and `genui_firebase_ai` packages as + dependencies in your `pubspec.yaml` file: + + ```bash + flutter pub add genui genui_firebase_ai ``` 5. In your app's `main` method, ensure that the widget bindings are initialized, @@ -298,19 +295,13 @@ In addition to using the catalog of widgets in `CoreCatalogItems`, you can create custom widgets for the agent to generate. Use the following instructions. -#### Import `json_schema_builder` - -Add the `json_schema_builder` package as a dependency in `pubspec.yaml`. Use the -same commit reference as the one for `genui`. +#### Depend on the `json_schema_builder` package -```yaml -dependencies: - # ... - json_schema_builder: - git: - url: https://github.com/flutter/genui.git - path: packages/json_schema_builder +Use `flutter pub add` to add `json_schema_builder` as a dependency in +your `pubspec.yaml` file: +```bash +flutter pub add json_schema_builder ``` #### Create the new widget's schema diff --git a/packages/genui_a2ui/README.md b/packages/genui_a2ui/README.md index aa2193488..4edfeb378 100644 --- a/packages/genui_a2ui/README.md +++ b/packages/genui_a2ui/README.md @@ -20,17 +20,12 @@ An integration package for [`genui`](https://pub.dev/packages/genui) and the [A2 ### Installation -Add the following to your `pubspec.yaml`: - -```yaml -dependencies: - flutter: - sdk: flutter - genui: ^0.6.0 # Or the latest version - genui_a2ui: ^0.6.0 # Or the latest version -``` +Use `flutter pub add` to add the latest versions of `genui` and `genui_a2ui` as +dependencies in your `pubspec.yaml` file: -Then run `flutter pub get`. +```bash +flutter pub add genui genui_a2ui +``` ### Basic Usage diff --git a/packages/genui_google_generative_ai/README.md b/packages/genui_google_generative_ai/README.md index 05b128216..e02d284c7 100644 --- a/packages/genui_google_generative_ai/README.md +++ b/packages/genui_google_generative_ai/README.md @@ -14,7 +14,12 @@ To use this package, you will need a Gemini API key. If you don't already have o ### Installation -Add this package to your `pubspec.yaml`: "genui_google_generative_ai" +Use `flutter pub add` to add the latest versions of `genui` and `genui_google_generative_ai` as +dependencies in your `pubspec.yaml` file: + +```bash +flutter pub add genui genui_google_generative_ai +``` ### Usage From 9f14e622f3679784f9898d122ed7739d1c89428a Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Tue, 6 Jan 2026 23:44:22 +0000 Subject: [PATCH 25/34] Improve error handling for catalog example loading (#653) Wraps the example data parsing and component creation in a try-catch block to capture and rethrow errors with additional context (the item name). This makes it easier to identify which catalog item has invalid example data. --- .../development_utilities/catalog_view.dart | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/genui/lib/src/development_utilities/catalog_view.dart b/packages/genui/lib/src/development_utilities/catalog_view.dart index 699f5ea37..c40b901e5 100644 --- a/packages/genui/lib/src/development_utilities/catalog_view.dart +++ b/packages/genui/lib/src/development_utilities/catalog_view.dart @@ -67,30 +67,39 @@ class _DebugCatalogViewState extends State { final surfaceId = '${item.name}$indexPart'; final String exampleJsonString = exampleBuilder(); - final exampleData = jsonDecode(exampleJsonString) as List; - final List components = exampleData - .map((e) => Component.fromJson(e as JsonMap)) - .toList(); + try { + final exampleData = jsonDecode(exampleJsonString) as List; - Component? rootComponent; - rootComponent = components.firstWhereOrNull((c) => c.id == 'root'); + final List components = exampleData + .map((e) => Component.fromJson(e as JsonMap)) + .toList(); - if (rootComponent == null) { - debugPrint( - 'Skipping example for ${item.name} because it is missing a root ' - 'component.', + Component? rootComponent; + rootComponent = components.firstWhereOrNull((c) => c.id == 'root'); + + if (rootComponent == null) { + debugPrint( + 'Skipping example for ${item.name} because it is missing a root ' + 'component.', + ); + continue; + } + + _a2uiMessageProcessor.handleMessage( + SurfaceUpdate(surfaceId: surfaceId, components: components), + ); + _a2uiMessageProcessor.handleMessage( + BeginRendering(surfaceId: surfaceId, root: rootComponent.id), + ); + surfaceIds.add(surfaceId); + } catch (e, s) { + debugPrint('Failed to load example for "${item.name}":\n$e\n$s'); + throw Exception( + 'Failed to load example for "${item.name}". Check logs for ' + 'details.', ); - continue; } - - _a2uiMessageProcessor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), - ); - _a2uiMessageProcessor.handleMessage( - BeginRendering(surfaceId: surfaceId, root: rootComponent.id), - ); - surfaceIds.add(surfaceId); } } } From c29fca4fc72c97d525a49f5454ef52263694cfa0 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 8 Jan 2026 16:12:34 -0800 Subject: [PATCH 26/34] - --- packages/genai_primitives/example/main.dart | 10 +-- .../genai_primitives/lib/src/message.dart | 71 ++++++++++++------- .../lib/src/message_parts.dart | 36 ++++++---- .../lib/src/tool_definition.dart | 4 +- packages/genai_primitives/lib/src/utils.dart | 31 -------- packages/genai_primitives/pubspec.lock | 2 +- packages/genai_primitives/pubspec.yaml | 1 + .../test/genai_primitives_test.dart | 35 ++++----- 8 files changed, 88 insertions(+), 102 deletions(-) delete mode 100644 packages/genai_primitives/lib/src/utils.dart diff --git a/packages/genai_primitives/example/main.dart b/packages/genai_primitives/example/main.dart index 0436ce1dd..09d56afd5 100644 --- a/packages/genai_primitives/example/main.dart +++ b/packages/genai_primitives/example/main.dart @@ -54,14 +54,14 @@ void main({void Function(Object? object) output = print}) { final history = [ // System message ChatMessage.system( - Message( + Message.fromText( 'You are a helpful weather assistant. ' 'Use the get_weather tool when needed.', ), ), // User message asking for weather - ChatMessage.user(Message('What is the weather in London?')), + ChatMessage.user(Message.fromText('What is the weather in London?')), ]; output('\n[Initial Conversation]'); @@ -71,7 +71,7 @@ void main({void Function(Object? object) output = print}) { // 3. Simulate Model Response with Tool Call final modelResponse = ChatMessage.model( - Message( + Message.fromText( '', // Empty text for tool call parts: [ const TextPart('Thinking: User wants weather for London...'), @@ -94,7 +94,7 @@ void main({void Function(Object? object) output = print}) { // 4. Simulate Tool Execution & Result final toolResult = ChatMessage.user( - Message( + Message.fromText( '', // User role is typically used for tool results in many APIs parts: [ const ToolPart.result( @@ -113,7 +113,7 @@ void main({void Function(Object? object) output = print}) { // 5. Simulate Final Model Response with Data (e.g. an image generated or // returned) final finalResponse = ChatMessage.model( - Message( + Message.fromText( 'Here is a chart of the weather trend:', parts: [ DataPart( diff --git a/packages/genai_primitives/lib/src/message.dart b/packages/genai_primitives/lib/src/message.dart index c66844f03..7455a0569 100644 --- a/packages/genai_primitives/lib/src/message.dart +++ b/packages/genai_primitives/lib/src/message.dart @@ -2,48 +2,68 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'message_parts.dart'; -import 'utils.dart'; final class _Json { static const parts = 'parts'; static const metadata = 'metadata'; } -/// A message in a conversation between a user and a model. +/// A message between participants of the interaction. @immutable class Message { /// Creates a new message. - const Message.fromParts({this.parts = const [], this.metadata = const {}}); - - /// Creates a new message, taking a text string as separate parameter. - Message( + /// + /// If `parts` or `metadata` is not provided, an empty collections are used. + /// + /// If there is no parts of type [TextPart], the [text] property + /// will be empty. + /// + /// If there are many parts of type [TextPart], the [text] property + /// will be a concatenation of all of them. + /// Many text parts is convenient to have to support + /// streaming of the message. + const Message({this.parts = const [], this.metadata = const {}}); + + /// Creates text message. + /// + /// Converts [text] to a [TextPart] and puts it as a single member of + /// the [parts] list. + Message.fromText( String text, { List parts = const [], - Map metadata = const {}, - }) : this.fromParts(parts: [TextPart(text), ...parts], metadata: metadata); + Map metadata = const {}, + }) : this(parts: [TextPart(text), ...parts], metadata: metadata); - /// Creates a message from a JSON-compatible map. - factory Message.fromJson(Map json) => Message.fromParts( - parts: (json[_Json.parts] as List) - .map((p) => Part.fromJson(p as Map)) + /// Deserializes a message seriealized with [toJson]. + factory Message.fromJson(Map json) => Message( + parts: (json[_Json.parts] as List) + .map((p) => Part.fromJson(p as Map)) .toList(), - metadata: (json[_Json.metadata] as Map?) ?? const {}, + metadata: (json[_Json.metadata] as Map?) ?? const {}, ); + /// Serializes the message. + Map toJson() => { + _Json.parts: parts.map((p) => p.toJson()).toList(), + _Json.metadata: metadata, + }; + /// The content parts of the message. final List parts; /// Optional metadata associated with this message. - /// Can include information like suppressed content, warnings, etc. - final Map metadata; + /// + /// This can include information like suppressed content, warnings, etc. + final Map metadata; - /// Gets the text content of the message by concatenating all text parts. + /// Concatenated [TextPart] parts. String get text => parts.whereType().map((p) => p.text).join(); - /// Checks if this message contains any tool calls. + /// Whether this message contains any tool calls. bool get hasToolCalls => parts.whereType().any((p) => p.kind == ToolPartKind.call); @@ -53,7 +73,7 @@ class Message { .where((p) => p.kind == ToolPartKind.call) .toList(); - /// Checks if this message contains any tool results. + /// Whether this message contains any tool results. bool get hasToolResults => parts.whereType().any((p) => p.kind == ToolPartKind.result); @@ -63,22 +83,19 @@ class Message { .where((p) => p.kind == ToolPartKind.result) .toList(); - /// Converts the message to a JSON-compatible map. - Map toJson() => { - _Json.parts: parts.map((p) => p.toJson()).toList(), - _Json.metadata: metadata, - }; - @override bool operator ==(Object other) { if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + + final deepEquality = const DeepCollectionEquality(); return other is Message && - listEquals(other.parts, parts) && - mapEquals(other.metadata, metadata); + deepEquality.equals(other.parts, parts) && + deepEquality.equals(other.metadata, metadata); } @override - int get hashCode => Object.hash(Object.hashAll(parts), mapHashCode(metadata)); + int get hashCode => Object.hashAll([parts, metadata]); @override String toString() => 'Message(parts: $parts, metadata: $metadata)'; diff --git a/packages/genai_primitives/lib/src/message_parts.dart b/packages/genai_primitives/lib/src/message_parts.dart index 497a0a5ad..80e667475 100644 --- a/packages/genai_primitives/lib/src/message_parts.dart +++ b/packages/genai_primitives/lib/src/message_parts.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:cross_file/cross_file.dart' show XFile; import 'package:meta/meta.dart'; import 'package:mime/mime.dart'; @@ -12,8 +13,6 @@ import 'package:mime/mime.dart'; import 'package:mime/src/default_extension_map.dart'; import 'package:path/path.dart' as p; -import 'utils.dart'; - final class _Json { static const type = 'type'; static const content = 'content'; @@ -35,12 +34,12 @@ final class _Part { /// Base class for message content parts. @immutable -abstract class Part { +sealed class Part { /// Creates a new part. const Part(); /// Creates a part from a JSON-compatible map. - factory Part.fromJson(Map json) { + factory Part.fromJson(Map json) { final Object? type = json[_Json.type]; switch (type) { @@ -48,7 +47,7 @@ abstract class Part { return TextPart(json[_Json.content] as String); case _Part.data: { - final content = json[_Json.content] as Map; + final content = json[_Json.content] as Map; final dataUri = content[_Json.bytes] as String; final Uri uri = Uri.parse(dataUri); return DataPart( @@ -59,7 +58,7 @@ abstract class Part { } case _Part.link: { - final content = json[_Json.content] as Map; + final content = json[_Json.content] as Map; return LinkPart( Uri.parse(content[_Json.url] as String), mimeType: content[_Json.mimeType] as String?, @@ -68,7 +67,7 @@ abstract class Part { } case _Part.tool: { - final content = json[_Json.content] as Map; + final content = json[_Json.content] as Map; // Check if it's a call or result based on presence of // arguments or result if (content.containsKey(_Json.arguments)) { @@ -76,7 +75,7 @@ abstract class Part { callId: content[_Json.id] as String, toolName: content[_Json.name] as String, arguments: - content[_Json.arguments] as Map? ?? {}, + content[_Json.arguments] as Map? ?? {}, ); } else { return ToolPart.result( @@ -116,7 +115,7 @@ abstract class Part { } /// Converts the part to a JSON-compatible map. - Map toJson() { + Map toJson() { final String typeName; final Object content; switch (this) { @@ -149,8 +148,6 @@ abstract class Part { if (p.result != null) _Json.result: p.result, }; break; - default: - throw UnimplementedError('Unknown part type: $runtimeType'); } return {_Json.type: typeName, _Json.content: content}; } @@ -168,6 +165,7 @@ class TextPart extends Part { @override bool operator ==(Object other) { if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; return other is TextPart && other.text == text; } @@ -225,8 +223,11 @@ class DataPart extends Part { @override bool operator ==(Object other) { if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + + final deepEquality = const DeepCollectionEquality(); return other is DataPart && - listEquals(other.bytes, bytes) && + deepEquality.equals(other.bytes, bytes) && other.mimeType == mimeType && other.name == name; } @@ -257,6 +258,8 @@ class LinkPart extends Part { @override bool operator ==(Object other) { if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is LinkPart && other.url == url && other.mimeType == mimeType && @@ -300,10 +303,10 @@ class ToolPart extends Part { final String toolName; /// The arguments for a tool call (null for results). - final Map? arguments; + final Map? arguments; /// The result of a tool execution (null for calls). - final dynamic result; + final Object? result; /// The arguments as a JSON string. String get argumentsRaw => arguments == null ? '' : jsonEncode(arguments); @@ -311,11 +314,14 @@ class ToolPart extends Part { @override bool operator ==(Object other) { if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + + final deepEquality = const DeepCollectionEquality(); return other is ToolPart && other.kind == kind && other.callId == callId && other.toolName == toolName && - mapEquals(other.arguments, arguments) && + deepEquality.equals(other.arguments, arguments) && other.result == result; } diff --git a/packages/genai_primitives/lib/src/tool_definition.dart b/packages/genai_primitives/lib/src/tool_definition.dart index 6bfd183e5..c413aef28 100644 --- a/packages/genai_primitives/lib/src/tool_definition.dart +++ b/packages/genai_primitives/lib/src/tool_definition.dart @@ -15,7 +15,7 @@ class ToolDefinition { inputSchema ?? Schema.fromMap({ 'type': 'object', - 'properties': {}, + 'properties': {}, }); /// The unique name of the tool that clearly communicates its purpose. @@ -30,7 +30,7 @@ class ToolDefinition { final Schema inputSchema; /// Converts the tool to a JSON-serializable map. - Map toJson() => { + Map toJson() => { 'name': name, 'description': description, 'inputSchema': inputSchema.value, diff --git a/packages/genai_primitives/lib/src/utils.dart b/packages/genai_primitives/lib/src/utils.dart deleted file mode 100644 index 103c45c16..000000000 --- a/packages/genai_primitives/lib/src/utils.dart +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Helper functions for equality checks -bool listEquals(List? a, List? b) { - if (a == null) return b == null; - if (b == null || a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (a[i] != b[i]) return false; - } - return true; -} - -bool mapEquals(Map? a, Map? b) { - if (a == null) return b == null; - if (b == null || a.length != b.length) return false; - for (final K key in a.keys) { - if (!b.containsKey(key) || a[key] != b[key]) return false; - } - return true; -} - -int mapHashCode(Map? map) { - if (map == null) return 0; - var hash = 0; - for (final MapEntry entry in map.entries) { - hash ^= Object.hash(entry.key, entry.value); - } - return hash; -} diff --git a/packages/genai_primitives/pubspec.lock b/packages/genai_primitives/pubspec.lock index 1edd9c015..349c0b8f6 100644 --- a/packages/genai_primitives/pubspec.lock +++ b/packages/genai_primitives/pubspec.lock @@ -66,7 +66,7 @@ packages: source: hosted version: "1.1.2" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" diff --git a/packages/genai_primitives/pubspec.yaml b/packages/genai_primitives/pubspec.yaml index c85671617..5a81adb7e 100644 --- a/packages/genai_primitives/pubspec.yaml +++ b/packages/genai_primitives/pubspec.yaml @@ -13,6 +13,7 @@ environment: sdk: ">=3.9.2 <4.0.0" dependencies: + collection: any cross_file: ^0.3.5+1 json_schema_builder: ^0.1.3 meta: ^1.17.0 diff --git a/packages/genai_primitives/test/genai_primitives_test.dart b/packages/genai_primitives/test/genai_primitives_test.dart index c657880fa..c546f262d 100644 --- a/packages/genai_primitives/test/genai_primitives_test.dart +++ b/packages/genai_primitives/test/genai_primitives_test.dart @@ -10,6 +10,8 @@ import 'package:json_schema_builder/json_schema_builder.dart'; import 'package:test/test.dart'; void main() { + // In this test dynamic is used instead of Object? + // to test support for dynamic types. group('Part', () { test('mimeType helper', () { // Test with extensions (may be environment dependent for text/plain) @@ -330,12 +332,12 @@ void main() { group('Message', () { test('fromParts', () { - final fromParts = const Message.fromParts(parts: [TextPart('hello')]); + final fromParts = const Message(parts: [TextPart('hello')]); expect(fromParts.text, equals('hello')); }); test('default constructor', () { - final message = Message('instructions'); + final message = Message.fromText('instructions'); expect(message.text, equals('instructions')); }); @@ -351,14 +353,14 @@ void main() { result: 'ok', ); - final msg1 = Message.fromParts(parts: [const TextPart('Hi'), toolCall]); + final msg1 = Message(parts: [const TextPart('Hi'), toolCall]); expect(msg1.hasToolCalls, isTrue); expect(msg1.hasToolResults, isFalse); expect(msg1.toolCalls, hasLength(1)); expect(msg1.toolResults, isEmpty); expect(msg1.text, equals('Hi')); - final msg2 = Message.fromParts(parts: [toolResult]); + final msg2 = Message(parts: [toolResult]); expect(msg2.hasToolCalls, isFalse); expect(msg2.hasToolResults, isTrue); expect(msg2.toolCalls, isEmpty); @@ -366,7 +368,7 @@ void main() { }); test('metadata', () { - final msg = const Message.fromParts( + final msg = const Message( parts: [TextPart('hi')], metadata: {'key': 'value'}, ); @@ -380,7 +382,7 @@ void main() { }); test('JSON serialization', () { - final msg = Message('response'); + final msg = Message.fromText('response'); final Map json = msg.toJson(); expect((json['parts'] as List).length, equals(1)); @@ -390,19 +392,10 @@ void main() { }); test('equality and hashCode', () { - const msg1 = Message.fromParts( - parts: [TextPart('hi')], - metadata: {'k': 'v'}, - ); - const msg2 = Message.fromParts( - parts: [TextPart('hi')], - metadata: {'k': 'v'}, - ); - const msg3 = Message.fromParts(parts: [TextPart('hello')]); - const msg4 = Message.fromParts( - parts: [TextPart('hi')], - metadata: {'k': 'other'}, - ); + const msg1 = Message(parts: [TextPart('hi')], metadata: {'k': 'v'}); + const msg2 = Message(parts: [TextPart('hi')], metadata: {'k': 'v'}); + const msg3 = Message(parts: [TextPart('hello')]); + const msg4 = Message(parts: [TextPart('hi')], metadata: {'k': 'other'}); expect(msg1, equals(msg2)); expect(msg1.hashCode, equals(msg2.hashCode)); @@ -411,7 +404,7 @@ void main() { }); test('text concatenation', () { - final msg = const Message.fromParts( + final msg = const Message( parts: [ TextPart('Part 1. '), ToolPart.call(callId: '1', toolName: 't', arguments: {}), @@ -422,7 +415,7 @@ void main() { }); test('toString', () { - final msg = Message('hi'); + final msg = Message.fromText('hi'); expect(msg.toString(), contains('Message')); expect(msg.toString(), contains('parts: [TextPart(hi)]')); }); From 56e12a801c6bc8a5718e32073ee58e6ec1a05d1f Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 8 Jan 2026 16:24:52 -0800 Subject: [PATCH 27/34] Update message.dart --- packages/genai_primitives/lib/src/message.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/genai_primitives/lib/src/message.dart b/packages/genai_primitives/lib/src/message.dart index 7455a0569..be8b66ff2 100644 --- a/packages/genai_primitives/lib/src/message.dart +++ b/packages/genai_primitives/lib/src/message.dart @@ -14,7 +14,7 @@ final class _Json { /// A message between participants of the interaction. @immutable -class Message { +final class Message { /// Creates a new message. /// /// If `parts` or `metadata` is not provided, an empty collections are used. From 36593a02c86b6c17aa48ee324bbcdb0e0f149664 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 8 Jan 2026 16:41:34 -0800 Subject: [PATCH 28/34] --- --- .../lib/src/message_parts.dart | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/packages/genai_primitives/lib/src/message_parts.dart b/packages/genai_primitives/lib/src/message_parts.dart index 80e667475..ef47efefd 100644 --- a/packages/genai_primitives/lib/src/message_parts.dart +++ b/packages/genai_primitives/lib/src/message_parts.dart @@ -115,42 +115,7 @@ sealed class Part { } /// Converts the part to a JSON-compatible map. - Map toJson() { - final String typeName; - final Object content; - switch (this) { - case final TextPart p: - typeName = _Part.text; - content = p.text; - break; - case final DataPart p: - typeName = _Part.data; - content = { - if (p.name != null) _Json.name: p.name, - _Json.mimeType: p.mimeType, - _Json.bytes: 'data:${p.mimeType};base64,${base64Encode(p.bytes)}', - }; - break; - case final LinkPart p: - typeName = _Part.link; - content = { - if (p.name != null) _Json.name: p.name, - if (p.mimeType != null) _Json.mimeType: p.mimeType, - _Json.url: p.url.toString(), - }; - break; - case final ToolPart p: - typeName = _Part.tool; - content = { - _Json.id: p.callId, - _Json.name: p.toolName, - if (p.arguments != null) _Json.arguments: p.arguments, - if (p.result != null) _Json.result: p.result, - }; - break; - } - return {_Json.type: typeName, _Json.content: content}; - } + Map toJson(); } /// A text part of a message. @@ -162,6 +127,12 @@ class TextPart extends Part { /// The text content. final String text; + @override + Map toJson() => { + _Json.type: _Part.text, + _Json.content: text, + }; + @override bool operator ==(Object other) { if (identical(this, other)) return true; @@ -220,6 +191,16 @@ class DataPart extends Part { /// Optional name for the data. final String? name; + @override + Map toJson() => { + _Json.type: _Part.data, + _Json.content: { + if (name != null) _Json.name: name, + _Json.mimeType: mimeType, + _Json.bytes: 'data:$mimeType;base64,${base64Encode(bytes)}', + }, + }; + @override bool operator ==(Object other) { if (identical(this, other)) return true; @@ -255,6 +236,16 @@ class LinkPart extends Part { /// Optional name for the link. final String? name; + @override + Map toJson() => { + _Json.type: _Part.link, + _Json.content: { + if (name != null) _Json.name: name, + if (mimeType != null) _Json.mimeType: mimeType, + _Json.url: url.toString(), + }, + }; + @override bool operator ==(Object other) { if (identical(this, other)) return true; @@ -311,6 +302,17 @@ class ToolPart extends Part { /// The arguments as a JSON string. String get argumentsRaw => arguments == null ? '' : jsonEncode(arguments); + @override + Map toJson() => { + _Json.type: _Part.tool, + _Json.content: { + _Json.id: callId, + _Json.name: toolName, + if (arguments != null) _Json.arguments: arguments, + if (result != null) _Json.result: result, + }, + }; + @override bool operator ==(Object other) { if (identical(this, other)) return true; From 167109f57ee29299cde0cad79380d760545e1a6b Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 8 Jan 2026 19:52:09 -0800 Subject: [PATCH 29/34] -- --- .../genai_primitives/lib/src/message.dart | 2 +- .../lib/src/message_parts.dart | 136 +++++++++++------- .../test/genai_primitives_test.dart | 12 +- 3 files changed, 92 insertions(+), 58 deletions(-) diff --git a/packages/genai_primitives/lib/src/message.dart b/packages/genai_primitives/lib/src/message.dart index be8b66ff2..63df91a8f 100644 --- a/packages/genai_primitives/lib/src/message.dart +++ b/packages/genai_primitives/lib/src/message.dart @@ -41,7 +41,7 @@ final class Message { /// Deserializes a message seriealized with [toJson]. factory Message.fromJson(Map json) => Message( parts: (json[_Json.parts] as List) - .map((p) => Part.fromJson(p as Map)) + .map((p) => const PartConverter().convert(p as Map)) .toList(), metadata: (json[_Json.metadata] as Map?) ?? const {}, ); diff --git a/packages/genai_primitives/lib/src/message_parts.dart b/packages/genai_primitives/lib/src/message_parts.dart index ef47efefd..208a82c16 100644 --- a/packages/genai_primitives/lib/src/message_parts.dart +++ b/packages/genai_primitives/lib/src/message_parts.dart @@ -34,60 +34,19 @@ final class _Part { /// Base class for message content parts. @immutable -sealed class Part { +abstract class Part { /// Creates a new part. const Part(); /// Creates a part from a JSON-compatible map. - factory Part.fromJson(Map json) { - final Object? type = json[_Json.type]; - - switch (type) { - case _Part.text: - return TextPart(json[_Json.content] as String); - case _Part.data: - { - final content = json[_Json.content] as Map; - final dataUri = content[_Json.bytes] as String; - final Uri uri = Uri.parse(dataUri); - return DataPart( - uri.data!.contentAsBytes(), - mimeType: content[_Json.mimeType] as String, - name: content[_Json.name] as String?, - ); - } - case _Part.link: - { - final content = json[_Json.content] as Map; - return LinkPart( - Uri.parse(content[_Json.url] as String), - mimeType: content[_Json.mimeType] as String?, - name: content[_Json.name] as String?, - ); - } - case _Part.tool: - { - final content = json[_Json.content] as Map; - // Check if it's a call or result based on presence of - // arguments or result - if (content.containsKey(_Json.arguments)) { - return ToolPart.call( - callId: content[_Json.id] as String, - toolName: content[_Json.name] as String, - arguments: - content[_Json.arguments] as Map? ?? {}, - ); - } else { - return ToolPart.result( - callId: content[_Json.id] as String, - toolName: content[_Json.name] as String, - result: content[_Json.result], - ); - } - } - default: - throw UnimplementedError('Unknown part type: $type'); - } + factory Part.fromJson( + Map json, { + JsonToPartConverter? customConverter, + }) { + return const PartConverter().convert( + json, + customConverter: customConverter, + ); } /// The default MIME type for binary data. @@ -118,6 +77,37 @@ sealed class Part { Map toJson(); } +typedef JsonToPartConverter = Converter, Part>; + +/// A converter that converts a JSON map to a [Part]. +@visibleForTesting +class PartConverter extends JsonToPartConverter { + const PartConverter(); + + @override + Part convert( + Map input, { + JsonToPartConverter? customConverter, + }) { + final Object? type = input[_Json.type]; + switch (type) { + case _Part.text: + return TextPart.fromJson(input); + case _Part.data: + return DataPart.fromJson(input); + case _Part.link: + return LinkPart.fromJson(input); + case _Part.tool: + return ToolPart.fromJson(input); + default: + if (customConverter == null) { + throw UnimplementedError('Unknown part type: $type'); + } + return customConverter.convert(input); + } + } +} + /// A text part of a message. @immutable class TextPart extends Part { @@ -127,6 +117,11 @@ class TextPart extends Part { /// The text content. final String text; + /// Creates a text part from a JSON-compatible map. + factory TextPart.fromJson(Map json) { + return TextPart(json[_Json.content] as String); + } + @override Map toJson() => { _Json.type: _Part.text, @@ -154,6 +149,18 @@ class DataPart extends Part { DataPart(this.bytes, {required this.mimeType, String? name}) : name = name ?? Part.nameFromMimeType(mimeType); + /// Creates a data part from a JSON-compatible map. + factory DataPart.fromJson(Map json) { + final content = json[_Json.content] as Map; + final dataUri = content[_Json.bytes] as String; + final Uri uri = Uri.parse(dataUri); + return DataPart( + uri.data!.contentAsBytes(), + mimeType: content[_Json.mimeType] as String, + name: content[_Json.name] as String?, + ); + } + /// Creates a data part from an [XFile]. static Future fromFile(XFile file) async { final Uint8List bytes = await file.readAsBytes(); @@ -236,6 +243,16 @@ class LinkPart extends Part { /// Optional name for the link. final String? name; + /// Creates a link part from a JSON-compatible map. + factory LinkPart.fromJson(Map json) { + final content = json[_Json.content] as Map; + return LinkPart( + Uri.parse(content[_Json.url] as String), + mimeType: content[_Json.mimeType] as String?, + name: content[_Json.name] as String?, + ); + } + @override Map toJson() => { _Json.type: _Part.link, @@ -267,7 +284,6 @@ class LinkPart extends Part { /// A tool interaction part of a message. @immutable class ToolPart extends Part { - /// Creates a tool call part. /// Creates a tool call part. const ToolPart.call({ required this.callId, @@ -302,6 +318,24 @@ class ToolPart extends Part { /// The arguments as a JSON string. String get argumentsRaw => arguments == null ? '' : jsonEncode(arguments); + /// Creates a tool part from a JSON-compatible map. + factory ToolPart.fromJson(Map json) { + final content = json[_Json.content] as Map; + if (content.containsKey(_Json.arguments)) { + return ToolPart.call( + callId: content[_Json.id] as String, + toolName: content[_Json.name] as String, + arguments: content[_Json.arguments] as Map? ?? {}, + ); + } else { + return ToolPart.result( + callId: content[_Json.id] as String, + toolName: content[_Json.name] as String, + result: content[_Json.result], + ); + } + } + @override Map toJson() => { _Json.type: _Part.tool, diff --git a/packages/genai_primitives/test/genai_primitives_test.dart b/packages/genai_primitives/test/genai_primitives_test.dart index c546f262d..b22ecf7e3 100644 --- a/packages/genai_primitives/test/genai_primitives_test.dart +++ b/packages/genai_primitives/test/genai_primitives_test.dart @@ -57,7 +57,7 @@ void main() { test('fromJson throws on unknown type', () { expect( - () => Part.fromJson({'type': 'Unknown', 'content': ''}), + () => const PartConverter().convert({'type': 'Unknown', 'content': ''}), throwsUnimplementedError, ); }); @@ -86,7 +86,7 @@ void main() { final Map json = part.toJson(); expect(json, equals({'type': 'Text', 'content': 'hello'})); - final reconstructed = Part.fromJson(json); + final Part reconstructed = const PartConverter().convert(json); expect(reconstructed, isA()); expect((reconstructed as TextPart).text, equals('hello')); }); @@ -122,7 +122,7 @@ void main() { expect(content['name'], equals('test.png')); expect(content['bytes'], startsWith('data:image/png;base64,')); - final reconstructed = Part.fromJson(json); + final Part reconstructed = const PartConverter().convert(json); expect(reconstructed, isA()); final dataPart = reconstructed as DataPart; expect(dataPart.mimeType, equals('image/png')); @@ -204,7 +204,7 @@ void main() { expect(content['mimeType'], equals('image/png')); expect(content['name'], equals('image')); - final reconstructed = Part.fromJson(json); + final Part reconstructed = const PartConverter().convert(json); expect(reconstructed, isA()); final linkPart = reconstructed as LinkPart; expect(linkPart.url, equals(uri)); @@ -246,7 +246,7 @@ void main() { isNull, ); // Ensures result is not present or null - final reconstructed = Part.fromJson(json); + final Part reconstructed = const PartConverter().convert(json); expect(reconstructed, isA()); final toolPart = reconstructed as ToolPart; expect(toolPart.kind, equals(ToolPartKind.call)); @@ -319,7 +319,7 @@ void main() { expect(content['name'], equals('get_weather')); expect(content['result'], equals({'temp': 20})); - final reconstructed = Part.fromJson(json); + final Part reconstructed = const PartConverter().convert(json); expect(reconstructed, isA()); final toolPart = reconstructed as ToolPart; expect(toolPart.kind, equals(ToolPartKind.result)); From dee7e6b5cee674cbe042019c2912904be33982c4 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 8 Jan 2026 19:59:06 -0800 Subject: [PATCH 30/34] - --- .../lib/src/message_parts.dart | 54 ++++++++++--------- .../test/genai_primitives_test.dart | 18 +++---- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/packages/genai_primitives/lib/src/message_parts.dart b/packages/genai_primitives/lib/src/message_parts.dart index 208a82c16..592e9d03a 100644 --- a/packages/genai_primitives/lib/src/message_parts.dart +++ b/packages/genai_primitives/lib/src/message_parts.dart @@ -49,30 +49,6 @@ abstract class Part { ); } - /// The default MIME type for binary data. - static const defaultMimeType = 'application/octet-stream'; - - /// Gets the MIME type for a file. - static String mimeType(String path, {Uint8List? headerBytes}) => - lookupMimeType(path, headerBytes: headerBytes) ?? defaultMimeType; - - /// Gets the name for a MIME type. - static String nameFromMimeType(String mimeType) { - final String ext = extensionFromMimeType(mimeType) ?? 'bin'; - return mimeType.startsWith('image/') ? 'image.$ext' : 'file.$ext'; - } - - /// Gets the extension for a MIME type. - static String? extensionFromMimeType(String mimeType) { - final String ext = defaultExtensionMap.entries - .firstWhere( - (e) => e.value == mimeType, - orElse: () => const MapEntry('', ''), - ) - .key; - return ext.isNotEmpty ? ext : null; - } - /// Converts the part to a JSON-compatible map. Map toJson(); } @@ -147,7 +123,7 @@ class TextPart extends Part { class DataPart extends Part { /// Creates a new data part. DataPart(this.bytes, {required this.mimeType, String? name}) - : name = name ?? Part.nameFromMimeType(mimeType); + : name = name ?? nameFromMimeType(mimeType); /// Creates a data part from a JSON-compatible map. factory DataPart.fromJson(Map json) { @@ -167,7 +143,7 @@ class DataPart extends Part { final String? name = _nameFromPath(file.path) ?? _emptyNull(file.name); final String mimeType = _emptyNull(file.mimeType) ?? - Part.mimeType( + mimeTypeForFile( name ?? '', headerBytes: Uint8List.fromList( bytes.take(defaultMagicNumbersMaxLength).toList(), @@ -226,6 +202,32 @@ class DataPart extends Part { @override String toString() => 'DataPart(mimeType: $mimeType, name: $name, bytes: ${bytes.length})'; + + static const defaultMimeType = 'application/octet-stream'; + + /// Gets the MIME type for a file. + @visibleForTesting + static String mimeTypeForFile(String path, {Uint8List? headerBytes}) => + lookupMimeType(path, headerBytes: headerBytes) ?? defaultMimeType; + + /// Gets the name for a MIME type. + @visibleForTesting + static String nameFromMimeType(String mimeType) { + final String ext = extensionFromMimeType(mimeType) ?? 'bin'; + return mimeType.startsWith('image/') ? 'image.$ext' : 'file.$ext'; + } + + /// Gets the extension for a MIME type. + @visibleForTesting + static String? extensionFromMimeType(String mimeType) { + final String ext = defaultExtensionMap.entries + .firstWhere( + (e) => e.value == mimeType, + orElse: () => const MapEntry('', ''), + ) + .key; + return ext.isNotEmpty ? ext : null; + } } /// A link part referencing external content. diff --git a/packages/genai_primitives/test/genai_primitives_test.dart b/packages/genai_primitives/test/genai_primitives_test.dart index b22ecf7e3..8a7b5979d 100644 --- a/packages/genai_primitives/test/genai_primitives_test.dart +++ b/packages/genai_primitives/test/genai_primitives_test.dart @@ -16,7 +16,7 @@ void main() { test('mimeType helper', () { // Test with extensions (may be environment dependent for text/plain) expect( - Part.mimeType('test.png'), + DataPart.mimeTypeForFile('test.png'), anyOf(equals('image/png'), equals('application/octet-stream')), ); @@ -32,27 +32,27 @@ void main() { 0x0A, ]); expect( - Part.mimeType('unknown', headerBytes: pngHeader), + DataPart.mimeTypeForFile('unknown', headerBytes: pngHeader), equals('image/png'), ); final pdfHeader = Uint8List.fromList([0x25, 0x50, 0x44, 0x46]); expect( - Part.mimeType('file', headerBytes: pdfHeader), + DataPart.mimeTypeForFile('file', headerBytes: pdfHeader), equals('application/pdf'), ); }); test('nameFromMimeType helper', () { - expect(Part.nameFromMimeType('image/png'), equals('image.png')); - expect(Part.nameFromMimeType('application/pdf'), equals('file.pdf')); - expect(Part.nameFromMimeType('unknown/type'), equals('file.bin')); + expect(DataPart.nameFromMimeType('image/png'), equals('image.png')); + expect(DataPart.nameFromMimeType('application/pdf'), equals('file.pdf')); + expect(DataPart.nameFromMimeType('unknown/type'), equals('file.bin')); }); test('extensionFromMimeType helper', () { - expect(Part.extensionFromMimeType('image/png'), equals('png')); - expect(Part.extensionFromMimeType('application/pdf'), equals('pdf')); - expect(Part.extensionFromMimeType('unknown/type'), isNull); + expect(DataPart.extensionFromMimeType('image/png'), equals('png')); + expect(DataPart.extensionFromMimeType('application/pdf'), equals('pdf')); + expect(DataPart.extensionFromMimeType('unknown/type'), isNull); }); test('fromJson throws on unknown type', () { From 1eb2b23337b9969beecb433f518132a05da73473 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 8 Jan 2026 20:04:33 -0800 Subject: [PATCH 31/34] - --- packages/genai_primitives/lib/src/message_parts.dart | 1 + .../genai_primitives/test/genai_primitives_test.dart | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/genai_primitives/lib/src/message_parts.dart b/packages/genai_primitives/lib/src/message_parts.dart index 592e9d03a..239d441e7 100644 --- a/packages/genai_primitives/lib/src/message_parts.dart +++ b/packages/genai_primitives/lib/src/message_parts.dart @@ -203,6 +203,7 @@ class DataPart extends Part { String toString() => 'DataPart(mimeType: $mimeType, name: $name, bytes: ${bytes.length})'; + @visibleForTesting static const defaultMimeType = 'application/octet-stream'; /// Gets the MIME type for a file. diff --git a/packages/genai_primitives/test/genai_primitives_test.dart b/packages/genai_primitives/test/genai_primitives_test.dart index 8a7b5979d..3951f2df8 100644 --- a/packages/genai_primitives/test/genai_primitives_test.dart +++ b/packages/genai_primitives/test/genai_primitives_test.dart @@ -55,6 +55,17 @@ void main() { expect(DataPart.extensionFromMimeType('unknown/type'), isNull); }); + test('defaultMimeType helper', () { + expect(DataPart.defaultMimeType, equals('application/octet-stream')); + }); + + test('uses defaultMimeType when unknown', () { + expect( + DataPart.mimeTypeForFile('unknown_file_no_extension'), + equals(DataPart.defaultMimeType), + ); + }); + test('fromJson throws on unknown type', () { expect( () => const PartConverter().convert({'type': 'Unknown', 'content': ''}), From b3c3014492c55ca4da4dbedbdd8eaac774bb2df2 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 8 Jan 2026 20:07:56 -0800 Subject: [PATCH 32/34] Create custom_part_test.dart --- .../test/custom_part_test.dart | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 packages/genai_primitives/test/custom_part_test.dart diff --git a/packages/genai_primitives/test/custom_part_test.dart b/packages/genai_primitives/test/custom_part_test.dart new file mode 100644 index 000000000..c0874e634 --- /dev/null +++ b/packages/genai_primitives/test/custom_part_test.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import 'package:genai_primitives/genai_primitives.dart'; +import 'package:test/test.dart'; + +// 1. Define a custom part class +class CustomPart extends Part { + final String customField; + + const CustomPart(this.customField); + + @override + Map toJson() { + return { + 'type': 'Custom', + 'content': {'customField': customField}, + }; + } + + @override + bool operator ==(Object other) => + other is CustomPart && other.customField == customField; + + @override + int get hashCode => customField.hashCode; + + @override + String toString() => 'CustomPart($customField)'; +} + +// 2. Define a custom converter +class CustomPartConverter extends Converter, Part> { + const CustomPartConverter(); + + @override + Part convert(Map input) { + if (input['type'] == 'Custom') { + final content = input['content'] as Map; + return CustomPart(content['customField'] as String); + } + throw UnimplementedError('Unknown custom part type: ${input['type']}'); + } +} + +void main() { + group('Custom Part Serialization', () { + test('round trip serialization with custom type', () { + const originalPart = CustomPart('custom_value'); + + // Serialize + final Map json = originalPart.toJson(); + expect(json['type'], equals('Custom')); + expect( + (json['content'] as Map)['customField'], + equals('custom_value'), + ); + + // Deserialize using Part.fromJson with customConverter + final reconstructedPart = Part.fromJson( + json, + customConverter: const CustomPartConverter(), + ); + + expect(reconstructedPart, isA()); + expect( + (reconstructedPart as CustomPart).customField, + equals('custom_value'), + ); + expect(reconstructedPart, equals(originalPart)); + }); + + test('Part.fromJson throws UnimplementedError for custom type', () { + final Map json = { + 'type': 'Custom', + 'content': {'customField': 'val'}, + }; + + expect(() => Part.fromJson(json), throwsUnimplementedError); + }); + + test('Part.fromJson handles standard types even with custom converter', () { + const textPart = TextPart('hello'); + final Map json = textPart.toJson(); + + // Should still work for standard parts + final reconstructed = Part.fromJson( + json, + customConverter: const CustomPartConverter(), + ); + + expect(reconstructed, equals(textPart)); + }); + }); +} From aebee60c5b0be029312bbb12092d38df362e876f Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 8 Jan 2026 20:11:39 -0800 Subject: [PATCH 33/34] Update custom_part_test.dart --- packages/genai_primitives/test/custom_part_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/genai_primitives/test/custom_part_test.dart b/packages/genai_primitives/test/custom_part_test.dart index c0874e634..4d1188814 100644 --- a/packages/genai_primitives/test/custom_part_test.dart +++ b/packages/genai_primitives/test/custom_part_test.dart @@ -1,3 +1,7 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:convert'; import 'package:genai_primitives/genai_primitives.dart'; From 62a421df0a7fbe798cba336789c891d7c909e11c Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 8 Jan 2026 20:18:01 -0800 Subject: [PATCH 34/34] - --- packages/genai_primitives/lib/src/message_parts.dart | 10 +++++----- packages/genai_primitives/test/custom_part_test.dart | 4 +--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/genai_primitives/lib/src/message_parts.dart b/packages/genai_primitives/lib/src/message_parts.dart index 239d441e7..0597b31d5 100644 --- a/packages/genai_primitives/lib/src/message_parts.dart +++ b/packages/genai_primitives/lib/src/message_parts.dart @@ -34,7 +34,7 @@ final class _Part { /// Base class for message content parts. @immutable -abstract class Part { +abstract base class Part { /// Creates a new part. const Part(); @@ -86,7 +86,7 @@ class PartConverter extends JsonToPartConverter { /// A text part of a message. @immutable -class TextPart extends Part { +base class TextPart extends Part { /// Creates a new text part. const TextPart(this.text); @@ -120,7 +120,7 @@ class TextPart extends Part { /// A data part containing binary data (e.g., images). @immutable -class DataPart extends Part { +base class DataPart extends Part { /// Creates a new data part. DataPart(this.bytes, {required this.mimeType, String? name}) : name = name ?? nameFromMimeType(mimeType); @@ -233,7 +233,7 @@ class DataPart extends Part { /// A link part referencing external content. @immutable -class LinkPart extends Part { +base class LinkPart extends Part { /// Creates a new link part. const LinkPart(this.url, {this.mimeType, this.name}); @@ -286,7 +286,7 @@ class LinkPart extends Part { /// A tool interaction part of a message. @immutable -class ToolPart extends Part { +base class ToolPart extends Part { /// Creates a tool call part. const ToolPart.call({ required this.callId, diff --git a/packages/genai_primitives/test/custom_part_test.dart b/packages/genai_primitives/test/custom_part_test.dart index 4d1188814..d51223861 100644 --- a/packages/genai_primitives/test/custom_part_test.dart +++ b/packages/genai_primitives/test/custom_part_test.dart @@ -7,8 +7,7 @@ import 'dart:convert'; import 'package:genai_primitives/genai_primitives.dart'; import 'package:test/test.dart'; -// 1. Define a custom part class -class CustomPart extends Part { +base class CustomPart extends Part { final String customField; const CustomPart(this.customField); @@ -32,7 +31,6 @@ class CustomPart extends Part { String toString() => 'CustomPart($customField)'; } -// 2. Define a custom converter class CustomPartConverter extends Converter, Part> { const CustomPartConverter();