A Coveo Machine Learning (Coveo ML) Relevance Generative Answering (RGA) model generates answers to complex natural language user queries in a Coveo-powered search interface.
The RGA model leverages generative AI technology to generate an answer based solely on the content that you specify, such as your enterprise content. The content resides in a secure index in your Coveo organization.
This article guides you through the process of integrating Coveo RGA in a front-end app with the @coveo/headless library.
It focuses on a React implementation, but the basics of using the library in Angular and vanilla JavaScript will be covered.
If you’re looking to jump into code, a great place to start is with the following React quickstart.
import {
buildGeneratedAnswer,
buildInteractiveCitation,
buildSearchEngine,
type GeneratedAnswerCitation,
type GeneratedAnswerState,
getSampleSearchEngineConfiguration,
type InteractiveCitation,
loadQueryActions,
loadSearchActions,
loadSearchAnalyticsActions,
type QueryActionCreators,
type SearchActionCreators,
type SearchAnalyticsActionCreators,
type SearchEngine,
type SearchEngineOptions,
} from '@coveo/headless';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
export const AnswerGenerator = () => {
const [rgaState, setRgaState] = useState<GeneratedAnswerState | undefined>();
const inputRef = useRef<HTMLInputElement>(null);
const engine: SearchEngine = useMemo(() => {
const searchEngineOptions: SearchEngineOptions = {
configuration: {
...getSampleSearchEngineConfiguration(),
},
};
return buildSearchEngine(searchEngineOptions);
}, []);
const {generatedAnswer, updateQuery, logSearchboxSubmit, executeSearch} =
useMemo(() => {
const {updateQuery}: QueryActionCreators = loadQueryActions(engine);
const {
logInterfaceLoad,
logSearchboxSubmit,
}: SearchAnalyticsActionCreators = loadSearchAnalyticsActions(engine);
const {executeSearch}: SearchActionCreators = loadSearchActions(engine);
const generatedAnswer = buildGeneratedAnswer(engine);
engine.dispatch(executeSearch(logInterfaceLoad()));
return {
generatedAnswer,
updateQuery,
logSearchboxSubmit,
executeSearch,
};
}, [engine]);
const submit = useCallback(() => {
engine.dispatch(updateQuery({q: inputRef.current?.value}));
engine.dispatch(executeSearch(logSearchboxSubmit()));
}, [engine, updateQuery, logSearchboxSubmit, executeSearch, inputRef]);
useEffect(() => {
const unsubscribe = generatedAnswer.subscribe(() =>
setRgaState(generatedAnswer.state)
);
return unsubscribe;
}, [generatedAnswer, setRgaState]);
return (
<>
{rgaState?.isLoading && <div>Loading...</div>}
<input
type="text"
defaultValue={''}
id="searchInput"
ref={inputRef}
disabled={rgaState?.isLoading}
/>
<button type="button" onClick={submit}>
Submit
</button>
{!rgaState?.isLoading && Boolean(rgaState?.answer) && (
<div>
<span className={`${rgaState?.isStreaming && 'rga-typing'} rga-text`}>
{rgaState?.answer}
</span>
{!rgaState?.isStreaming && (
<CitationsList
citations={rgaState?.citations}
searchEngine={engine}
isStreaming={Boolean(rgaState?.isStreaming)}
/>
)}
</div>
)}
</>
);
};
type TCitationsList = {
citations?: GeneratedAnswerState['citations'];
isStreaming: GeneratedAnswerState['isStreaming'];
searchEngine: SearchEngine;
};
const CitationsList = ({
citations,
isStreaming,
searchEngine,
}: TCitationsList) => {
if (!citations || isStreaming) return;
return (
<div>
{citations.map((citation: GeneratedAnswerCitation) => (
<Citation
key={citation.id}
citation={citation}
searchEngine={searchEngine}
/>
))}
</div>
);
};
type TCitation = {
citation: GeneratedAnswerCitation;
searchEngine: SearchEngine;
};
const Citation = ({citation, searchEngine}: TCitation) => {
const interactiveCitation: InteractiveCitation = buildInteractiveCitation(
searchEngine,
{
options: {
citation,
},
}
);
return (
<button
type="button"
onClick={() => interactiveCitation.select()}
onTouchEnd={() => interactiveCitation.cancelPendingSelect()}
onTouchStart={() => interactiveCitation.beginDelayedSelect()}
>
{citation.title}
</button>
);
};
Additional type references
Currently, only SearchEngine and InsightEngine support RGA.
This document will concentrate on implementing RGA with a SearchEngine, but the interactions with GeneratedAnswer would be the same if you were to use the InsightEngine.
The primary difference between the two implementations are the actions dispatched to the engines.
First, use buildSearchEngine to instantiate the SearchEngine that will be used to create your RGA controller.
/* lib/getSearchEngine.ts */
import {
buildSearchEngine,
getSampleSearchEngineConfiguration,
type SearchEngine,
type SearchEngineOptions,
} from '@coveo/headless';
export const getSearchEngine = (): SearchEngine => {
const searchEngineOptions: SearchEngineOptions = {
configuration: {
...getSampleSearchEngineConfiguration(),
search: {
pipeline: 'genqatest',
},
},
};
return buildSearchEngine(searchEngineOptions);
};
searchEngine controller. The above example is not intended for production use.The RGA controller generates responses based on the context of the query submitted to the engine.
Create the RGA controller by passing a reference to an engine into buildGeneratedAnswer.
You can optionally provide buildGeneratedAnswer with additional configuration options, as defined by GeneratedAnswerProps, to enhance the relevance of the response generated.
The engine is what receives dispatched actions, not the RGA controller (GeneratedAnswer).
This means that any code that uses RGA to generate a response needs a reference to the engine used to create the GeneratedAnswer controller.
/* lib/getAnswerGenerator.ts */
import {
buildGeneratedAnswer,
type GeneratedAnswer,
type GeneratedAnswerProps,
type SearchEngine,
} from '@coveo/headless';
export const getAnswerGenerator = (
engine: SearchEngine,
props?: GeneratedAnswerProps
): GeneratedAnswer => {
return buildGeneratedAnswer(engine, props);
};
In the following example a SearchEngine is created.
The GeneratedAnswer controller is created with that engine.
/* lib/engines.ts */
import type {
GeneratedAnswer,
GeneratedAnswerProps,
SearchEngine,
} from '@coveo/headless';
import {getAnswerGenerator} from './getAnswerGenerator.js';
import {getSearchEngine} from './getSearchEngine.js';
export const headlessEngine: SearchEngine = getSearchEngine();
export const answerGenerator = (
props?: GeneratedAnswerProps
): GeneratedAnswer => {
return getAnswerGenerator(headlessEngine, props);
};
All the underlying functionality of the engine remains available.
The GeneratedAnswer controller is attached to the same engine, so search results are still accessible through engine.state.search while the GeneratedAnswer controller exposes RGA-specific state.
Import the resulting engine into your component and dispatch actions to it.
The @coveo/headless exports functions to create a common set of action creators, you can extend these with your own custom set.
You’ll need the corresponding answerGenerator and to subscribe its state changes to access the RGA response in your component.
/* components/AnswerGenerator.tsx */
import {
type GeneratedAnswerState,
loadQueryActions,
loadSearchActions,
loadSearchAnalyticsActions,
type QueryActionCreators,
type SearchActionCreators,
type SearchAnalyticsActionCreators,
} from '@coveo/headless';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {answerGenerator, headlessEngine} from '../lib/engines.js';
export const AnswerGenerator = () => {
const [rgaState, setRgaState] = useState<GeneratedAnswerState>();
const inputRef = useRef<HTMLInputElement>(null);
const {updateQuery, executeSearch, logSearchboxSubmit, rgaController} =
useMemo(() => {
const rgaController = answerGenerator();
const {updateQuery}: QueryActionCreators =
loadQueryActions(headlessEngine);
const {executeSearch}: SearchActionCreators =
loadSearchActions(headlessEngine);
const {logSearchboxSubmit}: SearchAnalyticsActionCreators =
loadSearchAnalyticsActions(headlessEngine);
return {
rgaController,
updateQuery,
executeSearch,
logSearchboxSubmit,
};
}, [headlessEngine]);
const submitQuestion = useCallback(() => {
headlessEngine.dispatch(updateQuery({q: inputRef.current?.value}));
headlessEngine.dispatch(executeSearch(logSearchboxSubmit()));
}, [updateQuery, executeSearch, logSearchboxSubmit]);
useEffect(() => {
const unsubscribe = rgaController.subscribe(() =>
setRgaState(rgaController.state)
);
return unsubscribe;
}, [setRgaState, rgaController]);
return (
<>
<input
type="text"
defaultValue={''}
id="searchInput"
ref={inputRef}
disabled={rgaState?.isLoading}
/>
<button type="button" onClick={submitQuestion}>
Submit
</button>
{rgaState?.isLoading && <div>Loading...</div>}
<div>{rgaState?.answer}</div>
</>
);
};
loadQueryActions for additional details.loadSearchActions for additional details.loadSearchAnalyticsActions for additional details.GeneratedAnswer controller when the component mounts.The GeneratedAnswer controller provides methods to update its state and track user interactions.
Following is a summary of the available methods, describing their effects on GeneratedAnswerState and whether they trigger analytics:
| Method | Effect on state | Analytics |
|---|---|---|
openFeedbackModal() |
Sets feedbackModalOpen to true. |
❌ |
closeFeedbackModal() |
Sets feedbackModalOpen to false. |
❌ |
like() |
Marks the generated answer as liked. | ✅ Logs like event |
dislike() |
Marks the generated answer as disliked. | ✅ Logs dislike event |
sendFeedback(feedback: GeneratedAnswerFeedback) |
Submits user feedback and sets feedbackSubmitted to true. |
✅ Logs feedback event |
show() |
Marks the answer as visible. | ✅ Logs show event |
hide() |
Marks the answer as hidden. | ✅ Logs hide event |
expand() |
Marks the answer as expanded to show full response. | ✅ Logs expand event |
collapse() |
Marks the answer as collapsed to show partial response. | ✅ Logs collapse event |
enable() |
Enables the generated answer feature. | ❌ |
disable() |
Disables the generated answer feature. | ❌ |
logCitationClick(citationId: string) |
❌ | ✅ Logs citation click event |
logCitationHover(citationId: string, timeMs: number) |
❌ | ✅Logs citation hover event |
logCopyToClipboard() |
❌ | ✅ Logs copy-to-clipboard event |
retry() |
Tries to generate an answer again. | ❌ |
You’ll need to integrate these events into your code to be able to generate reports for RGA in your Coveo Dashboard. You can also find details on what actions are automatically tracked by reviewing Relevance Generative Answering (RGA) reports and UA events.
Along with the generated answer, RGA also returns the sources it used to produce that answer.
These sources, called citations, are available through the citations field of the GeneratedAnswerState.
Each citation follows the GeneratedAnswerCitation interface, which provides useful metadata such as the source title and URI.
Citations allow users to access the original sources that contributed to the generated answer for additional context.
The following example shows how to render a list of citations after the answer has finished streaming.
/* components/CitationsList.tsx */
import type {
GeneratedAnswerCitation,
GeneratedAnswerState,
SearchEngine,
} from '@coveo/headless';
import {Citation} from './Citation.js';
type TCitationsList = {
citations?: GeneratedAnswerState['citations'];
isStreaming: GeneratedAnswerState['isStreaming'];
searchEngine: SearchEngine;
};
export const CitationsList = ({
citations,
isStreaming,
searchEngine,
}: TCitationsList) => {
if (!citations || isStreaming) return;
return (
<div>
{citations.map((citation: GeneratedAnswerCitation) => (
<Citation
key={citation.id}
citation={citation}
searchEngine={searchEngine}
/>
))}
</div>
);
};
GeneratedAnswerCitation entities come from GeneratedAnswerStateisStreaming is part of GeneratedAnswerStateCitations also support analytic events, refer to Citation Event Tracking for details.
Use buildInteractiveCitation to track events on a specific citation.
This allows for the gathering of analytics on which sources of information your users are finding valuable when interacting with generated answers.
Below is a quick reference to the analytic tracking events provided by buildInteractiveCitation.
| Method | Analytics |
|---|---|
beginDelayedSelect() |
✅ Prepares to log selection event |
cancelPendingSelect() |
✅ Cancels the pending selection of beginDelayedSelect |
select() |
✅ Logs the selection event |
In order to track user interactions with results, you need to instantiate the InteractiveCitation with the specific GeneratedAnswerCitation being interacted with.
Once these events have been integrated with your code, they can be accessed in your Coveo RGA reports.
Following is an example of rendering a GeneratedAnswerCitation that implements event tracking.
/* components/Citation.tsx */
import {
buildInteractiveCitation,
type GeneratedAnswerCitation,
type InteractiveCitation,
type SearchEngine,
} from '@coveo/headless';
type TCitation = {
citation: GeneratedAnswerCitation;
searchEngine: SearchEngine;
};
export const Citation = ({citation, searchEngine}: TCitation) => {
const interactiveCitation: InteractiveCitation = buildInteractiveCitation(
searchEngine,
{
options: {
citation,
},
}
);
return (
<button
type="button"
onClick={() => interactiveCitation.select()}
onTouchEnd={() => interactiveCitation.cancelPendingSelect()}
onTouchStart={() => interactiveCitation.beginDelayedSelect()}
>
{citation.title}
</button>
);
};
buildInteractiveCitation to create a set of analytic actions that you can attach to an element containing a citation.Type definitions
The following is a basic implementation of the @coveo/headless RGA controller in an Angular component.
/* src/app/coveo-headless-rga.service.ts */
import {Injectable} from '@angular/core';
import {
buildGeneratedAnswer,
buildSearchBox,
buildSearchEngine,
type GeneratedAnswer,
getSampleSearchEngineConfiguration,
type SearchBox,
type SearchEngine,
} from '@coveo/headless';
@Injectable({providedIn: 'root'})
export class CoveoHeadlessRGAService {
private _engine?: SearchEngine;
initEngine() {
if (this._engine) return this._engine;
this._engine = buildSearchEngine({
configuration: {
...getSampleSearchEngineConfiguration(),
search: {
pipeline: 'genqatest',
},
},
});
return this._engine;
}
get engine(): SearchEngine {
if (!this._engine) {
throw new Error(
'Headless Engine not initialized. Call initEngine() first.'
);
}
return this._engine;
}
buildSearchBox(): SearchBox {
return buildSearchBox(this.engine);
}
buildGeneratedAnswer(): GeneratedAnswer {
return buildGeneratedAnswer(this.engine);
}
}
searchEngine controller. The above example is not intended for production use./* src/app/coveo-headless-rga.component.ts */
import {CommonModule} from '@angular/common';
import {
Component,
inject,
type OnDestroy,
type OnInit,
signal,
} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {
type GeneratedAnswerState,
loadQueryActions,
loadSearchActions,
loadSearchAnalyticsActions,
type SearchBoxState,
} from '@coveo/headless';
import type {CoveoHeadlessRGAService as CoveoHeadlessRGAServiceType} from './coveo-headless-rga.service';
import {CoveoHeadlessRGAService} from './coveo-headless-rga.service';
@Component({
selector: 'app-coveo-search',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './coveo-headless-rga.component.html',
})
export class CoveoSearchComponent implements OnInit, OnDestroy {
private searchBox!: ReturnType<CoveoHeadlessRGAServiceType['buildSearchBox']>;
private generatedAnswer!: ReturnType<
CoveoHeadlessRGAServiceType['buildGeneratedAnswer']
>;
searchBoxState = signal<SearchBoxState>({
isLoading: false,
isLoadingSuggestions: false,
searchBoxId: '',
suggestions: [],
value: '',
});
generatedAnswerState = signal<GeneratedAnswerState | undefined>(undefined);
private unsubscribers: Array<() => void> = [];
private headless = inject(CoveoHeadlessRGAService);
ngOnInit(): void {
this.headless.initEngine();
this.searchBox = this.headless.buildSearchBox();
this.generatedAnswer = this.headless.buildGeneratedAnswer();
const {logInterfaceLoad} = loadSearchAnalyticsActions(this.headless.engine);
const {executeSearch} = loadSearchActions(this.headless.engine);
this.headless.engine.dispatch(executeSearch(logInterfaceLoad()));
this.unsubscribers.push(
this.searchBox.subscribe(() =>
this.searchBoxState.set(this.searchBox.state)
),
this.generatedAnswer.subscribe(() => {
return this.generatedAnswerState.set(this.generatedAnswer.state);
})
);
}
ngOnDestroy(): void {
this.unsubscribers.forEach((u) => u());
}
onSearchSubmit(): void {
const {updateQuery} = loadQueryActions(this.headless.engine);
const {logSearchboxSubmit} = loadSearchAnalyticsActions(
this.headless.engine
);
const {executeSearch} = loadSearchActions(this.headless.engine);
this.headless.engine.dispatch(
updateQuery({q: this.searchBoxState()?.value})
);
this.headless.engine.dispatch(executeSearch(logSearchboxSubmit()));
}
onClear(): void {
const {updateQuery} = loadQueryActions(this.headless.engine);
const {logSearchboxSubmit} = loadSearchAnalyticsActions(
this.headless.engine
);
const {executeSearch} = loadSearchActions(this.headless.engine);
this.headless.engine.dispatch(updateQuery({q: ''}));
this.headless.engine.dispatch(executeSearch(logSearchboxSubmit()));
}
}
<!-- src/app/coveo-headless-rga.component.ts -->
<div class="coveo">
<form class="search-row" (ngSubmit)="onSearchSubmit()">
<input
type="search"
[(ngModel)]="searchBoxState().value"
name="q"
placeholder="Ask your question..."
autocomplete="off"
/>
<button type="submit">Search</button>
<button type="button" class="ghost" (click)="onClear()">Clear</button>
</form>
<div class="layout">
<main class="results">
<div class="empty" *ngIf="generatedAnswerState()?.isLoading === false">
{{generatedAnswerState()?.answer}}
</div>
</main>
</div>
</div>
The following is a basic implementation of the @coveo/headless RGA Controller outside of any JavaScript framework.
import {
buildGeneratedAnswer,
buildSearchEngine,
getSampleSearchEngineConfiguration,
loadQueryActions,
loadSearchActions,
loadSearchAnalyticsActions,
} from 'https://static.cloud.coveo.com/headless/v3/headless.esm.js';
const coveoHeadlessRga = () => {
const SUBMIT_BUTTON_ID = 'fetch-answer';
const QUERY_INPUT_ID = 'query-input';
const RESPONSE_DIV_ID = 'response';
const searchEngine = buildSearchEngine({
configuration: {
...getSampleSearchEngineConfiguration(),
search: {
pipeline: 'genqatest',
},
},
});
const rgaController = buildGeneratedAnswer(searchEngine);
const {updateQuery} = loadQueryActions(searchEngine);
const {executeSearch} = loadSearchActions(searchEngine);
const {logInterfaceLoad, logSearchboxSubmit} =
loadSearchAnalyticsActions(searchEngine);
console.log('rgaController', rgaController);
rgaController.subscribe(() => {
const contentDiv = document.getElementById(RESPONSE_DIV_ID);
console.log('rgaController', rgaController.state);
if (!rgaController.state.answer) return;
contentDiv.innerHTML = `
<h2>Generated Answer Component</h2>
<p>${rgaController.state.answer}</p>
`;
});
document.getElementById(SUBMIT_BUTTON_ID).addEventListener('click', () => {
const queryInput = document.getElementById(QUERY_INPUT_ID);
const query = queryInput.value;
if (!query) return;
searchEngine.dispatch(updateQuery({q: query}));
searchEngine.dispatch(executeSearch(logSearchboxSubmit()));
});
searchEngine.dispatch(executeSearch(logInterfaceLoad()));
};
document.addEventListener('DOMContentLoaded', coveoHeadlessRga);
searchEngine controller. The above example is not intended for production use.rgaController is updated.