-
Notifications
You must be signed in to change notification settings - Fork 98
Updates to make the library applicable for wider range of use cases, and more stable. #644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
/gemini review |
|
@csells , how does it look? Options for name for the class ChatMessage:
|
There was a problem hiding this 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.
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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); | |
| } |
| Message( | ||
| String text, { | ||
| List<Part> parts = const [], | ||
| Map<String, dynamic> metadata = const {}, | ||
| }) : this.fromParts(parts: [TextPart(text), ...parts], metadata: metadata); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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); |
| 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'), | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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');
}
}| 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}')); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) : '';.
Uh oh!
There was an error while loading. Please reload this page.