Skip to content

Commit 13c2e87

Browse files
authored
feat(llc): add AI events (#2057)
* feat(llc): AI events * chore: update CHANGELOG.md * test: update tests * test: add more tests * chore: fix analyzer warning
1 parent 4b062b2 commit 13c2e87

File tree

12 files changed

+148
-28
lines changed

12 files changed

+148
-28
lines changed

packages/stream_chat/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Upcoming
2+
3+
✅ Added
4+
5+
- Added support for AI assistant states and events.
6+
17
## 8.2.0
28

39
✅ Added

packages/stream_chat/lib/src/client/channel.dart

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,16 @@ class Channel {
11581158
}
11591159
}
11601160

1161+
/// Sends an event to stop AI response generation, leaving the message in
1162+
/// its current state.
1163+
Future<EmptyResponse> stopAIResponse() async {
1164+
return sendEvent(
1165+
Event(
1166+
type: EventType.aiIndicatorStop,
1167+
),
1168+
);
1169+
}
1170+
11611171
/// Update the channel's [name].
11621172
///
11631173
/// This is the same as calling [updatePartial] and providing a map with a
@@ -1961,8 +1971,8 @@ class ChannelClientState {
19611971
final existingWatchers = channelState.watchers;
19621972
updateChannelState(channelState.copyWith(
19631973
watchers: [
1964-
...?existingWatchers,
19651974
watcher,
1975+
...?existingWatchers?.where((user) => user.id != watcher.id),
19661976
],
19671977
));
19681978
}
@@ -1977,9 +1987,9 @@ class ChannelClientState {
19771987
if (watcher != null) {
19781988
final existingWatchers = channelState.watchers;
19791989
updateChannelState(channelState.copyWith(
1980-
watchers: existingWatchers
1981-
?.where((user) => user.id != watcher.id)
1982-
.toList(growable: false),
1990+
watchers: [
1991+
...?existingWatchers?.where((user) => user.id != watcher.id)
1992+
],
19831993
));
19841994
}
19851995
}),
@@ -2367,7 +2377,7 @@ class ChannelClientState {
23672377
channelStateStream.map((cs) => cs.members),
23682378
_channel.client.state.usersStream,
23692379
(members, users) =>
2370-
members!.map((e) => e!.copyWith(user: users[e.user!.id])).toList(),
2380+
[...?members?.map((e) => e!.copyWith(user: users[e.user!.id]))],
23712381
).distinct(const ListEquality().equals);
23722382

23732383
/// Channel watcher count.
@@ -2387,7 +2397,7 @@ class ChannelClientState {
23872397
List<User>?, Map<String?, User?>, List<User>>(
23882398
channelStateStream.map((cs) => cs.watchers),
23892399
_channel.client.state.usersStream,
2390-
(watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(),
2400+
(watchers, users) => [...?watchers?.map((e) => users[e.id] ?? e)],
23912401
).distinct(const ListEquality().equals);
23922402

23932403
/// Channel member for the current user.

packages/stream_chat/lib/src/core/error/stream_chat_error.dart

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ class StreamChatNetworkError extends StreamChatError {
7676
ChatErrorCode errorCode, {
7777
int? statusCode,
7878
this.data,
79+
StackTrace? stacktrace,
7980
this.isRequestCancelledError = false,
8081
}) : code = errorCode.code,
8182
statusCode = statusCode ?? data?.statusCode,
83+
stackTrace = stacktrace ?? StackTrace.current,
8284
super(errorCode.message);
8385

8486
///
@@ -87,8 +89,10 @@ class StreamChatNetworkError extends StreamChatError {
8789
required String message,
8890
this.statusCode,
8991
this.data,
92+
StackTrace? stacktrace,
9093
this.isRequestCancelledError = false,
91-
}) : super(message);
94+
}) : stackTrace = stacktrace ?? StackTrace.current,
95+
super(message);
9296

