Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions packages/stream_chat_flutter_ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@
extend `DefaultAITranslations`, so a string added in a later version is a compile error instead
of English silently showing up in your app. Existing subclasses of `DefaultAITranslations` keep
working.
- `AIMessagePartKind`, `AIReasoningStatus`, `AIToolCallStatus`, `AIToolExecutor`,
`AIToolApprovalDecision`, `AIMessageRole` and `AIMessageAttachmentKind` are extension types on
`String` instead of classes: compare or switch on them as strings, with no `.value`. All of them
now have `values` and `isKnown`.
Comment thread
xsahil03x marked this conversation as resolved.
Resolved
- Pie slices are labelled with their percentage; when the chart has a legend the name moves there.
- A pie's screen-reader summary lists its slice names.
- `ChartSemantics.isEmpty` is `true` for a pie with no positive slice.
Expand Down
4 changes: 2 additions & 2 deletions packages/stream_chat_flutter_ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ the user's answer, `AIToolApprovalDecision.allowed` or `.declined`, goes to your
late final approver = AIToolApprover(
identity: AIClientIdentity(userId: currentUser.id, clientId: installId),
// Throw to let the person answer again.
decide: (call, decision) => backend.answerToolCall(call.id, decision.value),
decide: (call, decision) => backend.answerToolCall(call.id, decision),
);

AIMessagePartsView(parts: parts, approver: approver);
Expand Down Expand Up @@ -947,7 +947,7 @@ List<AIMessageAttachment> get attachments => [
];

