fix: sources count

This commit is contained in:
cogwheel0
2025-09-28 15:15:35 +05:30
parent b8856679da
commit 0ff48eeb38
4 changed files with 243 additions and 90 deletions

View File

@@ -0,0 +1,228 @@
import '../models/chat_message.dart';
/// Parses OpenWebUI style source payloads into flattened chat source references.
List<ChatSourceReference> parseOpenWebUISourceList(dynamic raw) {
if (raw is! List) {
return const <ChatSourceReference>[];
}
final aggregated = <String, _CitationAccumulator>{};
var fallbackIndex = 0;
for (final entry in raw) {
if (entry is! Map) {
continue;
}
final entryMap = _asStringKeyMap(entry);
if (entryMap == null) {
continue;
}
final baseSource =
_asStringKeyMap(entryMap['source']) ?? <String, dynamic>{};
entryMap.remove('source');
for (final key in ['id', 'name', 'title', 'url', 'link', 'type']) {
final value = entryMap[key];
if (value != null && baseSource[key] == null) {
baseSource[key] = value;
}
}
final documents = entryMap['document'] is List
? (entryMap['document'] as List)
: const [];
final metadataRaw = entryMap['metadata'];
final metadataList = metadataRaw is List
? metadataRaw
: metadataRaw is Map
? [metadataRaw]
: const [];
final distances = entryMap['distances'] is List
? (entryMap['distances'] as List)
: const [];
final counts = <int>[
documents.length,
metadataList.length,
distances.length,
].where((len) => len > 0).toList();
final loopCount = counts.isEmpty
? 1
: counts.reduce((value, element) => value > element ? value : element);
for (var index = 0; index < loopCount; index++) {
final document = index < documents.length ? documents[index] : null;
final metadata = index < metadataList.length ? metadataList[index] : null;
final distance = index < distances.length ? distances[index] : null;
final metadataMap = _asStringKeyMap(metadata) ?? <String, dynamic>{};
final idCandidate = _firstNonEmpty([
metadataMap['source'],
metadataMap['id'],
baseSource['id'],
entryMap['id'],
]);
final key = idCandidate?.isNotEmpty == true
? idCandidate!
: '__fallback_${fallbackIndex++}';
final accumulator = aggregated.putIfAbsent(
key,
() => _CitationAccumulator(
key: key,
source: Map<String, dynamic>.from(baseSource),
),
);
accumulator.explicitId ??= idCandidate?.toString();
accumulator.explicitType ??= _firstNonEmpty([
baseSource['type'],
entryMap['type'],
metadataMap['type'],
])?.toString();
final metadataName = _firstNonEmpty([
metadataMap['name'],
metadataMap['title'],
])?.toString();
if (metadataName != null && metadataName.isNotEmpty) {
accumulator.source['name'] = metadataName;
accumulator.source['title'] ??= metadataName;
}
if (_looksLikeUrl(idCandidate)) {
accumulator.source['url'] ??= idCandidate;
accumulator.source['name'] ??= idCandidate;
}
final metadataUrl = _firstNonEmpty([
metadataMap['url'],
metadataMap['link'],
metadataMap['source'],
accumulator.source['url'],
])?.toString();
if (_looksLikeUrl(metadataUrl)) {
accumulator.source['url'] = metadataUrl;
}
final snippet = _extractSnippet(document);
if (snippet != null && snippet.isNotEmpty) {
accumulator.documents.add(snippet);
}
if (metadataMap.isNotEmpty) {
accumulator.metadata.add(metadataMap);
}
if (distance != null) {
accumulator.distances.add(distance);
}
}
}
final results = <ChatSourceReference>[];
for (final accumulator in aggregated.values) {
final id = accumulator.explicitId;
final title = _firstNonEmpty([
accumulator.source['name'],
accumulator.source['title'],
id,
])?.toString();
final urlCandidate = _firstNonEmpty([
accumulator.source['url'],
id,
])?.toString();
final url = _looksLikeUrl(urlCandidate) ? urlCandidate : null;
final snippet = accumulator.documents.firstWhere(
(doc) => doc.trim().isNotEmpty,
orElse: () => '',
);
final metadata = <String, dynamic>{
if (accumulator.metadata.isNotEmpty) 'items': accumulator.metadata,
if (accumulator.documents.isNotEmpty) 'documents': accumulator.documents,
if (accumulator.distances.isNotEmpty) 'distances': accumulator.distances,
if (accumulator.source.isNotEmpty) 'source': accumulator.source,
};
metadata.removeWhere((key, value) {
if (value == null) return true;
if (value is List && value.isEmpty) return true;
if (value is Map && value.isEmpty) return true;
return false;
});
results.add(
ChatSourceReference(
id: (id != null && id.startsWith('__fallback_')) ? null : id,
title: title,
url: url,
snippet: snippet.isNotEmpty ? snippet : null,
type: accumulator.explicitType,
metadata: metadata.isNotEmpty ? metadata : null,
),
);
}
return results;
}
Map<String, dynamic>? _asStringKeyMap(dynamic value) {
if (value is Map) {
final map = <String, dynamic>{};
value.forEach((key, entryValue) {
map[key.toString()] = entryValue;
});
return map;
}
return null;
}
String? _firstNonEmpty(Iterable<dynamic> values) {
for (final value in values) {
if (value == null) {
continue;
}
final stringValue = value.toString();
if (stringValue.isNotEmpty) {
return stringValue;
}
}
return null;
}
bool _looksLikeUrl(String? value) {
if (value == null) {
return false;
}
return value.startsWith('http://') || value.startsWith('https://');
}
String? _extractSnippet(dynamic document) {
if (document == null) {
return null;
}
if (document is String) {
return document.trim();
}
return document.toString().trim().isNotEmpty ? document.toString() : null;
}
class _CitationAccumulator {
_CitationAccumulator({required this.key, required this.source});
final String key;
final Map<String, dynamic> source;
final List<String> documents = [];
final List<Map<String, dynamic>> metadata = [];
final List<dynamic> distances = [];
String? explicitId;
String? explicitType;
}