Skip to content

Commit

Permalink
feat(answer): error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Danny Gauthier authored and Danny Gauthier committed Jul 4, 2024
1 parent 8847e7b commit c2a4ec9
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 79 deletions.
146 changes: 68 additions & 78 deletions packages/headless/src/api/knowledge/stream-answer-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
EventSourceMessage,
fetchEventSource,
} from '@microsoft/fetch-event-source';
import {selectFieldsToIncludeInCitation} from '../../features/generated-answer/generated-answer-selectors';
import {
GeneratedAnswerStyle,
GeneratedContentFormat,
Expand All @@ -30,18 +31,12 @@ import {answerSlice} from './answer-slice';
type StateNeededByAnswerAPI = ConfigurationSection &
GeneratedAnswerSection &
SearchSection &
DebugSection & {answer: ReturnType<typeof answerApi.reducer>};

interface ErrorPayload {
message?: string;
code?: number;
}

class FatalError extends Error {
constructor(public payload: ErrorPayload) {
super(payload.message);
}
}
DebugSection & {
answer: ReturnType<typeof answerApi.reducer>;
query?: QueryState;
searchHub?: string;
pipeline?: string;
};

interface GeneratedAnswerStream {
answerStyle: GeneratedAnswerStyle | undefined;
Expand All @@ -51,6 +46,7 @@ interface GeneratedAnswerStream {
generated: boolean;
isStreaming: boolean;
isLoading: boolean;
error: {message: string; code: number} | undefined;
}
interface HeaderMessage {
answerStyle: GeneratedAnswerStyle;
Expand All @@ -73,7 +69,7 @@ const messageSchema = new Schema({
});

const citationsSchema = new Schema({
citation: new ArrayValue(
citations: new ArrayValue(
new RecordValue({
values: {
clickUri: new StringValue(),
Expand All @@ -96,7 +92,7 @@ const validateMessage = (message: {textDelta: string}) => {
};

const validateCitationsMessage = (citations: {
citation: GeneratedAnswerCitation[];
citations: GeneratedAnswerCitation[];
}) => {
citationsSchema.validate(citations);
};
Expand Down Expand Up @@ -133,10 +129,10 @@ const handleMessage = (

const handleCitations = (
draft: GeneratedAnswerStream,
payload: {citation: GeneratedAnswerCitation[]}
payload: {citations: GeneratedAnswerCitation[]}
) => {
validateCitationsMessage(payload);
draft.citations = payload.citation;
draft.citations = payload.citations;
};

const handleEndOfStream = (
Expand All @@ -148,14 +144,37 @@ const handleEndOfStream = (
draft.isStreaming = false;
};

interface MessageType {
payloadType: PayloadType;
payload: string;
finishReason?: string;
errorMessage?: string;
code?: number;
}

const handleError = (draft: GeneratedAnswerStream, message: MessageType) => {
draft.error = {
message: message.errorMessage!,
code: message.code!,
};
draft.isStreaming = false;
draft.isLoading = false;
// Throwing an error here breaks the client and prevents the error from reaching the state.
console.error(`${message.errorMessage} - code ${message.code}`);
};

const updateCacheWithEvent = (
event: EventSourceMessage,
draft: GeneratedAnswerStream
) => {
const message: {payloadType: PayloadType; payload: string} = JSON.parse(
event.data
);
const parsedPayload = JSON.parse(message.payload);
const message: MessageType = JSON.parse(event.data);
if (message.finishReason === 'ERROR' && message.errorMessage) {
handleError(draft, message);
}

const parsedPayload = message.payload.length
? JSON.parse(message.payload)
: {};
switch (message.payloadType) {
case 'genqa.headerMessageType':
handleHeaderMessage(draft, parsedPayload);
Expand All @@ -172,33 +191,6 @@ const updateCacheWithEvent = (
}
};

const onOpenStream = async (response: Response) => {
if (
response.ok &&
response.headers.get('content-type')?.includes('text/event-stream')
) {
return;
}

const isClientSideError =
response.status >= 400 && response.status < 500 && response.status !== 429;

if (isClientSideError) {
throw new FatalError({
message: 'Error opening stream',
code: response.status,
});
} else {
throw new Error();
}
};

const onError = (err: Error) => {
if (err instanceof FatalError) {
throw err;
}
};

export const answerApi = answerSlice.injectEndpoints({
overrideExisting: true,
endpoints: (builder) => ({
Expand All @@ -209,6 +201,7 @@ export const answerApi = answerSlice.injectEndpoints({
contentFormat: undefined,
answer: undefined,
citations: undefined,
error: undefined,
generated: false,
isStreaming: true,
isLoading: true,
Expand All @@ -224,11 +217,11 @@ export const answerApi = answerSlice.injectEndpoints({
* It cannot use the inferred state used by Redux, thus the casting.
* https://redux-toolkit.js.org/rtk-query/usage-with-typescript#typing-dispatch-and-getstate
*/
const {configuration} = getState() as unknown as StateNeededByAnswerAPI;
const {platformUrl, organizationId, accessToken, knowledge} =
configuration;
const {configuration, generatedAnswer} =
getState() as unknown as StateNeededByAnswerAPI;
const {platformUrl, organizationId, accessToken} = configuration;
await fetchEventSource(
`${platformUrl}/rest/organizations/${organizationId}/answer/v1/configs/${knowledge.answerConfigurationId}/generate`,
`${platformUrl}/rest/organizations/${organizationId}/answer/v1/configs/${generatedAnswer.answerConfigurationId}/generate`,
{
method: 'POST',
body: JSON.stringify(args),
Expand All @@ -239,49 +232,46 @@ export const answerApi = answerSlice.injectEndpoints({
'Accept-Encoding': '*',
},
fetch,
onopen: onOpenStream,
onmessage: (event) => {
updateCachedData((draft) => {
updateCacheWithEvent(event, draft);
});
},
onerror: onError,
onerror: (error) => {
throw error;
},
}
);
},
}),
}),
});

export const fetchAnswer = (
state: StateNeededByAnswerAPI & {
knowledge: ReturnType<typeof answerApi.reducer>;
query?: QueryState;
searchHub?: string;
pipeline?: string;
}
) => {
const query = selectQuery(state)?.q;
const constructAnswerQueryParams = (state: StateNeededByAnswerAPI) => {
const q = selectQuery(state)?.q;
const searchHub = selectSearchHub(state);
const pipeline = selectPipeline(state);
const citationsFieldToInclude = selectFieldsToIncludeInCitation(state) ?? [];

return answerApi.endpoints.getAnswer.initiate({
q: query,
return {
q,
pipelineRuleParameters: {
mlGenerativeQuestionAnswering: {
responseFormat: {
answerStyle: state.generatedAnswer.responseFormat.answerStyle,
},
citationsFieldToInclude,
},
},
...(searchHub?.length && {searchHub}),
...(pipeline?.length && {pipeline}),
});
};
};

export const selectAnswer = (
state: StateNeededByAnswerAPI & {
knowledge: ReturnType<typeof answerApi.reducer>;
query?: QueryState;
searchHub?: string;
pipeline?: string;
}
) =>
answerApi.endpoints.getAnswer.select({
q: selectQuery(state)?.q,
...(selectSearchHub(state)?.length && {searchHub: selectSearchHub(state)}),
...(selectPipeline(state)?.length && {pipeline: selectPipeline(state)}),
})(state);
export const fetchAnswer = (state: StateNeededByAnswerAPI) =>
answerApi.endpoints.getAnswer.initiate(constructAnswerQueryParams(state));

export const selectAnswer = (state: StateNeededByAnswerAPI) =>
answerApi.endpoints.getAnswer.select(constructAnswerQueryParams(state))(
state
);
3 changes: 2 additions & 1 deletion packages/headless/src/app/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '@reduxjs/toolkit';
import {Logger} from 'pino';
import {getRelayInstanceFromState} from '../api/analytics/analytics-relay-client';
import {answerApi} from '../api/knowledge/stream-answer-api';
import {
disableAnalytics,
enableAnalytics,
Expand Down Expand Up @@ -398,7 +399,7 @@ function createMiddleware<Reducers extends ReducersMapObject>(
renewTokenMiddleware,
logActionErrorMiddleware(logger),
analyticsMiddleware,
].concat(options.middlewares || []);
].concat(answerApi.middleware, options.middlewares || []);
}

function shouldWarnAboutOrganizationEndpoints(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ export interface GeneratedAnswerProps {
*/
expanded?: boolean;
};
/**
* The answer configuration ID used to leverage coveo answer management capabilities.
*/
answerConfigurationId?: string;
/**
* A list of indexed fields to include in the citations returned with the generated answer.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ export const updateResponseFormat = createAction(
})
);

export const updateAnswerConfigurationId = createAction(
'knowledge/updateAnswerConfigurationId',
(payload: string) => validatePayload(payload, stringValue)
);

export const registerFieldsToIncludeInCitations = createAction(
'generatedAnswer/registerFieldsToIncludeInCitations',
(payload: string[]) => validatePayload<string[]>(payload, nonEmptyStringArray)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {createSelector} from '@reduxjs/toolkit';
import {SearchAppState} from '../../state/search-app-state';
import {GeneratedAnswerSection} from '../../state/state-sections';

Expand All @@ -15,3 +16,9 @@ export function generativeQuestionAnsweringIdSelector(
) {
return state.search?.response?.extendedResults?.generativeQuestionAnsweringId;
}

export const selectFieldsToIncludeInCitation = createSelector(
(state: Partial<GeneratedAnswerSection>) =>
state.generatedAnswer?.fieldsToIncludeInCitations,
(fieldsToInclude) => fieldsToInclude
);
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
setIsAnswerGenerated,
expandGeneratedAnswer,
collapseGeneratedAnswer,
updateAnswerConfigurationId,
} from './generated-answer-actions';
import {getGeneratedAnswerInitialState} from './generated-answer-state';

Expand Down Expand Up @@ -111,4 +112,7 @@ export const generatedAnswerReducer = createReducer(
.addCase(collapseGeneratedAnswer, (state) => {
state.expanded = false;
})
.addCase(updateAnswerConfigurationId, (state, {payload}) => {
state.answerConfigurationId = payload;
})
);
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export interface GeneratedAnswerState {
* Whether the answer is expanded.
*/
expanded: boolean;
/**
* The answer configuration unique identifier.
*/
answerConfigurationId?: string;
}

export function getGeneratedAnswerInitialState(): GeneratedAnswerState {
Expand Down

0 comments on commit c2a4ec9

Please sign in to comment.