// In your AIComponentBuilders:
messageAttachment: (context, props) => switch (props.attachment.kind.value) {
messageAttachment: (context, props) => switch (props.attachment.kind) {
'athena_image' => ArtifactImage(
id: props.attachment.extraData['artifact_id']! as String,
revision: props.attachment.extraData['revision'] as int?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,20 @@ import 'package:stream_chat_flutter_ai/src/parts/ai_reasoning_part.dart';
/// Who wrote an [AIMessage].
///
/// An open set: compare against the roles you know ([user], [assistant]). A
/// message of any role other than [user] shows as the agent's.
@immutable
class AIMessageRole {
/// Wraps a raw role, such as `'system'`.
const AIMessageRole(this.value);

/// role this package doesn't know keeps its raw value and reports `false` from
/// [isKnown]; a message of any role other than [user] shows as the agent's.
extension type const AIMessageRole(String _) implements String {
/// The person using the app. Each of their messages starts a turn.
static const user = AIMessageRole('user');

/// The agent.
static const assistant = AIMessageRole('assistant');

/// The role's name.
final String value;

@override
bool operator ==(Object other) => identical(this, other) || other is AIMessageRole && other.value == value;
/// The roles this package knows.
static const values = [user, assistant];

@override
int get hashCode => value.hashCode;

@override
String toString() => 'AIMessageRole($value)';
/// Whether this is one of [values].
bool get isKnown => values.contains(this);
}

/// A message [AIConversationView] can show: the person's in a bubble with its
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,21 @@ import 'package:stream_chat_flutter_ai/src/conversation/ai_message_attachment_vi
/// What kind of attachment an [AIMessageAttachment] is.
///
/// An open set: compare against the kinds you know ([image], [file]) and give
/// the rest a fallback. [AIMessageAttachmentView] shows any kind but [image] as
/// a file.
@immutable
class AIMessageAttachmentKind {
/// Wraps a raw kind, such as one of your own.
const AIMessageAttachmentKind(this.value);

/// the rest a fallback. A kind this package doesn't know, such as one of your
/// own, keeps its raw value and reports `false` from [isKnown].
/// [AIMessageAttachmentView] shows any kind but [image] as a file.
extension type const AIMessageAttachmentKind(String _) implements String {
/// An image, shown as a thumbnail.
static const image = AIMessageAttachmentKind('image');

/// A file, such as a PDF, shown with its name, type and size.
static const file = AIMessageAttachmentKind('file');

/// The kind's name.
final String value;

@override
bool operator ==(Object other) => identical(this, other) || other is AIMessageAttachmentKind && other.value == value;
/// The kinds this package knows.
static const values = [image, file];

@override
int get hashCode => value.hashCode;

@override
String toString() => 'AIMessageAttachmentKind($value)';
/// Whether this is one of [values].
bool get isKnown => values.contains(this);
}

/// Something a message carries besides its text, such as an image or a file.
Expand Down
28 changes: 6 additions & 22 deletions packages/stream_chat_flutter_ai/lib/src/parts/ai_message_part.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,9 @@ typedef AIMessagePartAttachment = ({String type, Map<String, Object?> payload});
///
/// An open set: compare against the kinds you know ([reasoning], [toolCall])
/// and give the rest a fallback. A kind this package doesn't know β€” a newer
/// agent's, or one of your own β€” keeps its raw [value] and reports `false` from
/// agent's, or one of your own β€” keeps its raw value and reports `false` from
/// [isKnown].
@immutable
class AIMessagePartKind {
/// Wraps a raw attachment type.
const AIMessagePartKind(this.value);

extension type const AIMessagePartKind(String _) implements String {
/// A round of the model's reasoning (`ai_reasoning`).
static const reasoning = AIMessagePartKind('ai_reasoning');

Expand All @@ -46,20 +42,8 @@ class AIMessagePartKind {
/// The kinds this package knows.
static const values = [reasoning, toolCall];

/// The attachment type, such as `ai_reasoning`.
final String value;

/// Whether this is one of [values].
bool get isKnown => values.contains(this);

@override
bool operator ==(Object other) => identical(this, other) || other is AIMessagePartKind && other.value == value;

@override
int get hashCode => value.hashCode;

@override
String toString() => 'AIMessagePartKind($value)';
}

/// One step an AI agent took while replying, such as a round of reasoning or a
Expand Down Expand Up @@ -179,7 +163,7 @@ class AIMessagePart {
if (!type.startsWith(typePrefix)) return null;
final payloadId = PartFields(payload, type).string('id');
final id = payloadId ?? '$type-$position';
if (payloadId == null && type == AIMessagePartKind.toolCall.value) {
if (payloadId == null && type == AIMessagePartKind.toolCall) {
reportPartIssue(() => '$type at step $position has no `id`, so it can never be answered or run.');
}
return AIMessagePart(
Expand Down Expand Up @@ -293,7 +277,7 @@ class AIMessagePart {
exception: error,
stack: stack,
library: 'stream_chat_flutter_ai',
context: ErrorDescription('while decoding the ${kind.value} message part "$id"'),
context: ErrorDescription('while decoding the $kind message part "$id"'),
),
);
return null;
Expand All @@ -306,7 +290,7 @@ class AIMessagePart {
Map<String, Object?> toJson() => {
...payload,
if (!payload.containsKey('v') && version != supportedVersion) 'v': version,
'type': kind.value,
'type': kind,
};

@override
Expand All @@ -322,5 +306,5 @@ class AIMessagePart {
int get hashCode => Object.hash(id, kind, version, const DeepCollectionEquality().hash(payload));

@override
String toString() => 'AIMessagePart(id: $id, kind: ${kind.value}, version: $version)';
String toString() => 'AIMessagePart(id: $id, kind: $kind, version: $version)';
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ typedef AIToolCallWidgetBuilder = Widget? Function(BuildContext context, AIToolC
/// ```dart
/// AIMessagePartsView(
/// parts: parts,
/// partBuilder: (context, part) => switch (part.kind.value) {
/// partBuilder: (context, part) => switch (part.kind) {
/// 'ai_citation' => CitationView(part: part),
/// _ => null,
/// },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@ import 'package:stream_chat_flutter_ai/src/parts/json_fields.dart';
///
/// An open set: compare against the statuses you know ([streaming],
/// [completed]) and give the rest a sensible fallback. A status this package
/// doesn't know keeps its raw [value] and reports `false` from [isKnown].
@immutable
class AIReasoningStatus {
/// Wraps a raw status string.
const AIReasoningStatus(this.value);

/// doesn't know keeps its raw value and reports `false` from [isKnown].
extension type const AIReasoningStatus(String _) implements String {
/// The model is still thinking.
static const streaming = AIReasoningStatus('streaming');

Expand All @@ -21,20 +17,8 @@ class AIReasoningStatus {
/// The statuses this package knows.
static const values = [streaming, completed];

/// The status as the agent wrote it, such as `streaming`.
final String value;

/// Whether this is one of [values].
bool get isKnown => values.contains(this);

@override
bool operator ==(Object other) => identical(this, other) || other is AIReasoningStatus && other.value == value;

@override
int get hashCode => value.hashCode;

@override
String toString() => 'AIReasoningStatus($value)';
}

/// A round of the model's reasoning β€” the typed view of an `ai_reasoning`
Expand Down Expand Up @@ -123,7 +107,7 @@ class AIReasoningPart {
Map<String, Object?> toJson() => {
'id': id,
'v': AIMessagePart.supportedVersion,
'status': status.value,
'status': status,
'summary': ?summary,
'preview': ?preview,
'duration_ms': ?durationMs,
Expand All @@ -143,5 +127,5 @@ class AIReasoningPart {
int get hashCode => Object.hash(id, status, summary, preview, durationMs);

@override
String toString() => 'AIReasoningPart(id: $id, status: ${status.value}, summary: $summary, durationMs: $durationMs)';
String toString() => 'AIReasoningPart(id: $id, status: $status, summary: $summary, durationMs: $durationMs)';
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ import 'package:stream_chat_flutter_ai/src/parts/json_fields.dart';
/// `decision`.
///
/// An open set: compare against the decisions you know ([allowed],
/// [declined]). A decision this package doesn't know keeps its raw [value] and
/// [declined]). A decision this package doesn't know keeps its raw value and
/// reports `false` from [isKnown].
@immutable
class AIToolApprovalDecision {
/// Wraps a raw decision string.
const AIToolApprovalDecision(this.value);

extension type const AIToolApprovalDecision(String _) implements String {
/// The person allowed the call, so it goes on.
static const allowed = AIToolApprovalDecision('allowed');

Expand All @@ -21,20 +17,8 @@ class AIToolApprovalDecision {
/// The decisions this package knows.
static const values = [allowed, declined];

/// The decision as the agent wrote it, such as `allowed`.
final String value;

/// Whether this is one of [values].
bool get isKnown => values.contains(this);

@override
bool operator ==(Object other) => identical(this, other) || other is AIToolApprovalDecision && other.value == value;

@override
int get hashCode => value.hashCode;

@override
String toString() => 'AIToolApprovalDecision($value)';
}

/// What a tool call asks the person it waits for before it runs, such as
Expand Down Expand Up @@ -142,7 +126,7 @@ class AIToolApproval {
'reason': ?reason,
'allow_title': ?allowTitle,
'decline_title': ?declineTitle,
'decision': ?decision?.value,
'decision': ?decision,
};

@override
Expand All @@ -160,5 +144,5 @@ class AIToolApproval {
int get hashCode => Object.hash(title, message, reason, allowTitle, declineTitle, decision);

@override
String toString() => 'AIToolApproval(title: $title, decision: ${decision?.value})';
String toString() => 'AIToolApproval(title: $title, decision: $decision)';
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ typedef AIToolApprovalContentBuilder =
/// ```dart
/// late final approver = AIToolApprover(
/// identity: AIClientIdentity(userId: currentUser.id, clientId: installId),
/// decide: (call, decision) => backend.answerToolCall(call.id, decision.value),
/// decide: (call, decision) => backend.answerToolCall(call.id, decision),
/// );
///
/// AIMessagePartsView(parts: parts, approver: approver)
Expand Down Expand Up @@ -250,7 +250,7 @@ final class AIToolApprovalSending extends AIToolApprovalState {
int get hashCode => Object.hash(AIToolApprovalSending, decision);

@override
String toString() => 'AIToolApprovalSending(${decision.value})';
String toString() => 'AIToolApprovalSending($decision)';
}

/// The answer [decision] arrived; the call's step hasn't changed yet.
Expand All @@ -268,7 +268,7 @@ final class AIToolApprovalSent extends AIToolApprovalState {
int get hashCode => Object.hash(AIToolApprovalSent, decision);

@override
String toString() => 'AIToolApprovalSent(${decision.value})';
String toString() => 'AIToolApprovalSent($decision)';
}

/// The answer [decision] couldn't be sent, because of [error]; the person can
Expand All @@ -291,5 +291,5 @@ final class AIToolApprovalFailed extends AIToolApprovalState {
int get hashCode => Object.hash(AIToolApprovalFailed, decision, error);

@override
String toString() => 'AIToolApprovalFailed(${decision.value}, $error)';
String toString() => 'AIToolApprovalFailed($decision, $error)';
}
Loading
Loading