Fix stale references shown while streaming and empty final reply in Go mode (#17146)

This commit is contained in:
euvre
2026-07-21 14:53:40 +08:00
committed by GitHub
parent bb6b43b5c9
commit f7a5629487
4 changed files with 33 additions and 6 deletions

View File

@@ -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, " ")),

View File

@@ -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 {

View File

@@ -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{}{}}},
),
}

View File

@@ -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 };
};