Skip to content

Conversation

@polina-c
Copy link
Collaborator

@polina-c polina-c commented Dec 24, 2025

  1. Remove ChatMessageRole: this library should not enforce the set of roles, that can vary from case to case. The role should be part of envelope in client's code.
  2. Rename ChatMessage to Message, because this library can be used in chats and in other systems.
  3. Use constants for repeating literals
  4. Remove postfix 'Part' from part type literals, to reduce size of messages.
  5. Extend test coverage for primitives.
  6. Add test for example.
  7. fix case of null in nameFromMimeType

gemini-code-assist[bot]

This comment was marked as outdated.

@polina-c polina-c changed the title - Some cleanups to make the library useable for wide range of use cases. Dec 24, 2025
@polina-c polina-c changed the title Some cleanups to make the library useable for wide range of use cases. Updates to make the library applicable for wide range of use cases. Dec 24, 2025
@polina-c polina-c changed the title Updates to make the library applicable for wide range of use cases. Updates to make the library applicable for wider range of use cases. Dec 24, 2025
gemini-code-assist[bot]

This comment was marked as outdated.

@polina-c
Copy link
Collaborator Author

/gemini review

@polina-c polina-c marked this pull request as ready for review December 24, 2025 06:08
@polina-c
Copy link
Collaborator Author

polina-c commented Dec 24, 2025

@csells , how does it look?

Options for name for the class ChatMessage:

  1. Message
  2. MessageBody
  3. MessageContent
  4. Content

@polina-c polina-c changed the title Updates to make the library applicable for wider range of use cases. Updates to make the library applicable for wider range of use cases, and more stable. Dec 24, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several significant and beneficial changes to make the library more flexible and robust. Key changes include removing the enforced ChatMessageRole, renaming ChatMessage to Message, and using constants for JSON keys to reduce message size. The test coverage has been significantly improved, including a new test for the example, which is excellent. My review focuses on some opportunities to improve code clarity and leverage more modern Dart features, such as using const constructors for immutable classes and refactoring switch statements/expressions for better readability. Overall, this is a solid pull request that improves the library's design and usability.

Comment on lines +13 to +25
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This is a good example class to demonstrate how roles can be added by the client. To follow best practices for immutable data classes in Dart, you could make the constructors const. This allows them to be created as compile-time constants where possible, which can improve performance. This would be possible if the Message default constructor is also made const.

Suggested change
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);
}
class ChatMessage {
final Role role;
final Message content;
const ChatMessage({required this.role, required this.content});
const ChatMessage.system(Message content)
: this(role: Role.system, content: content);
const ChatMessage.user(Message content) : this(role: Role.user, content: content);
const ChatMessage.model(Message content) : this(role: Role.model, content: content);
}

Comment on lines +22 to +26
Message(
String text, {
List<Part> parts = const [],
Map<String, dynamic> metadata = const {},
}) : this.fromParts(parts: [TextPart(text), ...parts], metadata: metadata);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The default constructor for Message can be made const. This would allow for creating Message objects as compile-time constants, which is beneficial for performance and aligns with the immutable nature of the class.

Suggested change
Message(
String text, {
List<Part> parts = const [],
Map<String, dynamic> metadata = const {},
}) : this.fromParts(parts: [TextPart(text), ...parts], metadata: metadata);
const Message(
String text, {
List<Part> parts = const [],
Map<String, dynamic> metadata = const {},
}) : this.fromParts(parts: [TextPart(text), ...parts], metadata: metadata);

Comment on lines +43 to +86
factory Part.fromJson(Map<String, dynamic> 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<String, dynamic>;
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<String, dynamic>;
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<String, dynamic>;
// 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<String, dynamic>? ?? {},
);
} 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'),
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using immediately-invoked function expressions (IIFEs) inside a switch expression can be a bit hard to read. For better clarity and maintainability, consider refactoring this to a standard switch statement. This avoids the extra () { ... }() syntax and makes the logic for each case more straightforward.

  factory Part.fromJson(Map<String, dynamic> 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<String, dynamic>;
        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<String, dynamic>;
        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<String, dynamic>;
        // 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<String, dynamic>? ?? {},
          );
        } 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');
    }
  }

Comment on lines +265 to +279
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}'));
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These are good tests for argumentsRaw. While reviewing the implementation this test covers, I noticed a small opportunity for simplification. The check arguments!.isEmpty is redundant because jsonEncode({}) already produces '{}'. The implementation could be simplified to String get argumentsRaw => arguments != null ? jsonEncode(arguments) : '';.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant