Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/genai_primitives/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
TODO: describe initial version here
# `genai_primitives` Changelog

## 0.1.0

- Initial release
10 changes: 0 additions & 10 deletions packages/genai_primitives/example/example.dart

This file was deleted.

116 changes: 116 additions & 0 deletions packages/genai_primitives/example/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:genai_primitives/genai_primitives.dart';
import 'package:json_schema_builder/json_schema_builder.dart';

void main() {
print('--- GenAI Primitives Example ---');

// 1. Define a Tool
final ToolDefinition<Object> getWeatherTool = ToolDefinition(
name: 'get_weather',
description: 'Get the current weather for a location',
inputSchema: Schema.object(
properties: {
'location': Schema.string(
description: 'The city and state, e.g. San Francisco, CA',
),
'unit': Schema.string(
enumValues: ['celsius', 'fahrenheit'],
description: 'The unit of temperature',
),
},
required: ['location'],
),
);

print('\n[Tool Definition]');
print(const JsonEncoder.withIndent(' ').convert(getWeatherTool.toJson()));

// 2. Create a conversation history
final history = <ChatMessage>[
// System message
ChatMessage.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?'),
];

print('\n[Initial Conversation]');
for (final msg in history) {
print('${msg.role.name}: ${msg.text}');
}

// 3. Simulate Model Response with Tool Call
final modelResponse = ChatMessage.model(
'', // Empty text for tool call
parts: [
const TextPart('Thinking: User wants weather for London...'),
const ToolPart.call(
id: 'call_123',
name: '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) {
print('Tool Call: ${call.name}(${call.arguments})');
}
}

// 4. Simulate Tool Execution & Result
final toolResult = ChatMessage.user(
'', // User role is typically used for tool results in many APIs
parts: [
const ToolPart.result(
id: 'call_123',
name: 'get_weather',
result: {'temperature': 15, 'condition': 'Cloudy'},
),
],
);
history.add(toolResult);

print('\n[Tool Result]');
print('Result: ${toolResult.toolResults.first.result}');

// 5. Simulate Final Model Response with Data (e.g. an image generated or
// returned)
final finalResponse = ChatMessage.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',
),
],
);
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<DataPart>().first;
print(
'Attachment: ${dataPart.name} '
'(${dataPart.mimeType}, ${dataPart.bytes.length} bytes)',
);
}

// 6. Demonstrate JSON serialization of the whole history
print('\n[Full History JSON]');
print(
const JsonEncoder.withIndent(
' ',
).convert(history.map((m) => m.toJson()).toList()),
);
}
10 changes: 4 additions & 6 deletions packages/genai_primitives/lib/genai_primitives.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// Support for doing something awesome.
///
/// More dartdocs go here.
/// A set of primitives for working with generative AI.
library;

export 'src/genai_primitives_base.dart';

// TODO: Export any libraries intended for clients of this package.
export 'src/chat_message.dart';
export 'src/message_parts.dart';
export 'src/tool_definition.dart';
125 changes: 125 additions & 0 deletions packages/genai_primitives/lib/src/chat_message.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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<String, dynamic> json) => ChatMessage(
role: ChatMessageRole.values.byName(json['role'] as String),
parts: (json['parts'] as List<dynamic>)
.map((p) => Part.fromJson(p as Map<String, dynamic>))
.toList(),
metadata: json['metadata'] as Map<String, dynamic>,
);

/// Creates a system message.
factory ChatMessage.system(
String text, {
List<Part> parts = const [],
Map<String, dynamic>? metadata,
}) => ChatMessage(
role: ChatMessageRole.system,
parts: [TextPart(text), ...parts],
metadata: metadata ?? const {},
);

/// Creates a user message with text.
factory ChatMessage.user(
String text, {
List<Part> parts = const [],
Map<String, dynamic>? metadata,
}) => ChatMessage(
role: ChatMessageRole.user,
parts: [TextPart(text), ...parts],
metadata: metadata ?? const {},
);

/// Creates a model message with text.
factory ChatMessage.model(
String text, {
List<Part> parts = const [],
Map<String, dynamic>? 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<Part> parts;

/// Optional metadata associated with this message.
/// Can include information like suppressed content, warnings, etc.
final Map<String, dynamic> metadata;

/// Gets the text content of the message by concatenating all text parts.
String get text => parts.whereType<TextPart>().map((p) => p.text).join();

/// Checks if this message contains any tool calls.
bool get hasToolCalls =>
parts.whereType<ToolPart>().any((p) => p.kind == ToolPartKind.call);

/// Gets all tool calls in this message.
List<ToolPart> get toolCalls => parts
.whereType<ToolPart>()
.where((p) => p.kind == ToolPartKind.call)
.toList();

/// Checks if this message contains any tool results.
bool get hasToolResults =>
parts.whereType<ToolPart>().any((p) => p.kind == ToolPartKind.result);

/// Gets all tool results in this message.
List<ToolPart> get toolResults => parts
.whereType<ToolPart>()
.where((p) => p.kind == ToolPartKind.result)
.toList();

/// Converts the message to a JSON-compatible map.
Map<String, dynamic> toJson() => {
'role': role.name,
'parts': parts.map((p) => p.toJson()).toList(),
'metadata': metadata,
};

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ChatMessage &&
runtimeType == other.runtimeType &&
role == other.role &&
listEquals(parts, other.parts) &&
mapEquals(metadata, other.metadata);

@override
int get hashCode => role.hashCode ^ parts.hashCode ^ metadata.hashCode;

@override
String toString() =>
'Message(role: $role, parts: $parts, metadata: $metadata)';
}

/// The role of a message author.
enum ChatMessageRole {
Copy link
Collaborator

@polina-c polina-c Dec 18, 2025

Choose a reason for hiding this comment

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

Can you add more details to doc strings for enum and its items?

I can represent a confused user here:

  • doc for enum is saying that each item is a role of a message author, while doc for each item is describing type of message, not author

  • can you elaborate what is 'system'?

  • documentation is saying 'from', but not 'to'. Is it correct that system always send messages to model? If yes, should we specify it here? And the same about model: is it correct that model's messages are intended to be handled by system and then system will show to the user whatever is intended for user?

  • or my assumptions above are completely wrong and the enum relates to difference between 'user prompt' and 'system prompt' somehow?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe in this library instead of ChatMessage having field ChatMessageRole role, it make sense for it to have the field 'String type', so that every dependent can define their set of message types.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Another option:

class ChatMessage<T>{
  ...
  final T type;
  ...
}

Then users will be able to pass their enums for the message type. Maybe some of them will have more than one model and more than one system.

Copy link
Collaborator

@polina-c polina-c Dec 18, 2025

Choose a reason for hiding this comment

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

Or, we can remove type completely, so that users just derive from ChatMessage and add whatever fields they want to add. Or users can compose ChatMessage into another object, that contains envelope information.

This seems to be cleanest option for me.

/// A message from the system that sets context or instructions.
system,

/// A message from the end user.
user,

/// A message from the model.
model,
}
10 changes: 0 additions & 10 deletions packages/genai_primitives/lib/src/genai_primitives_base.dart

This file was deleted.

Loading
Loading