feat(chat): Improve error handling and message versioning

This commit is contained in:
cogwheel0
2025-12-15 20:55:26 +05:30
parent 45532bf78f
commit 9018e382f7
5 changed files with 257 additions and 32 deletions

View File

@@ -1048,7 +1048,7 @@ class ApiService {
'modelIdx': 0,
'done': true,
if (ver.files != null) 'files': _sanitizeFilesForWebUI(ver.files),
// Mirror follow-ups, code executions, and sources for versions
// Mirror follow-ups, code executions, sources, and errors for versions
if (ver.followUps.isNotEmpty)
'followUps': List<String>.from(ver.followUps),
if (ver.codeExecutions.isNotEmpty)
@@ -1057,6 +1057,8 @@ class ApiService {
.toList(),
if (ver.sources.isNotEmpty)
'sources': ver.sources.map((s) => s.toJson()).toList(),
// Preserve error field for OpenWebUI compatibility
if (ver.error != null) 'error': ver.error!.toJson(),
};
// Link into parent (parentForVersions is always non-null here)
if (messagesMap.containsKey(parentForVersions)) {

View File

@@ -146,16 +146,24 @@ Map<String, dynamic> parseFullConversation(Map<String, dynamic> chatData) {
merged['content'] = synthesized;
}
messages.add(
_parseOpenWebUIMessageToJson(merged, historyMsg: historyMsg),
final parsed = _parseOpenWebUIMessageToJson(
merged,
historyMsg: historyMsg,
);
// Add versions from siblings
_addVersionsFromSiblings(parsed, msgData, historyMessagesMap);
messages.add(parsed);
index = j;
continue;
}
messages.add(
_parseOpenWebUIMessageToJson(msgData, historyMsg: historyMsg),
final parsed = _parseOpenWebUIMessageToJson(
msgData,
historyMsg: historyMsg,
);
// Add versions from siblings
_addVersionsFromSiblings(parsed, msgData, historyMessagesMap);
messages.add(parsed);
index++;
}
}
@@ -191,6 +199,120 @@ List<Map<String, dynamic>>? _extractToolCalls(
return null;
}
/// Add versions from sibling messages (alternative responses with same parent).
/// Siblings are stored in `_siblings` by `_buildMessagesListFromHistory`.
void _addVersionsFromSiblings(
Map<String, dynamic> parsed,
Map<String, dynamic> msgData,
Map<String, dynamic>? historyMessagesMap,
) {
final siblings = msgData['_siblings'];
if (siblings is! List || siblings.isEmpty) return;
final versions = <Map<String, dynamic>>[];
for (final siblingData in siblings) {
if (siblingData is! Map<String, dynamic>) continue;
final siblingId = siblingData['id']?.toString();
final historyMsg = historyMessagesMap != null && siblingId != null
? (historyMessagesMap[siblingId] as Map<String, dynamic>?)
: null;
// Parse the sibling as a version
final version = _parseSiblingAsVersion(siblingData, historyMsg: historyMsg);
if (version != null) {
versions.add(version);
}
}
if (versions.isNotEmpty) {
parsed['versions'] = versions;
}
}
/// Parse a sibling message as a ChatMessageVersion JSON map.
Map<String, dynamic>? _parseSiblingAsVersion(
Map<String, dynamic> msgData, {
Map<String, dynamic>? historyMsg,
}) {
// Extract content (same logic as _parseOpenWebUIMessageToJson)
dynamic content = msgData['content'];
if ((content == null || (content is String && content.isEmpty)) &&
historyMsg != null &&
historyMsg['content'] != null) {
content = historyMsg['content'];
}
var contentString = '';
if (content is List) {
final buffer = StringBuffer();
for (final entry in content) {
if (entry is Map && entry['type'] == 'text') {
final text = entry['text']?.toString();
if (text != null && text.isNotEmpty) {
buffer.write(text);
}
}
}
contentString = buffer.toString();
} else {
contentString = content?.toString() ?? '';
}
if (historyMsg != null) {
final histContent = historyMsg['content'];
if (histContent is String && histContent.length > contentString.length) {
contentString = histContent;
}
}
// Extract files
final effectiveFiles = msgData['files'] ?? historyMsg?['files'];
List<Map<String, dynamic>>? files;
if (effectiveFiles is List) {
final allFiles = <Map<String, dynamic>>[];
for (final entry in effectiveFiles) {
if (entry is! Map) continue;
if (entry['type'] != null && entry['url'] != null) {
final fileMap = <String, dynamic>{
'type': entry['type'],
'url': entry['url'],
};
if (entry['name'] != null) fileMap['name'] = entry['name'];
if (entry['size'] != null) fileMap['size'] = entry['size'];
allFiles.add(fileMap);
}
}
files = allFiles.isNotEmpty ? allFiles : null;
}
// Extract other fields
final sourcesRaw = historyMsg != null
? historyMsg['sources'] ?? historyMsg['citations']
: msgData['sources'] ?? msgData['citations'];
final followUpsRaw = historyMsg != null
? historyMsg['followUps'] ?? historyMsg['follow_ups']
: msgData['followUps'] ?? msgData['follow_ups'];
final codeExecRaw = historyMsg != null
? historyMsg['codeExecutions'] ?? historyMsg['code_executions']
: msgData['codeExecutions'] ?? msgData['code_executions'];
final rawUsage = _coerceJsonMap(historyMsg?['usage'] ?? msgData['usage']);
final errorData = _extractErrorData(msgData, historyMsg);
return <String, dynamic>{
'id': (msgData['id'] ?? _uuid.v4()).toString(),
'content': contentString,
'timestamp': _parseTimestamp(msgData['timestamp']).toIso8601String(),
if (msgData['model'] != null) 'model': msgData['model'].toString(),
if (files != null) 'files': files,
'sources': _parseSourcesField(sourcesRaw),
'followUps': _coerceStringList(followUpsRaw),
'codeExecutions': _parseCodeExecutionsField(codeExecRaw),
if (rawUsage.isNotEmpty) 'usage': rawUsage,
if (errorData != null) 'error': errorData,
};
}
/// Extract error data from OpenWebUI message format.
/// OpenWebUI stores errors in a separate 'error' field with 'content' inside.
/// Returns a map suitable for ChatMessageError.fromJson().
@@ -393,6 +515,8 @@ String _resolveRole(Map<String, dynamic> msgData) {
return 'user';
}
/// Build the message chain from history, following parent links from currentId.
/// Also collects sibling messages (alternative versions) for each message.
List<Map<String, dynamic>> _buildMessagesListFromHistory(
Map<String, dynamic> history,
) {
@@ -402,6 +526,7 @@ List<Map<String, dynamic>> _buildMessagesListFromHistory(
return const [];
}
// Build the main chain from currentId back to root
List<Map<String, dynamic>> buildChain(String? id) {
if (id == null) return const [];
final raw = messagesMap[id];
@@ -415,7 +540,48 @@ List<Map<String, dynamic>> _buildMessagesListFromHistory(
return [msg];
}
return buildChain(currentId);
final chain = buildChain(currentId);
// For each message in the chain, find sibling versions
// Siblings are other children of the same parent
for (final msg in chain) {
final parentId = msg['parentId']?.toString();
if (parentId == null || parentId.isEmpty) continue;
final parent = messagesMap[parentId];
if (parent is! Map) continue;
final childrenIds = parent['childrenIds'];
if (childrenIds is! List || childrenIds.length <= 1) continue;
// Collect sibling messages (same role, different id)
final msgId = msg['id']?.toString();
final msgRole = msg['role']?.toString();
final siblings = <Map<String, dynamic>>[];
for (final siblingId in childrenIds) {
final sibId = siblingId?.toString();
if (sibId == null || sibId == msgId) continue;
final siblingRaw = messagesMap[sibId];
if (siblingRaw is! Map) continue;
final sibling = _coerceJsonMap(siblingRaw);
final siblingRole = sibling['role']?.toString();
// Only include siblings with the same role (e.g., alternative assistant responses)
if (siblingRole == msgRole) {
sibling['id'] = sibId;
siblings.add(sibling);
}
}
if (siblings.isNotEmpty) {
msg['_siblings'] = siblings;
}
}
return chain;
}
DateTime _parseTimestamp(dynamic timestamp) {