9397
///
9498
factory StreamChatNetworkError.fromDioException(DioException exception) {
@@ -108,8 +112,9 @@ class StreamChatNetworkError extends StreamChatError {
108112
'',
109113
statusCode: errorResponse?.statusCode ?? response?.statusCode,
110114
data: errorResponse,
115+
stacktrace: exception.stackTrace,
111116
isRequestCancelledError: exception.type == DioExceptionType.cancel,
112-
)..stackTrace = exception.stackTrace;
117+
);
113118
}
114119

115120
/// Error code
@@ -124,10 +129,8 @@ class StreamChatNetworkError extends StreamChatError {
124129
/// True, in case the error is due to a cancelled network request.
125130
final bool isRequestCancelledError;
126131

127-
StackTrace? _stackTrace;
128-
129-
///
130-
set stackTrace(StackTrace? stack) => _stackTrace = stack;
132+
/// The optional stack trace attached to the error.
133+
final StackTrace? stackTrace;
131134

132135
///
133136
ChatErrorCode? get errorCode => chatErrorCodeFromCode(code);
@@ -145,8 +148,8 @@ class StreamChatNetworkError extends StreamChatError {
145148
if (data != null) params += ', data: $data';
146149
var msg = 'StreamChatNetworkError($params)';
147150

148-
if (printStackTrace && _stackTrace != null) {
149-
msg += '\n$_stackTrace';
151+
if (printStackTrace && stackTrace != null) {
152+
msg += '\n$stackTrace';
150153
}
151154
return msg;
152155
}

packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class AuthInterceptor extends QueuedInterceptor {
3030
final dioError = StreamChatDioError(
3131
error: error,
3232
requestOptions: options,
33+
stackTrace: StackTrace.current,
3334
);
3435
return handler.reject(dioError, true);
3536
}

packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ class StreamChatDioError extends DioException {
99
required super.requestOptions,
1010
super.response,
1111
super.type,
12+
StackTrace? stackTrace,
13+
super.message,
1214
}) : super(
1315
error: error,
16+
stackTrace: stackTrace ?? StackTrace.current,
1417
);
1518

1619
@override

packages/stream_chat/lib/src/core/http/stream_http_client.dart

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,10 @@ class StreamHttpClient {
9797
void close({bool force = false}) => httpClient.close(force: force);
9898

9999
StreamChatNetworkError _parseError(DioException exception) {
100-
StreamChatNetworkError error;
101100
// locally thrown dio error
102-
if (exception is StreamChatDioError) {
103-
error = exception.error;
104-
} else {
105-
// real network request dio error
106-
error = StreamChatNetworkError.fromDioException(exception);
107-
}
108-
return error..stackTrace = exception.stackTrace;
101+
if (exception is StreamChatDioError) return exception.error;
102+
// real network request dio error
103+
return StreamChatNetworkError.fromDioException(exception);
109104
}
110105

111106
/// Handy method to make http GET request with error parsing.

packages/stream_chat/lib/src/core/models/event.dart

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class Event {
2626
this.channelType,
2727
this.parentId,
2828
this.hardDelete,
29+
this.aiState,
30+
this.aiMessage,
31+
this.messageId,
2932
this.extraData = const {},
3033
this.isLocal = true,
3134
}) : createdAt = createdAt?.toUtc() ?? DateTime.now().toUtc();
@@ -94,6 +97,16 @@ class Event {
9497
@JsonKey(includeIfNull: false)
9598
final bool? hardDelete;
9699

100+
/// The current state of the AI assistant.
101+
@JsonKey(unknownEnumValue: AITypingState.idle)
102+
final AITypingState? aiState;
103+
104+
/// Additional message from the AI assistant.
105+
final String? aiMessage;
106+
107+
/// The message id to which the event belongs.
108+
final String? messageId;
109+
97110
/// Map of custom channel extraData
98111
final Map<String, Object?> extraData;
99112

@@ -136,6 +149,9 @@ class Event {
136149
'parent_id',
137150
'hard_delete',
138151
'is_local',
152+
'ai_state',
153+
'ai_message',
154+
'message_id',
139155
];
140156

141157
/// Serialize to json
@@ -162,6 +178,9 @@ class Event {
162178
bool? online,
163179
String? parentId,
164180
bool? hardDelete,
181+
AITypingState? aiState,
182+
String? aiMessage,
183+
String? messageId,
165184
Map<String, Object?>? extraData,
166185
}) =>
167186
Event(
@@ -183,14 +202,15 @@ class Event {
183202
parentId: parentId ?? this.parentId,
184203
hardDelete: hardDelete ?? this.hardDelete,
185204
extraData: extraData ?? this.extraData,
205+
aiState: aiState ?? this.aiState,
206+
aiMessage: aiMessage ?? this.aiMessage,
207+
messageId: messageId ?? this.messageId,
186208
isLocal: isLocal,
187209
);
188210
}
189211

190212
/// The channel embedded in the event object
191-
@JsonSerializable(
192-
createToJson: false,
193-
)
213+
@JsonSerializable(createToJson: false)
194214
class EventChannel extends ChannelModel {
195215
/// Constructor used for json serialization
196216
EventChannel({
@@ -207,12 +227,12 @@ class EventChannel extends ChannelModel {
207227
required DateTime super.updatedAt,
208228
super.deletedAt,
209229
super.memberCount,
210-
Map<String, Object?>? extraData,
211230
super.cooldown,
212231
super.team,
213232
super.disabled,
214233
super.hidden,
215234
super.truncatedAt,
235+
Map<String, Object?>? extraData,
216236
}) : super(extraData: extraData ?? {});
217237

218238
/// Create a new instance from a json
@@ -232,3 +252,31 @@ class EventChannel extends ChannelModel {
232252
...ChannelModel.topLevelFields,
233253
];
234254
}
255+
256+
/// {@template aiState}
257+
/// The current typing state of the AI assistant.
258+
///
259+
/// This is used to determine the state of the AI assistant when it's generating
260+
/// a response for the provided query.
261+
/// {@endtemplate}
262+
enum AITypingState {
263+
/// The AI assistant is idle.
264+
@JsonValue('AI_STATE_IDLE')
265+
idle,
266+
267+
/// The AI assistant is in an error state.
268+
@JsonValue('AI_STATE_ERROR')
269+
error,
270+
271+
/// The AI assistant is checking external sources.
272+
@JsonValue('AI_STATE_CHECKING_SOURCES')
273+
checkingSources,
274+
275+
/// The AI assistant is thinking.
276+
@JsonValue('AI_STATE_THINKING')
277+
thinking,
278+
279+
/// The AI assistant is generating a response.
280+
@JsonValue('AI_STATE_GENERATING')
281+
generating,
282+
}

packages/stream_chat/lib/src/core/models/event.g.dart

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/stream_chat/lib/src/event_type.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,13 @@ class EventType {
112112

113113
/// Event sent when the user's mutes list is updated
114114
static const String notificationMutesUpdated = 'notification.mutes_updated';
115+
116+
/// Event sent when the AI indicator is updated
117+
static const String aiIndicatorUpdate = 'ai_indicator.update';
118+
119+
/// Event sent when the AI indicator is stopped
120+
static const String aiIndicatorStop = 'ai_indicator.stop';
121+
122+
/// Event sent when the AI indicator is cleared
123+
static const String aiIndicatorClear = 'ai_indicator.clear';
115124
}

packages/stream_chat/test/fixtures/event.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,7 @@
2525
"online": false,
2626
"image": "https://getstream.io/random_svg/?name=Dry+meadow",
2727
"name": "Dry meadow"
28-
}
28+
},
29+
"ai_state": "AI_STATE_THINKING",
30+
"ai_message": "Some message"
2931
}

0 commit comments

Comments
 (0)