forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchat_evidence_reference.dart
More file actions
357 lines (324 loc) · 11.9 KB
/
Copy pathchat_evidence_reference.dart
File metadata and controls
357 lines (324 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/// Additive, versioned references that let a chat answer point at supporting
/// conversation or screen evidence without making that evidence required for
/// the text answer to render.
///
/// Older mobile releases ignore the containing JSON field. Newer releases
/// ignore unknown fields/kinds/states, so the envelope can evolve without
/// changing the existing ServerMessage JSON contract.
library;
import 'dart:convert';
enum ChatEvidenceReferenceKind {
conversationSummary('conversation_summary'),
conversationSegment('conversation_segment'),
screen('screen'),
keyframe('keyframe'),
request('request'),
unknown('unknown');
const ChatEvidenceReferenceKind(this.wireValue);
final String wireValue;
static ChatEvidenceReferenceKind fromWire(Object? value) {
final wireValue = value?.toString().trim().toLowerCase();
return values.firstWhere(
(kind) => kind.wireValue == wireValue,
orElse: () => ChatEvidenceReferenceKind.unknown,
);
}
}
enum ChatEvidenceReferenceState {
available('available'),
loading('loading'),
offline('offline'),
pruned('pruned'),
failed('failed'),
unknown('unknown');
const ChatEvidenceReferenceState(this.wireValue);
final String wireValue;
static ChatEvidenceReferenceState fromWire(Object? value) {
final wireValue = value?.toString().trim().toLowerCase();
return values.firstWhere(
(state) => state.wireValue == wireValue,
orElse: () => ChatEvidenceReferenceState.unknown,
);
}
}
class ChatEvidenceReference {
static const maxReferencesPerEnvelope = 24;
static const maxIdentifierCharacters = 256;
static const maxTitleCharacters = 160;
static const maxSummaryCharacters = 600;
static const maxErrorCodeCharacters = 128;
static const maxErrorMessageCharacters = 600;
static const maxMetadataEntries = 16;
static const maxMetadataSerializedCharacters = 2000;
static const maxMetadataDepth = 3;
static const maxMetadataListItems = 24;
const ChatEvidenceReference({
required this.id,
required this.kind,
required this.state,
this.title,
this.summary,
this.conversationId,
this.segmentId,
this.frameId,
this.requestId,
this.startMs,
this.endMs,
this.capturedAtMs,
this.errorCode,
this.errorMessage,
this.metadata = const {},
});
final String id;
final ChatEvidenceReferenceKind kind;
final ChatEvidenceReferenceState state;
final String? title;
final String? summary;
final String? conversationId;
final String? segmentId;
final String? frameId;
final String? requestId;
final int? startMs;
final int? endMs;
final int? capturedAtMs;
final String? errorCode;
final String? errorMessage;
final Map<String, dynamic> metadata;
factory ChatEvidenceReference.fromJson(Map<String, dynamic> json) {
return ChatEvidenceReference(
id: _string(json['id'] ?? json['reference_id'], maxLength: maxIdentifierCharacters) ?? '',
kind: ChatEvidenceReferenceKind.fromWire(json['kind'] ?? json['type']),
state: ChatEvidenceReferenceState.fromWire(
json['state'] ?? json['status'],
),
title: _string(json['title'], maxLength: maxTitleCharacters),
summary: _string(json['summary'] ?? json['preview'], maxLength: maxSummaryCharacters),
conversationId: _string(
json['conversation_id'] ?? json['conversationId'],
maxLength: maxIdentifierCharacters,
),
segmentId: _string(
json['segment_id'] ?? json['segmentId'],
maxLength: maxIdentifierCharacters,
),
frameId: _string(json['frame_id'] ?? json['frameId'], maxLength: maxIdentifierCharacters),
requestId: _string(json['request_id'] ?? json['requestId'], maxLength: maxIdentifierCharacters),
startMs: _int(json['start_ms'] ?? json['startMs']),
endMs: _int(json['end_ms'] ?? json['endMs']),
capturedAtMs: _int(json['captured_at_ms'] ?? json['capturedAtMs']),
errorCode: _string(
json['error_code'] ?? json['errorCode'],
maxLength: maxErrorCodeCharacters,
),
errorMessage: _string(
json['error_message'] ?? json['errorMessage'],
maxLength: maxErrorMessageCharacters,
),
metadata: _map(json['metadata']),
);
}
/// A usable reference is optional UI chrome; the answer must not depend on it.
bool get canOpen {
if (id.trim().isEmpty || state != ChatEvidenceReferenceState.available) return false;
return switch (kind) {
ChatEvidenceReferenceKind.conversationSummary => conversationId != null,
ChatEvidenceReferenceKind.conversationSegment => conversationId != null && segmentId != null,
ChatEvidenceReferenceKind.screen || ChatEvidenceReferenceKind.keyframe => frameId != null,
ChatEvidenceReferenceKind.request => requestId != null,
ChatEvidenceReferenceKind.unknown => false,
};
}
String get sourceLabel {
switch (kind) {
case ChatEvidenceReferenceKind.conversationSummary:
return 'Conversation summary';
case ChatEvidenceReferenceKind.conversationSegment:
return 'Conversation segment';
case ChatEvidenceReferenceKind.screen:
return 'Current screen';
case ChatEvidenceReferenceKind.keyframe:
return 'Screen keyframe';
case ChatEvidenceReferenceKind.request:
return 'Evidence request';
case ChatEvidenceReferenceKind.unknown:
return 'Evidence';
}
}
String get statusLabel {
switch (state) {
case ChatEvidenceReferenceState.available:
return 'Available';
case ChatEvidenceReferenceState.loading:
return 'Loading';
case ChatEvidenceReferenceState.offline:
return 'Unavailable offline';
case ChatEvidenceReferenceState.pruned:
return 'No longer available';
case ChatEvidenceReferenceState.failed:
return 'Failed to load';
case ChatEvidenceReferenceState.unknown:
return 'Unavailable';
}
}
String get accessibilityLabel {
final detail = (title ?? summary)?.trim();
final suffix = detail == null || detail.isEmpty ? '' : ': $detail';
return '$sourceLabel$suffix, $statusLabel';
}
Map<String, dynamic> toJson() {
final json = <String, dynamic>{
'id': id,
'kind': kind.wireValue,
'state': state.wireValue,
};
_put(json, 'title', title);
_put(json, 'summary', summary);
_put(json, 'conversation_id', conversationId);
_put(json, 'segment_id', segmentId);
_put(json, 'frame_id', frameId);
_put(json, 'request_id', requestId);
_put(json, 'start_ms', startMs);
_put(json, 'end_ms', endMs);
_put(json, 'captured_at_ms', capturedAtMs);
_put(
json,
'error_code',
_string(errorCode, maxLength: maxErrorCodeCharacters),
);
_put(
json,
'error_message',
_string(errorMessage, maxLength: maxErrorMessageCharacters),
);
final boundedMetadata = _map(metadata);
if (boundedMetadata.isNotEmpty) json['metadata'] = boundedMetadata;
return json;
}
static String? _string(Object? value, {int? maxLength}) {
if (value is! String) return null;
final stripped = value.trim();
if (stripped.isEmpty) return null;
if (maxLength != null && stripped.length > maxLength) return stripped.substring(0, maxLength);
return stripped;
}
static int? _int(Object? value) => value is num ? value.toInt() : int.tryParse(value?.toString() ?? '');
static Map<String, dynamic> _map(Object? value) {
if (value is! Map) return const {};
final bounded = <String, dynamic>{};
for (final entry in value.entries.take(maxMetadataEntries)) {
final key = _string(entry.key.toString(), maxLength: maxIdentifierCharacters);
final item = _metadataValue(entry.value, 0);
if (key != null && !identical(item, _omitMetadataValue)) bounded[key] = item;
}
while (bounded.isNotEmpty) {
try {
if (jsonEncode(bounded).length <= maxMetadataSerializedCharacters) break;
} catch (_) {
return const {};
}
bounded.remove(bounded.keys.last);
}
return bounded;
}
static final Object _omitMetadataValue = Object();
static dynamic _metadataValue(Object? value, int depth) {
if (value == null || value is bool) return value;
if (value is num) return value.isFinite ? value : _omitMetadataValue;
if (value is String) return _string(value, maxLength: maxSummaryCharacters);
if (depth > maxMetadataDepth) return _omitMetadataValue;
if (value is List) {
return value
.take(maxMetadataListItems)
.map((item) => _metadataValue(item, depth + 1))
.where((item) => !identical(item, _omitMetadataValue))
.toList(growable: false);
}
if (value is Map) {
final nested = <String, dynamic>{};
for (final entry in value.entries.take(maxMetadataEntries)) {
final key = _string(entry.key.toString(), maxLength: maxIdentifierCharacters);
final item = _metadataValue(entry.value, depth + 1);
if (key != null && !identical(item, _omitMetadataValue)) nested[key] = item;
}
return nested;
}
return _omitMetadataValue;
}
static void _put(Map<String, dynamic> json, String key, Object? value) {
if (value != null) json[key] = value;
}
}
class ChatEvidenceReferenceEnvelope {
static const currentSchemaVersion = 1;
const ChatEvidenceReferenceEnvelope({
this.schemaVersion = currentSchemaVersion,
required this.references,
this.requestId,
});
final int schemaVersion;
final String? requestId;
final List<ChatEvidenceReference> references;
bool get isEmpty => references.isEmpty;
factory ChatEvidenceReferenceEnvelope.fromJson(Map<String, dynamic> json) {
const schemaKeys = ['schema_version', 'schemaVersion', 'version'];
final schemaKey = schemaKeys.firstWhere(
json.containsKey,
orElse: () => '',
);
final schemaVersion = schemaKey.isEmpty ? currentSchemaVersion : _schemaVersion(json[schemaKey]) ?? 0;
final rawReferences = json['references'] ?? json['evidence_refs'] ?? json['evidence_references'];
final references = rawReferences is List
? rawReferences
.whereType<Map>()
.map((value) {
Map<String, dynamic> reference;
try {
reference = Map<String, dynamic>.from(value);
} catch (_) {
return null;
}
if (schemaVersion != currentSchemaVersion) {
// Preserve the envelope for diagnostics, but a future wire
// contract must never become actionable under v1 semantics.
reference['kind'] = 'unknown';
reference['state'] = 'unknown';
}
return ChatEvidenceReference.fromJson(
reference,
);
})
.whereType<ChatEvidenceReference>()
.take(ChatEvidenceReference.maxReferencesPerEnvelope)
.toList(growable: false)
: const <ChatEvidenceReference>[];
return ChatEvidenceReferenceEnvelope(
schemaVersion: schemaVersion,
requestId: ChatEvidenceReference._string(
json['request_id'] ?? json['requestId'],
maxLength: ChatEvidenceReference.maxIdentifierCharacters,
),
references: references,
);
}
static ChatEvidenceReferenceEnvelope? tryFromJson(Object? value) {
if (value is! Map) return null;
try {
return ChatEvidenceReferenceEnvelope.fromJson(Map<String, dynamic>.from(value));
} catch (_) {
return null;
}
}
static int? _schemaVersion(Object? value) {
if (value is int) return value;
if (value is num && value.isFinite && value == value.toInt()) return value.toInt();
if (value is String) return int.tryParse(value.trim());
return null;
}
Map<String, dynamic> toJson() {
return {
'schema_version': schemaVersion,
if (requestId != null) 'request_id': requestId,
'references': references.map((reference) => reference.toJson()).toList(growable: false),
};
}
}