Skip to content

Commit

Permalink
fix(headless): send end of stream event even when no answer is genera…
Browse files Browse the repository at this point in the history
…ted (#4801)

https://coveord.atlassian.net/browse/SVCC-4482

This is a backport of #4800 for v2.

When perfoming a CRGA request, an "end of stream" Usage Analytics event
should be sent even when no answer is generated. However, when sending
the request to the Answer API, the "end of stream" Usage Analytics event
is sent only when an answer is generated.

This PR fixes the issue. In addition, tests have been added to cover the
handling of the various event source messages.
  • Loading branch information
lbergeron authored Jan 6, 2025
1 parent d0efeb8 commit c3aa7c6
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 8 deletions.
12 changes: 5 additions & 7 deletions packages/headless/src/api/knowledge/stream-answer-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const handleError = (
console.error(`${message.errorMessage} - code ${message.code}`);
};

const updateCacheWithEvent = (
export const updateCacheWithEvent = (
event: EventSourceMessage,
draft: GeneratedAnswerStream,
dispatch: ThunkDispatch<StateNeededByAnswerAPI, unknown, UnknownAction>
Expand Down Expand Up @@ -161,12 +161,10 @@ const updateCacheWithEvent = (
}
break;
case 'genqa.endOfStreamType':
if (draft.answer?.length || parsedPayload.answerGenerated) {
handleEndOfStream(draft, parsedPayload);
dispatch(
logGeneratedAnswerStreamEnd(parsedPayload.answerGenerated ?? false)
);
}
handleEndOfStream(draft, parsedPayload);
dispatch(
logGeneratedAnswerStreamEnd(parsedPayload.answerGenerated ?? false)
);
break;
}
};
Expand Down
196 changes: 195 additions & 1 deletion packages/headless/src/api/knowledge/tests/stream-answer-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {constructAnswerQueryParams} from '../stream-answer-api';
import {EventSourceMessage} from '@microsoft/fetch-event-source';
import {
constructAnswerQueryParams,
GeneratedAnswerStream,
updateCacheWithEvent,
} from '../stream-answer-api';
import {
expectedStreamAnswerAPIParam,
streamAnswerAPIStateMock,
Expand Down Expand Up @@ -34,4 +39,193 @@ describe('#streamAnswerApi', () => {
expect(queryParams).toEqual(expectedStreamAnswerAPIParam);
});
});

describe('updateCacheWithEvent', () => {
const buildEvent = (data: Record<string, any>): EventSourceMessage => {
return {
id: '001',
event: 'test',
data: JSON.stringify(data),
};
};

const buildSuccessEvent = (data: {
payloadType: string;
payload: Record<string, any>;
}): EventSourceMessage => {
return {
id: '001',
event: 'test',
data: JSON.stringify(
Object.assign(
{
payloadType: data.payloadType,
payload: JSON.stringify(data.payload),
},
{
finishReason: 'success',
errorMessage: '',
code: 200,
}
)
),
};
};

const buildDefaultDraft = (
draft: Record<string, any> = {}
): GeneratedAnswerStream =>
Object.assign(
{...draft},
{
isLoading: false,
isStreaming: false,
}
);

it('should handle the error when finishReason is ERROR', () => {
const dispatch = jest.fn();
const event: EventSourceMessage = buildEvent({
finishReason: 'ERROR',
errorMessage: 'some error',
code: 500,
payload: '',
payloadType: 'genqa.messageType',
});

const draft = buildDefaultDraft();

updateCacheWithEvent(event, draft, dispatch);

expect(draft).toHaveProperty('error', {
message: 'some error',
code: 500,
});
expect(draft).toHaveProperty('isStreaming', false);
expect(draft).toHaveProperty('isLoading', false);
});

it('should handle header message type', () => {
const dispatch = jest.fn();
const event = buildSuccessEvent({
payloadType: 'genqa.headerMessageType',
payload: {contentFormat: 'text/markdown', answerStyle: 'default'},
});
const draft = buildDefaultDraft();

updateCacheWithEvent(event, draft, dispatch);

expect(draft).toHaveProperty('contentFormat', 'text/markdown');
expect(draft).toHaveProperty('isStreaming', true);
expect(draft).toHaveProperty('isLoading', false);
expect(dispatch).toHaveBeenCalledWith({
type: 'generatedAnswer/setAnswerContentFormat',
payload: 'text/markdown',
});
});

it('should handle the message type', () => {
const dispatch = jest.fn();
const event = buildSuccessEvent({
payloadType: 'genqa.messageType',
payload: {
textDelta: 'some answer',
},
});
const draft = buildDefaultDraft({answer: undefined});

updateCacheWithEvent(event, draft, dispatch);

expect(draft).toHaveProperty('answer', 'some answer');
expect(dispatch).toHaveBeenCalledWith({
type: 'generatedAnswer/updateMessage',
payload: {
textDelta: 'some answer',
},
});
});

it('should handle message type and append answer', () => {
const dispatch = jest.fn();
const event = buildSuccessEvent({
payloadType: 'genqa.messageType',
payload: {
textDelta: 'with some more info',
},
});
const draft = buildDefaultDraft({answer: 'some answer '});

updateCacheWithEvent(event, draft, dispatch);

expect(draft).toHaveProperty('answer', 'some answer with some more info');
expect(dispatch).toHaveBeenCalledWith({
type: 'generatedAnswer/updateMessage',
payload: {
textDelta: 'with some more info',
},
});
});

it('should handle citations message', () => {
const dispatch = jest.fn();
const citation = {
id: '1',
permanentid: '1',
source: 'source',
title: 'some title',
uri: 'some uri',
};
const event = buildSuccessEvent({
payloadType: 'genqa.citationsType',
payload: {
citations: [citation],
},
});
const draft = buildDefaultDraft();

updateCacheWithEvent(event, draft, dispatch);

expect(draft).toHaveProperty('citations', [citation]);
expect(dispatch).toHaveBeenCalledWith({
type: 'generatedAnswer/updateCitations',
payload: {
citations: [citation],
},
});
});

it('should handle end of stream message when answer is generated', () => {
const dispatch = jest.fn();
const event = buildSuccessEvent({
payloadType: 'genqa.endOfStreamType',
payload: {
answerGenerated: true,
},
});
const draft = buildDefaultDraft({answer: 'some answer'});

updateCacheWithEvent(event, draft, dispatch);

expect(draft).toHaveProperty('generated', true);
expect(draft).toHaveProperty('isStreaming', false);
expect(dispatch).toHaveBeenCalled();
});

it('should handle end of stream message when answer is not generated', () => {
const dispatch = jest.fn();
const event = buildSuccessEvent({
payloadType: 'genqa.endOfStreamType',
payload: {
answerGenerated: false,
},
});
const draft = buildDefaultDraft();

updateCacheWithEvent(event, draft, dispatch);

expect(draft).toHaveProperty('generated', false);
expect(draft).toHaveProperty('isStreaming', false);
expect(dispatch).toHaveBeenCalled();
});
});
});

0 comments on commit c3aa7c6

Please sign in to comment.