From 2585526e3f6ac15179e2d4d487b267147ec69438 Mon Sep 17 00:00:00 2001 From: ulleo Date: Thu, 14 Aug 2025 14:38:20 +0800 Subject: [PATCH] fix: parse JSON error during typing answer in chat --- backend/apps/chat/task/llm.py | 46 +++--- .../src/views/chat/answer/AnalysisAnswer.vue | 110 +++++++------ .../src/views/chat/answer/ChartAnswer.vue | 148 +++++++++--------- .../src/views/chat/answer/PredictAnswer.vue | 124 ++++++++------- .../src/views/chat/component/charts/Pie.ts | 3 - 5 files changed, 211 insertions(+), 220 deletions(-) diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index 95e92fe7..b0b00bc7 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -805,7 +805,7 @@ class LLMService: try: # return id if in_chat: - yield orjson.dumps({'type': 'id', 'id': self.get_record().id}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'id', 'id': self.get_record().id}).decode() + '\n\n' # return title if self.change_title: @@ -814,7 +814,7 @@ class LLMService: rename_object=RenameChat(id=self.get_record().chat_id, brief=self.chat_question.question.strip()[:20])) if in_chat: - yield orjson.dumps({'type': 'brief', 'brief': brief}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'brief', 'brief': brief}).decode() + '\n\n' # select datasource if datasource is none if not self.ds: @@ -823,11 +823,11 @@ class LLMService: for chunk in ds_res: SQLBotLogUtil.info(chunk) if in_chat: - yield orjson.dumps( + yield 'data:' + orjson.dumps( {'content': chunk.get('content'), 'reasoning_content': chunk.get('reasoning_content'), 'type': 'datasource-result'}).decode() + '\n\n' if in_chat: - yield orjson.dumps({'id': self.ds.id, 'datasource_name': self.ds.name, + yield 'data:' + orjson.dumps({'id': self.ds.id, 'datasource_name': self.ds.name, 'engine_type': self.ds.type_name or self.ds.type, 'type': 'datasource'}).decode() + '\n\n' @@ -843,11 +843,11 @@ class LLMService: for chunk in sql_res: full_sql_text += chunk.get('content') if in_chat: - yield orjson.dumps( + yield 'data:' + orjson.dumps( {'content': chunk.get('content'), 'reasoning_content': chunk.get('reasoning_content'), 'type': 'sql-result'}).decode() + '\n\n' if in_chat: - yield orjson.dumps({'type': 'info', 'msg': 'sql generated'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'info', 'msg': 'sql generated'}).decode() + '\n\n' # filter sql SQLBotLogUtil.info(full_sql_text) @@ -881,7 +881,7 @@ class LLMService: SQLBotLogUtil.info(sql) format_sql = sqlparse.format(sql, reindent=True) if in_chat: - yield orjson.dumps({'content': format_sql, 'type': 'sql'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'content': format_sql, 'type': 'sql'}).decode() + '\n\n' else: yield f'```sql\n{format_sql}\n```\n\n' @@ -889,7 +889,7 @@ class LLMService: result = self.execute_sql(sql=sql) self.save_sql_data(data_obj=result) if in_chat: - yield orjson.dumps({'content': 'execute-success', 'type': 'sql-data'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'content': 'execute-success', 'type': 'sql-data'}).decode() + '\n\n' # generate chart chart_res = self.generate_chart() @@ -897,18 +897,18 @@ class LLMService: for chunk in chart_res: full_chart_text += chunk.get('content') if in_chat: - yield orjson.dumps( + yield 'data:' + orjson.dumps( {'content': chunk.get('content'), 'reasoning_content': chunk.get('reasoning_content'), 'type': 'chart-result'}).decode() + '\n\n' if in_chat: - yield orjson.dumps({'type': 'info', 'msg': 'chart generated'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'info', 'msg': 'chart generated'}).decode() + '\n\n' # filter chart SQLBotLogUtil.info(full_chart_text) chart = self.check_save_chart(res=full_chart_text) SQLBotLogUtil.info(chart) if in_chat: - yield orjson.dumps({'content': orjson.dumps(chart).decode(), 'type': 'chart'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'content': orjson.dumps(chart).decode(), 'type': 'chart'}).decode() + '\n\n' else: data = [] _fields = {} @@ -940,7 +940,7 @@ class LLMService: record = self.finish() if in_chat: - yield orjson.dumps({'type': 'finish'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'finish'}).decode() + '\n\n' else: # todo generate picture if chart['type'] != 'table': @@ -956,7 +956,7 @@ class LLMService: error_msg = orjson.dumps({'message': str(e), 'traceback': traceback.format_exc(limit=1)}).decode() self.save_error(message=error_msg) if in_chat: - yield orjson.dumps({'content': error_msg, 'type': 'error'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'content': error_msg, 'type': 'error'}).decode() + '\n\n' else: yield f'> ❌ **ERROR**\n\n> \n\n> {error_msg}。' @@ -990,37 +990,37 @@ class LLMService: def run_analysis_or_predict_task(self, action_type: str): try: - yield orjson.dumps({'type': 'id', 'id': self.get_record().id}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'id', 'id': self.get_record().id}).decode() + '\n\n' if action_type == 'analysis': # generate analysis analysis_res = self.generate_analysis() for chunk in analysis_res: - yield orjson.dumps( + yield 'data:' + orjson.dumps( {'content': chunk.get('content'), 'reasoning_content': chunk.get('reasoning_content'), 'type': 'analysis-result'}).decode() + '\n\n' - yield orjson.dumps({'type': 'info', 'msg': 'analysis generated'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'info', 'msg': 'analysis generated'}).decode() + '\n\n' - yield orjson.dumps({'type': 'analysis_finish'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'analysis_finish'}).decode() + '\n\n' elif action_type == 'predict': # generate predict analysis_res = self.generate_predict() full_text = '' for chunk in analysis_res: - yield orjson.dumps( + yield 'data:' + orjson.dumps( {'content': chunk.get('content'), 'reasoning_content': chunk.get('reasoning_content'), 'type': 'predict-result'}).decode() + '\n\n' full_text += chunk.get('content') - yield orjson.dumps({'type': 'info', 'msg': 'predict generated'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'info', 'msg': 'predict generated'}).decode() + '\n\n' _data = self.check_save_predict_data(res=full_text) if _data: - yield orjson.dumps({'type': 'predict-success'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'predict-success'}).decode() + '\n\n' else: - yield orjson.dumps({'type': 'predict-failed'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'predict-failed'}).decode() + '\n\n' - yield orjson.dumps({'type': 'predict_finish'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'type': 'predict_finish'}).decode() + '\n\n' self.finish() except Exception as e: @@ -1030,7 +1030,7 @@ class LLMService: else: error_msg = orjson.dumps({'message': str(e), 'traceback': traceback.format_exc(limit=1)}).decode() self.save_error(message=error_msg) - yield orjson.dumps({'content': error_msg, 'type': 'error'}).decode() + '\n\n' + yield 'data:' + orjson.dumps({'content': error_msg, 'type': 'error'}).decode() + '\n\n' finally: # end pass diff --git a/frontend/src/views/chat/answer/AnalysisAnswer.vue b/frontend/src/views/chat/answer/AnalysisAnswer.vue index 34358d5d..8e3f495d 100644 --- a/frontend/src/views/chat/answer/AnalysisAnswer.vue +++ b/frontend/src/views/chat/answer/AnalysisAnswer.vue @@ -98,11 +98,13 @@ const sendMessage = async () => { const controller: AbortController = new AbortController() const response = await chatApi.analysis(currentRecord.analysis_record_id, controller) const reader = response.body.getReader() - const decoder = new TextDecoder() + const decoder = new TextDecoder('utf-8') let analysis_answer = '' let analysis_answer_thinking = '' + let tempResult = '' + while (true) { if (stopFlag.value) { controller.abort() @@ -116,65 +118,61 @@ const sendMessage = async () => { break } - const chunk = decoder.decode(value) - - let _list = [chunk] - - const lines = chunk.trim().split('}\n\n{') - if (lines.length > 1) { - _list = [] - for (let line of lines) { - if (!line.trim().startsWith('{')) { - line = '{' + line.trim() - } - if (!line.trim().endsWith('}')) { - line = line.trim() + '}' - } - _list.push(line) - } + let chunk = decoder.decode(value, { stream: true }) + tempResult += chunk + const split = tempResult.match(/data:.*}\n\n/g) + if (split) { + chunk = split.join('') + tempResult = tempResult.replace(chunk, '') + } else { + continue } - for (const str of _list) { - let data - try { - data = JSON.parse(str) - } catch (err) { - console.error('JSON string:', str) - throw err - } + if (chunk && chunk.startsWith('data:{')) { + if (split) { + for (const str of split) { + let data + try { + data = JSON.parse(str.replace('data:{', '{')) + } catch (err) { + console.error('JSON string:', str) + throw err + } - if (data.code && data.code !== 200) { - ElMessage({ - message: data.msg, - type: 'error', - showClose: true, - }) - _loading.value = false - return - } + if (data.code && data.code !== 200) { + ElMessage({ + message: data.msg, + type: 'error', + showClose: true, + }) + _loading.value = false + return + } - switch (data.type) { - case 'id': - currentRecord.id = data.id - _currentChat.value.records[index.value].id = data.id - break - case 'info': - console.info(data.msg) - break - case 'error': - currentRecord.error = data.content - emits('error') - break - case 'analysis-result': - analysis_answer += data.content - analysis_answer_thinking += data.reasoning_content - _currentChat.value.records[index.value].analysis = analysis_answer - _currentChat.value.records[index.value].analysis_thinking = analysis_answer_thinking - break - case 'analysis_finish': - emits('finish', currentRecord.id) - break + switch (data.type) { + case 'id': + currentRecord.id = data.id + _currentChat.value.records[index.value].id = data.id + break + case 'info': + console.info(data.msg) + break + case 'error': + currentRecord.error = data.content + emits('error') + break + case 'analysis-result': + analysis_answer += data.content + analysis_answer_thinking += data.reasoning_content + _currentChat.value.records[index.value].analysis = analysis_answer + _currentChat.value.records[index.value].analysis_thinking = analysis_answer_thinking + break + case 'analysis_finish': + emits('finish', currentRecord.id) + break + } + await nextTick() + } } - await nextTick() } } } catch (error) { diff --git a/frontend/src/views/chat/answer/ChartAnswer.vue b/frontend/src/views/chat/answer/ChartAnswer.vue index 1a0f1c3e..deeb6a78 100644 --- a/frontend/src/views/chat/answer/ChartAnswer.vue +++ b/frontend/src/views/chat/answer/ChartAnswer.vue @@ -104,11 +104,13 @@ const sendMessage = async () => { } const response = await questionApi.add(param, controller) const reader = response.body.getReader() - const decoder = new TextDecoder() + const decoder = new TextDecoder('utf-8') let sql_answer = '' let chart_answer = '' + let tempResult = '' + while (true) { if (stopFlag.value) { controller.abort() @@ -121,84 +123,80 @@ const sendMessage = async () => { break } - const chunk = decoder.decode(value) - - let _list = [chunk] - - const lines = chunk.trim().split('}\n\n{') - if (lines.length > 1) { - _list = [] - for (let line of lines) { - if (!line.trim().startsWith('{')) { - line = '{' + line.trim() - } - if (!line.trim().endsWith('}')) { - line = line.trim() + '}' - } - _list.push(line) - } + let chunk = decoder.decode(value, { stream: true }) + tempResult += chunk + const split = tempResult.match(/data:.*}\n\n/g) + if (split) { + chunk = split.join('') + tempResult = tempResult.replace(chunk, '') + } else { + continue } - for (const str of _list) { - let data - try { - data = JSON.parse(str) - } catch (err) { - console.error('JSON string:', str) - throw err - } + if (chunk && chunk.startsWith('data:{')) { + if (split) { + for (const str of split) { + let data + try { + data = JSON.parse(str.replace('data:{', '{')) + } catch (err) { + console.error('JSON string:', str) + throw err + } - if (data.code && data.code !== 200) { - ElMessage({ - message: data.msg, - type: 'error', - showClose: true, - }) - _loading.value = false - return - } + if (data.code && data.code !== 200) { + ElMessage({ + message: data.msg, + type: 'error', + showClose: true, + }) + _loading.value = false + return + } - switch (data.type) { - case 'id': - currentRecord.id = data.id - _currentChat.value.records[index.value].id = data.id - break - case 'info': - console.info(data.msg) - break - case 'brief': - _currentChat.value.brief = data.brief - _chatList.value.forEach((c: Chat) => { - if (c.id === _currentChat.value.id) { - c.brief = _currentChat.value.brief - } - }) - break - case 'error': - currentRecord.error = data.content - emits('error') - break - case 'sql-result': - sql_answer += data.reasoning_content - _currentChat.value.records[index.value].sql_answer = sql_answer - break - case 'sql': - _currentChat.value.records[index.value].sql = data.content - break - case 'sql-data': - getChatData(_currentChat.value.records[index.value].id) - break - case 'chart-result': - chart_answer += data.reasoning_content - _currentChat.value.records[index.value].chart_answer = chart_answer - break - case 'chart': - _currentChat.value.records[index.value].chart = data.content - break - case 'finish': - emits('finish', currentRecord.id) - break + switch (data.type) { + case 'id': + currentRecord.id = data.id + _currentChat.value.records[index.value].id = data.id + break + case 'info': + console.info(data.msg) + break + case 'brief': + _currentChat.value.brief = data.brief + _chatList.value.forEach((c: Chat) => { + if (c.id === _currentChat.value.id) { + c.brief = _currentChat.value.brief + } + }) + break + case 'error': + currentRecord.error = data.content + emits('error') + break + case 'sql-result': + sql_answer += data.reasoning_content + _currentChat.value.records[index.value].sql_answer = sql_answer + break + case 'sql': + _currentChat.value.records[index.value].sql = data.content + break + case 'sql-data': + getChatData(_currentChat.value.records[index.value].id) + break + case 'chart-result': + chart_answer += data.reasoning_content + _currentChat.value.records[index.value].chart_answer = chart_answer + break + case 'chart': + _currentChat.value.records[index.value].chart = data.content + break + case 'finish': + emits('finish', currentRecord.id) + break + } + await nextTick() + } } - await nextTick() } } } catch (error) { diff --git a/frontend/src/views/chat/answer/PredictAnswer.vue b/frontend/src/views/chat/answer/PredictAnswer.vue index 8792138f..df892947 100644 --- a/frontend/src/views/chat/answer/PredictAnswer.vue +++ b/frontend/src/views/chat/answer/PredictAnswer.vue @@ -99,11 +99,13 @@ const sendMessage = async () => { const controller: AbortController = new AbortController() const response = await chatApi.predict(currentRecord.predict_record_id, controller) const reader = response.body.getReader() - const decoder = new TextDecoder() + const decoder = new TextDecoder('utf-8') let predict_answer = '' let predict_content = '' + let tempResult = '' + while (true) { if (stopFlag.value) { controller.abort() @@ -117,72 +119,68 @@ const sendMessage = async () => { break } - const chunk = decoder.decode(value) - - let _list = [chunk] - - const lines = chunk.trim().split('}\n\n{') - if (lines.length > 1) { - _list = [] - for (let line of lines) { - if (!line.trim().startsWith('{')) { - line = '{' + line.trim() - } - if (!line.trim().endsWith('}')) { - line = line.trim() + '}' - } - _list.push(line) - } + let chunk = decoder.decode(value, { stream: true }) + tempResult += chunk + const split = tempResult.match(/data:.*}\n\n/g) + if (split) { + chunk = split.join('') + tempResult = tempResult.replace(chunk, '') + } else { + continue } - for (const str of _list) { - let data - try { - data = JSON.parse(str) - } catch (err) { - console.error('JSON string:', str) - throw err - } + if (chunk && chunk.startsWith('data:{')) { + if (split) { + for (const str of split) { + let data + try { + data = JSON.parse(str.replace('data:{', '{')) + } catch (err) { + console.error('JSON string:', str) + throw err + } - if (data.code && data.code !== 200) { - ElMessage({ - message: data.msg, - type: 'error', - showClose: true, - }) - return - } + if (data.code && data.code !== 200) { + ElMessage({ + message: data.msg, + type: 'error', + showClose: true, + }) + return + } - switch (data.type) { - case 'id': - currentRecord.id = data.id - _currentChat.value.records[index.value].id = data.id - break - case 'info': - console.info(data.msg) - break - case 'error': - currentRecord.error = data.content - emits('error') - break - case 'predict-result': - predict_answer += data.reasoning_content - predict_content += data.content - _currentChat.value.records[index.value].predict = predict_answer - _currentChat.value.records[index.value].predict_content = predict_content - break - case 'predict-failed': - emits('error') - break - case 'predict-success': - //currentChat.value.records[_index].predict_data = data.content - getChatPredictData(_currentChat.value.records[index.value].id) - emits('finish', currentRecord.id) - break - case 'predict_finish': - _loading.value = false - break + switch (data.type) { + case 'id': + currentRecord.id = data.id + _currentChat.value.records[index.value].id = data.id + break + case 'info': + console.info(data.msg) + break + case 'error': + currentRecord.error = data.content + emits('error') + break + case 'predict-result': + predict_answer += data.reasoning_content + predict_content += data.content + _currentChat.value.records[index.value].predict = predict_answer + _currentChat.value.records[index.value].predict_content = predict_content + break + case 'predict-failed': + emits('error') + break + case 'predict-success': + //currentChat.value.records[_index].predict_data = data.content + getChatPredictData(_currentChat.value.records[index.value].id) + emits('finish', currentRecord.id) + break + case 'predict_finish': + _loading.value = false + break + } + await nextTick() + } } - await nextTick() } } } catch (error) { diff --git a/frontend/src/views/chat/component/charts/Pie.ts b/frontend/src/views/chat/component/charts/Pie.ts index 8faf28e2..433b1f05 100644 --- a/frontend/src/views/chat/component/charts/Pie.ts +++ b/frontend/src/views/chat/component/charts/Pie.ts @@ -37,9 +37,6 @@ export class Pie extends BaseG2Chart { { position: 'outside', text: (data: any) => { - console.log( - `${data[series[0].value]}: ${data[y[0].value]}${_data.isPercent ? '%' : ''}` - ) return `${data[series[0].value]}: ${data[y[0].value]}${_data.isPercent ? '%' : ''}` }, transform: [{ type: 'exceedAdjust' }, { type: 'overlapHide' }],