Merge pull request #291 from cogwheel0/fix-message-persistence
fix-message-persistence
This commit is contained in:
@@ -33,6 +33,106 @@ void _traceApi(String message) {
|
||||
DebugLogger.log(message, scope: 'api/trace');
|
||||
}
|
||||
|
||||
/// Converts ChatSourceReference list back to OpenWebUI's expected format.
|
||||
/// OpenWebUI expects: { source: {...}, document: [...], metadata: [...] }
|
||||
/// But ChatSourceReference stores: { id, title, url, snippet, type, metadata }
|
||||
List<Map<String, dynamic>> _convertSourcesToOpenWebUIFormat(
|
||||
List<ChatSourceReference> sources,
|
||||
) {
|
||||
return sources.map((ref) {
|
||||
final result = <String, dynamic>{};
|
||||
|
||||
// Build the source object
|
||||
final sourceObj = <String, dynamic>{};
|
||||
if (ref.id != null) sourceObj['id'] = ref.id;
|
||||
if (ref.title != null) sourceObj['name'] = ref.title;
|
||||
if (ref.url != null) sourceObj['url'] = ref.url;
|
||||
if (ref.type != null) sourceObj['type'] = ref.type;
|
||||
|
||||
// Extract nested source from metadata if present
|
||||
final metadataSource = ref.metadata?['source'];
|
||||
if (metadataSource is Map) {
|
||||
for (final entry in metadataSource.entries) {
|
||||
sourceObj[entry.key.toString()] ??= entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceObj.isNotEmpty) {
|
||||
result['source'] = sourceObj;
|
||||
}
|
||||
|
||||
// Extract documents from metadata or use snippet
|
||||
final documents = ref.metadata?['documents'];
|
||||
if (documents is List && documents.isNotEmpty) {
|
||||
result['document'] = documents;
|
||||
} else if (ref.snippet != null && ref.snippet!.isNotEmpty) {
|
||||
result['document'] = [ref.snippet];
|
||||
}
|
||||
|
||||
// Extract metadata items
|
||||
final metadataItems = ref.metadata?['items'];
|
||||
if (metadataItems is List && metadataItems.isNotEmpty) {
|
||||
result['metadata'] = metadataItems;
|
||||
} else {
|
||||
// Create a basic metadata entry
|
||||
final basicMeta = <String, dynamic>{};
|
||||
if (ref.id != null) basicMeta['source'] = ref.id;
|
||||
if (ref.title != null) basicMeta['name'] = ref.title;
|
||||
if (result['document'] is List) {
|
||||
result['metadata'] = List.generate(
|
||||
(result['document'] as List).length,
|
||||
(_) => Map<String, dynamic>.from(basicMeta),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract distances if present
|
||||
final distances = ref.metadata?['distances'];
|
||||
if (distances is List && distances.isNotEmpty) {
|
||||
result['distances'] = distances;
|
||||
}
|
||||
|
||||
return result;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// Converts ChatCodeExecution list to OpenWebUI's expected format.
|
||||
/// OpenWebUI expects `code_executions` (snake_case) with specific structure.
|
||||
/// ChatCodeExecution stores: { id, name, language, code, result, metadata }
|
||||
/// OpenWebUI expects: { id, name, code, language?, result?: { error?, output?, files? } }
|
||||
List<Map<String, dynamic>> _convertCodeExecutionsToOpenWebUIFormat(
|
||||
List<ChatCodeExecution> executions,
|
||||
) {
|
||||
return executions.map((exec) {
|
||||
final result = <String, dynamic>{
|
||||
'id': exec.id,
|
||||
if (exec.name != null) 'name': exec.name,
|
||||
if (exec.code != null) 'code': exec.code,
|
||||
if (exec.language != null) 'language': exec.language,
|
||||
};
|
||||
|
||||
// Convert the result if present
|
||||
if (exec.result != null) {
|
||||
final execResult = <String, dynamic>{};
|
||||
if (exec.result!.output != null) execResult['output'] = exec.result!.output;
|
||||
if (exec.result!.error != null) execResult['error'] = exec.result!.error;
|
||||
if (exec.result!.files.isNotEmpty) {
|
||||
execResult['files'] = exec.result!.files
|
||||
.map((f) => <String, dynamic>{
|
||||
if (f.name != null) 'name': f.name,
|
||||
if (f.url != null) 'url': f.url,
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
if (execResult.isNotEmpty) {
|
||||
result['result'] = execResult;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
class ApiService {
|
||||
final Dio _dio;
|
||||
final ServerConfig serverConfig;
|
||||
@@ -966,7 +1066,10 @@ class ApiService {
|
||||
if (msg.role == 'assistant' && msg.model != null)
|
||||
'modelName': msg.model,
|
||||
if (msg.role == 'assistant') 'modelIdx': 0,
|
||||
if (msg.role == 'assistant') 'done': !msg.isStreaming,
|
||||
// Always set done: true when persisting to server.
|
||||
// If streaming is interrupted, the message should still be marked done
|
||||
// to prevent the web client from treating it as an in-progress stream.
|
||||
if (msg.role == 'assistant') 'done': true,
|
||||
if (msg.role == 'user' && model != null) 'models': [model],
|
||||
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty)
|
||||
'attachment_ids': List<String>.from(msg.attachmentIds!),
|
||||
@@ -977,9 +1080,11 @@ class ApiService {
|
||||
if (msg.followUps.isNotEmpty)
|
||||
'followUps': List<String>.from(msg.followUps),
|
||||
if (msg.codeExecutions.isNotEmpty)
|
||||
'codeExecutions': msg.codeExecutions.map((e) => e.toJson()).toList(),
|
||||
'code_executions':
|
||||
_convertCodeExecutionsToOpenWebUIFormat(msg.codeExecutions),
|
||||
// Convert sources back to OpenWebUI format (with document array)
|
||||
if (msg.sources.isNotEmpty)
|
||||
'sources': msg.sources.map((s) => s.toJson()).toList(),
|
||||
'sources': _convertSourcesToOpenWebUIFormat(msg.sources),
|
||||
// Include usage statistics for persistence (issue #274)
|
||||
if (msg.usage != null) 'usage': msg.usage,
|
||||
// Preserve error field for OpenWebUI compatibility
|
||||
@@ -1005,7 +1110,8 @@ class ApiService {
|
||||
if (msg.role == 'assistant' && msg.model != null)
|
||||
'modelName': msg.model,
|
||||
if (msg.role == 'assistant') 'modelIdx': 0,
|
||||
if (msg.role == 'assistant') 'done': !msg.isStreaming,
|
||||
// Always set done: true when persisting to server.
|
||||
if (msg.role == 'assistant') 'done': true,
|
||||
if (msg.role == 'user' && model != null) 'models': [model],
|
||||
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty)
|
||||
'attachment_ids': List<String>.from(msg.attachmentIds!),
|
||||
@@ -1016,9 +1122,11 @@ class ApiService {
|
||||
if (msg.followUps.isNotEmpty)
|
||||
'followUps': List<String>.from(msg.followUps),
|
||||
if (msg.codeExecutions.isNotEmpty)
|
||||
'codeExecutions': msg.codeExecutions.map((e) => e.toJson()).toList(),
|
||||
'code_executions':
|
||||
_convertCodeExecutionsToOpenWebUIFormat(msg.codeExecutions),
|
||||
// Convert sources back to OpenWebUI format (with document array)
|
||||
if (msg.sources.isNotEmpty)
|
||||
'sources': msg.sources.map((s) => s.toJson()).toList(),
|
||||
'sources': _convertSourcesToOpenWebUIFormat(msg.sources),
|
||||
// Include usage statistics for persistence (issue #274)
|
||||
if (msg.usage != null) 'usage': msg.usage,
|
||||
// Preserve error field for OpenWebUI compatibility
|
||||
@@ -1053,11 +1161,11 @@ class ApiService {
|
||||
if (ver.followUps.isNotEmpty)
|
||||
'followUps': List<String>.from(ver.followUps),
|
||||
if (ver.codeExecutions.isNotEmpty)
|
||||
'codeExecutions': ver.codeExecutions
|
||||
.map((e) => e.toJson())
|
||||
.toList(),
|
||||
'code_executions':
|
||||
_convertCodeExecutionsToOpenWebUIFormat(ver.codeExecutions),
|
||||
// Convert sources back to OpenWebUI format (with document array)
|
||||
if (ver.sources.isNotEmpty)
|
||||
'sources': ver.sources.map((s) => s.toJson()).toList(),
|
||||
'sources': _convertSourcesToOpenWebUIFormat(ver.sources),
|
||||
// Preserve error field for OpenWebUI compatibility
|
||||
if (ver.error != null) 'error': ver.error!.toJson(),
|
||||
};
|
||||
|
||||
@@ -883,7 +883,7 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
|
||||
}
|
||||
|
||||
// Pre-seed an assistant skeleton message (with a given id or a new one),
|
||||
// persist it to the server to keep the chain correct, and return the id.
|
||||
// persist it to the server to establish the message structure, and return the id.
|
||||
Future<String> _preseedAssistantAndPersist(
|
||||
dynamic ref, {
|
||||
String? existingAssistantId,
|
||||
@@ -926,7 +926,11 @@ Future<String> _preseedAssistantAndPersist(
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Sync conversation state to ensure WebUI can load conversation history
|
||||
// Sync conversation state to establish the full message structure on the server.
|
||||
// The server's upsert only sets parentId and model - we need to set role,
|
||||
// timestamp, childrenIds, etc. for proper message rendering.
|
||||
// Note: syncConversationMessages always sets done:true to prevent broken UI
|
||||
// if streaming is interrupted (see api_service.dart).
|
||||
try {
|
||||
final api = ref.read(apiServiceProvider);
|
||||
final activeConv = ref.read(activeConversationProvider);
|
||||
|
||||
Reference in New Issue
Block a user