diff --git a/internal/service/chat_pipeline.go b/internal/service/chat_pipeline.go index 35f4593990..9b4e04e0a2 100644 --- a/internal/service/chat_pipeline.go +++ b/internal/service/chat_pipeline.go @@ -849,7 +849,11 @@ func (s *ChatPipelineService) AsyncChat( // Two results are yielded (mirroring Python dialog_service.py): // 1. Final=false — carries the answer text so streaming consumers // actually display the fallback message. - // 2. Final=true — closes the stream with the reference/prompt. + // 2. Final=true — closes the stream with the same full answer plus + // the reference/prompt. Python yields the full answer again in the + // final event (dialog_service.py:807); consumers that only look at + // the final event (e.g. the OpenAI-compatible endpoint) would + // otherwise see an empty reply. if len(knowledges) == 0 { if emptyResp, ok := promptConfig["empty_response"].(string); ok && emptyResp != "" { out <- AsyncChatResult{ @@ -857,7 +861,7 @@ func (s *ChatPipelineService) AsyncChat( Reference: map[string]interface{}{}, } out <- AsyncChatResult{ - Answer: "", + Answer: emptyResp, Reference: kbinfos, AudioBinary: s.synthesizeTTS(ttsModel, emptyResp), Prompt: fmt.Sprintf("\n\n### Query:\n%s", strings.Join(questions, " ")), diff --git a/internal/service/chat_session.go b/internal/service/chat_session.go index 5eaabc4553..8afedc3f15 100644 --- a/internal/service/chat_session.go +++ b/internal/service/chat_session.go @@ -1311,7 +1311,12 @@ func (s *ChatSessionService) Completion(userID string, conversationID string, me var answer strings.Builder var finalRef map[string]interface{} for result := range resultChan { - if result.Answer != "" { + if result.Final && result.Answer != "" { + // The final event carries the complete (decorated) answer; + // it replaces any accumulated deltas rather than appending. + answer.Reset() + answer.WriteString(result.Answer) + } else if result.Answer != "" { answer.WriteString(result.Answer) } if result.Reference != nil { @@ -1673,7 +1678,12 @@ func (s *ChatSessionService) ChatCompletions( var answer strings.Builder var finalRef map[string]interface{} for result := range resultChan { - if result.Answer != "" { + if result.Final && result.Answer != "" { + // The final event carries the complete (decorated) answer; + // it replaces any accumulated deltas rather than appending. + answer.Reset() + answer.WriteString(result.Answer) + } else if result.Answer != "" { answer.WriteString(result.Answer) } if result.Reference != nil { diff --git a/internal/service/chat_session_test.go b/internal/service/chat_session_test.go index 795fc1d327..fea16bbc0a 100644 --- a/internal/service/chat_session_test.go +++ b/internal/service/chat_session_test.go @@ -822,7 +822,9 @@ func TestCompletion_Success(t *testing.T) { pipeline := &fakePipeline{ resultChan: makeResultChan( AsyncChatResult{Answer: "Hello", Reference: map[string]interface{}{"chunks": []interface{}{}}}, - AsyncChatResult{Answer: " world", Final: true, Reference: map[string]interface{}{"chunks": []interface{}{}}}, + // The real pipeline's final result carries the complete answer, + // not a trailing delta. + AsyncChatResult{Answer: "Hello world", Final: true, Reference: map[string]interface{}{"chunks": []interface{}{}}}, ), } diff --git a/web/src/pages/next-chats/utils.ts b/web/src/pages/next-chats/utils.ts index 6e1b55c2f9..b3d57b44d2 100644 --- a/web/src/pages/next-chats/utils.ts +++ b/web/src/pages/next-chats/utils.ts @@ -40,9 +40,20 @@ export const buildMessageItemReference = ( const referenceIndex = assistantMessages.findIndex( (x) => x.id === message.id, ); + // An assistant message that has not received any content yet is still + // being generated. Never resolve its reference from the conversation + // reference list — the indices only align with completed answers, so the + // lookup would surface a stale reference from a previous turn (or from a + // previously opened conversation) while the answer is streaming. + const isPendingAnswer = + message.role === MessageType.Assistant && + !message.content && + isEmpty(message?.reference); const reference = !isEmpty(message?.reference) ? message?.reference - : (conversation?.reference ?? [])[referenceIndex]; + : isPendingAnswer + ? undefined + : (conversation?.reference ?? [])[referenceIndex]; return reference ?? { doc_aggs: [], chunks: [], total: 0 }; };