From 1d6fb2f0017885a3e4f95aece8ba1bc24fac435f Mon Sep 17 00:00:00 2001 From: Adeel Ehsan Date: Thu, 25 Jul 2024 05:30:22 +0500 Subject: [PATCH] Integrated APIv2 for streaming and non streaming (#163) * Integrated APIv2 for streaming and non streaming * updated summary types * updated README * updates to README * don't raise error if corpus key or id is available * parsed muliple corppra keys and update the query * updated the filters in APIv2 query * updated with main * updated package * updated stream v1 config * reverted the config for ask news * bugfix with rerank_num_results --------- Co-authored-by: Ofer Mendelevitch --- README.md | 14 +- config/ask-news/config.yaml | 2 +- package-lock.json | 8 +- package.json | 2 +- server/index.js | 2 + src/contexts/ConfigurationContext.tsx | 44 ++- src/contexts/SearchContext.tsx | 408 +++++++++++++++++-------- src/contexts/apiV2sendSearchRequest.ts | 206 +++++++++++++ src/utils/deserializeSearchResponse.ts | 76 +++-- src/views/search/types.ts | 111 ++++++- 10 files changed, 695 insertions(+), 178 deletions(-) create mode 100644 src/contexts/apiV2sendSearchRequest.ts diff --git a/README.md b/README.md index d41918f0..b16cd29e 100644 --- a/README.md +++ b/README.md @@ -100,18 +100,26 @@ Note that the variables in the `.env` file all have the `REACT_APP` prefix, as i ```yaml # These config vars are required for connecting to your Vectara data and issuing requests. -corpus_id: 5 customer_id: 123456789 +corpus_id: 5 +corpus_key: vectara_docs_1 api_key: "zqt_abcdef..." ``` -Note that `corpud_id` can be a set of corpora in which case each query runs against all those corpora. -In such a case, the format is a comma-separated list of corpus IDs, for example: +Notes: +* Vectara APIV2 uses `corpus_key`, but the older `corpus_id` is also supported for backwards compatibility. We encourage you to use `corpus_key` as we may deprecate support for V1 in the future. +* `corpus_key` (or `corpus_id`) can be a set of corpora in which case each query runs against all those corpora. In such a case, the format is a comma-separated list of corpus keys or corpus IDs, for example: ```yaml corpus_id: "123,234,345" ``` +OR + +```yaml +corpus_key: "vectara_docs_1,vectara_website_3" +``` + ### Search header (optional) These configuration parameters enable you to configure the look and feel of the search header. diff --git a/config/ask-news/config.yaml b/config/ask-news/config.yaml index 2e0f3de6..e9da2b90 100644 --- a/config/ask-news/config.yaml +++ b/config/ask-news/config.yaml @@ -13,4 +13,4 @@ summary_num_sentences: 3 summary_num_results: 7 enable_source_filters: True -sources: "BBC,NPR,FOX,CNBC,CNN" +sources: "BBC,NPR,FOX,CNBC,CNN" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 89c05177..702c877c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@types/prismjs": "^1.26.0", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", - "@vectara/stream-query-client": "^2.1.0", + "@vectara/stream-query-client": "^3.2.0", "analytics": "^0.8.9", "axios": "^0.27.2", "classnames": "^2.3.2", @@ -4641,9 +4641,9 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vectara/stream-query-client": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vectara/stream-query-client/-/stream-query-client-2.1.0.tgz", - "integrity": "sha512-RsDSweRFAEmZKAdAH6D+ZWPiQEQNxnHU6Cx4aVQ8jwHgnzmIt2jKgLvz4vm4x8Wxktx4LKWwMuZGbbT95cF5+g==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vectara/stream-query-client/-/stream-query-client-3.2.0.tgz", + "integrity": "sha512-OPUik3bIVOKPc3ytvpFTyGbrEHDicbn9nFWiG5ztgAovaDlRL4bu6d0sXOAQLFYgylVMSML+xV4iIsql25P9yg==" }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", diff --git a/package.json b/package.json index 4ff95056..e35cd77d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@types/prismjs": "^1.26.0", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", - "@vectara/stream-query-client": "^2.1.0", + "@vectara/stream-query-client": "^3.2.0", "analytics": "^0.8.9", "axios": "^0.27.2", "classnames": "^2.3.2", diff --git a/server/index.js b/server/index.js index 4fab1a9d..83cf3edf 100644 --- a/server/index.js +++ b/server/index.js @@ -45,6 +45,7 @@ app.post("/config", (req, res) => { // Search endpoint, corpus_id, + corpus_key, customer_id, api_key, @@ -118,6 +119,7 @@ app.post("/config", (req, res) => { // Search endpoint, corpus_id, + corpus_key, customer_id, api_key, diff --git a/src/contexts/ConfigurationContext.tsx b/src/contexts/ConfigurationContext.tsx index ccf8bd19..2a53fc18 100644 --- a/src/contexts/ConfigurationContext.tsx +++ b/src/contexts/ConfigurationContext.tsx @@ -19,6 +19,7 @@ import { interface Config { // Search config_endpoint?: string; + config_corpus_key?: string; config_corpus_id?: string; config_customer_id?: string; config_api_key?: string; @@ -63,7 +64,7 @@ interface Config { // Summary config_summary_default_language?: string; - config_summary_num_results?: number; + config_summary_num_results?: number config_summary_num_sentences?: number; config_summary_prompt_name?: string; config_summary_prompt_text_filename?: string; @@ -92,10 +93,11 @@ interface Config { type ConfigProp = keyof Config; -const requiredConfigVars = ["corpus_id", "customer_id", "api_key", "endpoint"]; +const requiredConfigVars = ["corpus_key", "corpus_id", "customer_id", "api_key", "endpoint"]; type Search = { endpoint?: string; + corpusKey?: string; corpusId?: string; customerId?: string; apiKey?: string; @@ -358,19 +360,28 @@ export const ConfigContextProvider = ({ children }: Props) => { } } - const missingConfigProps = requiredConfigVars.reduce( - (accum, configVarName) => { - if (config[`config_${configVarName}` as ConfigProp] === undefined) - accum.push(configVarName); + const missingConfigProps = requiredConfigVars.reduce((accum, configVarName) => { + if (configVarName === "corpus_key" || configVarName === "corpus_id" ) { + // Skip this check and handle it separately return accum; - }, - [] as string[] - ); + } else { + if (config[`config_${configVarName}` as ConfigProp] === undefined) { + accum.push(configVarName); + } + } + return accum; + }, [] as string[]); + + if (config.config_corpus_key === undefined && config.config_corpus_id === undefined) { + missingConfigProps.push("corpus_key or corpus_id"); + } + setMissingConfigProps(missingConfigProps); const { // Search config_endpoint, + config_corpus_key, config_corpus_id, config_customer_id, config_api_key, @@ -441,6 +452,7 @@ export const ConfigContextProvider = ({ children }: Props) => { setSearch({ endpoint: config_endpoint, + corpusKey: config_corpus_key, corpusId: config_corpus_id, customerId: config_customer_id, apiKey: config_api_key, @@ -501,7 +513,7 @@ export const ConfigContextProvider = ({ children }: Props) => { } const getRerankerDiversty = (rerankerNname: string | undefined) => { - if (rerankerNname === "mmr") return config_mmr_diversity_bias ?? rerank.diversityBias ?? 0.3 + if (rerankerNname === "mmr") return Number(config_mmr_diversity_bias) ?? rerank.diversityBias ?? 0.3 else return rerank.diversityBias ?? 0.3 } @@ -527,8 +539,8 @@ export const ConfigContextProvider = ({ children }: Props) => { config_summary_default_language as SummaryLanguage, "auto" ), - summaryNumResults: config_summary_num_results ?? 7, - summaryNumSentences: config_summary_num_sentences ?? 3, + summaryNumResults: Number(config_summary_num_results) ?? 7, + summaryNumSentences: Number(config_summary_num_sentences) ?? 3, summaryPromptOptions: getPromptOptions(), summaryPromptName: config_summary_prompt_name ?? "vectara-summary-ext-24-05-sml", @@ -561,15 +573,15 @@ export const ConfigContextProvider = ({ children }: Props) => { setRerank({ isEnabled: isRankerEnabled(config_reranker_name), - numResults: config_rerank_num_results ?? rerank.numResults, + numResults: Number(config_rerank_num_results ?? rerank.numResults), id: getRerankerId(config_reranker_name), diversityBias: getRerankerDiversty(config_reranker_name), }); setHybrid({ - numWords: config_hybrid_search_num_words ?? hybrid.numWords, - lambdaLong: config_hybrid_search_lambda_long ?? hybrid.lambdaLong, - lambdaShort: config_hybrid_search_lambda_short ?? hybrid.lambdaShort, + numWords: Number(config_hybrid_search_num_words) ?? hybrid.numWords, + lambdaLong: Number(config_hybrid_search_lambda_long) ?? hybrid.lambdaLong, + lambdaShort: Number(config_hybrid_search_lambda_short) ?? hybrid.lambdaShort, }); setResults({ diff --git a/src/contexts/SearchContext.tsx b/src/contexts/SearchContext.tsx index 4efe694d..e6ca10a5 100644 --- a/src/contexts/SearchContext.tsx +++ b/src/contexts/SearchContext.tsx @@ -9,10 +9,10 @@ import { } from "react"; import { useSearchParams } from "react-router-dom"; import { - DeserializedSearchResult, - SearchResponse, - SummaryLanguage, - SearchError, FcsMode, + DeserializedSearchResult, + SearchResponse, + SummaryLanguage, + SearchError, FcsMode, mmr_reranker_id, ApiV2SearchResponse } from "../views/search/types"; import { useConfigContext } from "./ConfigurationContext"; import { sendSearchRequest } from "./sendSearchRequest"; @@ -23,8 +23,10 @@ import { retrieveHistory, } from "./history"; import { deserializeSearchResponse } from "../utils/deserializeSearchResponse"; -import { StreamUpdate } from "@vectara/stream-query-client/lib/types"; -import { streamQuery } from "@vectara/stream-query-client"; + +import { ApiV2, ApiV1, streamQueryV1, streamQueryV2 } from "@vectara/stream-query-client"; +import { apiV2sendSearchRequest } from "./apiV2sendSearchRequest"; +import { END_TAG, START_TAG } from "../utils/parseSnippet"; interface SearchContextType { filterValue: string; @@ -107,23 +109,23 @@ export const SearchContextProvider = ({ children }: Props) => { // Basic search const [isSearching, setIsSearching] = useState(false); const [searchError, setSearchError] = useState(); - const [searchResponse, setSearchResponse] = useState(); + const [searchResponse, setSearchResponse] = useState(); const [searchTime, setSearchTime] = useState(0); // Summarization const [isSummarizing, setIsSummarizing] = useState(false); const [summarizationError, setSummarizationError] = useState< - SearchError | undefined + SearchError | undefined >(); const [summarizationResponse, setSummarizationResponse] = - useState(); + useState(); const [summaryTime, setSummaryTime] = useState(0); const [factualConsistencyScore, setFactualConsistencyScore] = useState(); // Citation selection const searchResultsRef = useRef([]); const [selectedSearchResultPosition, setSelectedSearchResultPosition] = - useState(); + useState(); useEffect(() => { setHistory(retrieveHistory()); @@ -144,8 +146,8 @@ export const SearchContextProvider = ({ children }: Props) => { value: getQueryParam(urlParams, "query") ?? "", filter: getQueryParam(urlParams, "filter"), language: getQueryParam(urlParams, "language") as - | SummaryLanguage - | undefined, + | SummaryLanguage + | undefined, isPersistable: false, mode: getQueryParam(urlParams, "mode") ?? "", }); @@ -157,8 +159,8 @@ export const SearchContextProvider = ({ children }: Props) => { useEffect(() => { if (searchResults) { searchResultsRef.current = searchResultsRef.current.slice( - 0, - searchResults.length + 0, + searchResults.length ); } else { searchResultsRef.current = []; @@ -172,8 +174,8 @@ export const SearchContextProvider = ({ children }: Props) => { const selectSearchResultAt = (position: number) => { if ( - !searchResultsRef.current[position] || - selectedSearchResultPosition === position + !searchResultsRef.current[position] || + selectedSearchResultPosition === position ) { // Reset selected position. setSelectedSearchResultPosition(undefined); @@ -188,7 +190,7 @@ export const SearchContextProvider = ({ children }: Props) => { }; const getLanguage = (): SummaryLanguage => - (languageValue ?? summary.defaultLanguage) as SummaryLanguage; + (languageValue ?? summary.defaultLanguage) as SummaryLanguage; const onSearch = async ({ value = searchValue, @@ -236,103 +238,233 @@ export const SearchContextProvider = ({ children }: Props) => { setIsSearching(true); setIsSummarizing(true); setSelectedSearchResultPosition(undefined); + if (search.corpusKey) { + if (search.enableStreamQuery) { + try { + const startTime = Date.now(); + const onStreamEvent = (event: ApiV2.StreamEvent) => { + if (searchId === searchCount) { + switch (event.type) { + case "requestError": + case "genericError": + case "error": + setIsSearching(false); + setSearchResponse(undefined); + break; + + case "searchResults": + setSearchResponse(event.searchResults) + setIsSearching(false); + setSearchTime(Date.now() - startTime); + + break; + + case "generationChunk": + setSummarizationError(undefined); + setSummarizationResponse(event.updatedText ?? undefined); + break; - let initialSearchResponse; - - try { - const startTime = Date.now(); - initialSearchResponse = await sendSearchRequest({ - filter, - query_str: value, - rerank: rerank.isEnabled, - rerankNumResults: rerank.numResults, - rerankerId: rerank.id, - rerankDiversityBias: rerank.diversityBias, - hybridNumWords: hybrid.numWords, - hybridLambdaLong: hybrid.lambdaLong, - hybridLambdaShort: hybrid.lambdaShort, - mode: mode, - customerId: search.customerId!, - corpusId: search.corpusId!, - endpoint: search.endpoint!, - apiKey: search.apiKey!, - logQuery: true - }); - const totalTime = Date.now() - startTime; - - // If we send multiple requests in rapid succession, we only want to - // display the results of the most recent request. - if (searchId === searchCount) { - setIsSearching(false); - setSearchTime(totalTime); - setSearchResponse(initialSearchResponse); + case "factualConsistencyScore": + setFactualConsistencyScore(event.factualConsistencyScore > 0 ? event.factualConsistencyScore : undefined) + break; - if (initialSearchResponse.response.length > 0) { - setSearchError(undefined); - } else { - setSearchError({ - message: "There weren't any results for your search.", - }); + case "end": + setIsSummarizing(false); + setSummaryTime(Date.now() - startTime); + break; + } + } + }; + + const streamQueryConfig: ApiV2.StreamQueryConfig = { + apiKey: search.apiKey!, + customerId: search.customerId!, + query: value, + corpusKey: search.corpusKey!, + domain: `https://${search.endpoint!}`, + search: { + offset: 0, + metadataFilter: filter, + lexicalInterpolation: + value.trim().split(" ").length > hybrid.numWords ? hybrid.lambdaLong : hybrid.lambdaShort, + reranker: rerank.isEnabled ? (rerank.id === mmr_reranker_id + ? { + type: "mmr", + diversityBias: rerank.diversityBias || 0 + } + : { + type: "customer_reranker", + // rnk_ prefix needed for conversion from API v1 to v2. + rerankerId: `rnk_${rerank.id}` + }) : { type: "none" }, + contextConfiguration: { + sentencesBefore: summary.summaryNumSentences, + sentencesAfter: summary.summaryNumSentences, + startTag: START_TAG, + endTag: END_TAG + } + }, + generation: { + promptName: promptName, + promptText: summary.summaryPromptText, + maxUsedSearchResults: summary.summaryNumResults, + enableFactualConsistencyScore: isFactualConsistentScoreEnabled, + responseLanguage: language + + } + }; + + await streamQueryV2({ streamQueryConfig, onStreamEvent }) + + } catch (error) { + console.log("Summary error", error); + console.log("Search error", error); + setIsSearching(false); + setSearchError(error as SearchError); + setSearchResponse(undefined); + setIsSummarizing(false); + setSummarizationError(error as SearchError); + setSummarizationResponse(undefined); + return; + } + } + else { + try { + const startTime = Date.now(); + const response: ApiV2SearchResponse = await apiV2sendSearchRequest({ + apiKey: search.apiKey!, + customerId: search.customerId!, + query: value, + corpusKey: search.corpusKey!, + endpoint: search.endpoint!, + search: { + offset: 0, + metadataFilter: filter, + lexicalInterpolation: + value.trim().split(" ").length > hybrid.numWords ? hybrid.lambdaLong : hybrid.lambdaShort, + reranker: rerank.isEnabled ? (rerank.id === mmr_reranker_id + ? { + type: "mmr", + diversityBias: rerank.diversityBias || 0 + } + : { + type: "customer_reranker", + // rnk_ prefix needed for conversion from API v1 to v2. + rerankerId: `rnk_${rerank.id}` + }) : { type: "none" }, + contextConfiguration: { + sentencesBefore: summary.summaryNumSentences, + sentencesAfter: summary.summaryNumSentences, + startTag: START_TAG, + endTag: END_TAG + } + }, + generation: { + promptName: promptName, + promptText: summary.summaryPromptText, + maxUsedSearchResults: summary.summaryNumResults, + enableFactualConsistencyScore: isFactualConsistentScoreEnabled, + responseLanguage: language + + } + }) + const totalTime = Date.now() - startTime; + if (searchId === searchCount) { + setSearchResponse(response.search_results) + setIsSearching(false); + setSearchTime(totalTime); + setIsSummarizing(false); + setSummarizationError(undefined); + setSummarizationResponse(response.summary); + setSummaryTime(totalTime); + setFactualConsistencyScore(response.factual_consistency_score > 0 ? response.factual_consistency_score : undefined) + + } + }catch (error) { + console.log("Search error", error); + setIsSearching(false); + setSearchError(error as SearchError); + setSearchResponse(undefined); + setIsSummarizing(false); + setSummarizationError(error as SearchError); + setSummarizationResponse(undefined); } } - } catch (error) { - console.log("Search error", error); - setIsSearching(false); - setSearchError(error as SearchError); - setSearchResponse(undefined); - } - // Second call - search and summarize (if summary is enabled); this may take a while to return results - if (isSummaryEnabled) { - if (initialSearchResponse.response.length > 0) { + } + else { + let initialSearchResponse; + try { const startTime = Date.now(); - try { - if(search.enableStreamQuery) { - const onStreamUpdate = (update: StreamUpdate) => { - // If we send multiple requests in rapid succession, we only want to - // display the results of the most recent request. - const fcsDetail = update.details?.factualConsistency - if (searchId === searchCount) { - if (update.isDone) { - setIsSummarizing(false); - setSummaryTime(Date.now() - startTime); - } - setSummarizationError(undefined); - setSummarizationResponse(update.updatedText ?? undefined); - setFactualConsistencyScore(fcsDetail?.score) + initialSearchResponse = await sendSearchRequest({ + filter, + query_str: value, + rerank: rerank.isEnabled, + rerankNumResults: rerank.numResults, + rerankerId: rerank.id, + rerankDiversityBias: rerank.diversityBias, + hybridNumWords: hybrid.numWords, + hybridLambdaLong: hybrid.lambdaLong, + hybridLambdaShort: hybrid.lambdaShort, + mode: mode, + customerId: search.customerId!, + corpusId: search.corpusId!, + endpoint: search.endpoint!, + apiKey: search.apiKey!, + logQuery: true + }); + const totalTime = Date.now() - startTime; + + // If we send multiple requests in rapid succession, we only want to + // display the results of the most recent request. + if (searchId === searchCount) { + setIsSearching(false); + setSearchTime(totalTime); + setSearchResponse(initialSearchResponse); + + if (initialSearchResponse.response.length > 0) { + setSearchError(undefined); + } else { + setSearchError({ + message: "There weren't any results for your search.", + }); + } + } + } catch (error) { + console.log("Search error", error); + setIsSearching(false); + setSearchError(error as SearchError); + setSearchResponse(undefined); + } + + // Second call - search and summarize (if summary is enabled); this may take a while to return results + if (isSummaryEnabled) { + if (initialSearchResponse.response.length > 0) { + const startTime = Date.now(); + try { + if(search.enableStreamQuery) { + const onStreamUpdate = (update: ApiV1.StreamUpdate) => { + // If we send multiple requests in rapid succession, we only want to + // display the results of the most recent request. + const fcsDetail = update.details?.factualConsistency + if (searchId === searchCount) { + if (update.isDone) { + setIsSummarizing(false); + setSummaryTime(Date.now() - startTime); } + setSummarizationError(undefined); + setSummarizationResponse(update.updatedText ?? undefined); + setFactualConsistencyScore(fcsDetail?.score) + } }; const hybridLambda = value === "undefined" || value.trim().split(" ").length > hybrid.numWords - ? hybrid.lambdaLong - : hybrid.lambdaShort; - streamQuery( - { - filter, - queryValue: value, - rerank: rerank.isEnabled, - rerankNumResults: rerank.numResults, - rerankerId: rerank.id, - rerankDiversityBias: rerank.diversityBias, - summaryNumResults: summary.summaryNumResults, - summaryNumSentences: summary.summaryNumSentences, - summaryPromptName: promptName, - enableFactualConsistencyScore: isFactualConsistentScoreEnabled, - lambda: hybridLambda, - language, - customerId: search.customerId!, - corpusIds: search.corpusId!.split(","), - endpoint: search.endpoint!, - apiKey: search.apiKey!, - }, - onStreamUpdate - ); - } - else { - const response = await sendSearchRequest({ + ? hybrid.lambdaLong + : hybrid.lambdaShort; + streamQueryV1( + { filter, - query_str: value, - summaryMode: true, + queryValue: value, rerank: rerank.isEnabled, rerankNumResults: rerank.numResults, rerankerId: rerank.id, @@ -340,43 +472,67 @@ export const SearchContextProvider = ({ children }: Props) => { summaryNumResults: summary.summaryNumResults, summaryNumSentences: summary.summaryNumSentences, summaryPromptName: promptName, - summaryPromptText: summary.summaryPromptText, enableFactualConsistencyScore: isFactualConsistentScoreEnabled, - hybridNumWords: hybrid.numWords, - hybridLambdaLong: hybrid.lambdaLong, - hybridLambdaShort: hybrid.lambdaShort, + lambda: hybridLambda, language, customerId: search.customerId!, - corpusId: search.corpusId!, - endpoint: search.endpoint!, + corpusIds: search.corpusId!.split(","), + endpoint: `https://${search.endpoint!}/v1/stream-query`, apiKey: search.apiKey!, + }, + onStreamUpdate + ); + } + else { + const response = await sendSearchRequest({ + filter, + query_str: value, + summaryMode: true, + rerank: rerank.isEnabled, + rerankNumResults: rerank.numResults, + rerankerId: rerank.id, + rerankDiversityBias: rerank.diversityBias, + summaryNumResults: summary.summaryNumResults, + summaryNumSentences: summary.summaryNumSentences, + summaryPromptName: promptName, + summaryPromptText: summary.summaryPromptText, + enableFactualConsistencyScore: isFactualConsistentScoreEnabled, + hybridNumWords: hybrid.numWords, + hybridLambdaLong: hybrid.lambdaLong, + hybridLambdaShort: hybrid.lambdaShort, + language, + customerId: search.customerId!, + corpusId: search.corpusId!, + endpoint: search.endpoint!, + apiKey: search.apiKey!, }); const totalTime = Date.now() - startTime; // If we send multiple requests in rapid succession, we only want to // display the results of the most recent request. if (searchId === searchCount) { - setIsSummarizing(false); - setSummarizationError(undefined); - setSummarizationResponse(response.summary[0]?.text); - setSummaryTime(totalTime); - setFactualConsistencyScore(response?.summary[0]?.factualConsistency?.score) + setIsSummarizing(false); + setSummarizationError(undefined); + setSummarizationResponse(response.summary[0]?.text); + setSummaryTime(totalTime); + setFactualConsistencyScore(response?.summary[0]?.factualConsistency?.score) } + } + } catch (error) { + console.log("Summary error", error); + setIsSummarizing(false); + setSummarizationError(error as SearchError); + setSummarizationResponse(undefined); + return } - } catch (error) { - console.log("Summary error", error); + } else { setIsSummarizing(false); - setSummarizationError(error as SearchError); + setSummarizationError({ + message: "No search results to summarize", + }); setSummarizationResponse(undefined); - return } - } else { - setIsSummarizing(false); - setSummarizationError({ - message: "No search results to summarize", - }); - setSummarizationResponse(undefined); } } } else { @@ -440,8 +596,8 @@ export const useSearchContext = () => { const context = useContext(SearchContext); if (context === undefined) { throw new Error( - "useSearchContext must be used within a SearchContextProvider" + "useSearchContext must be used within a SearchContextProvider" ); } return context; -}; +}; \ No newline at end of file diff --git a/src/contexts/apiV2sendSearchRequest.ts b/src/contexts/apiV2sendSearchRequest.ts new file mode 100644 index 00000000..f74fa98f --- /dev/null +++ b/src/contexts/apiV2sendSearchRequest.ts @@ -0,0 +1,206 @@ +import { SummaryLanguage, QueryBody, QueryRequestHeaders } from "../views/search/types"; + +type GenerationConfig = { + promptName?: string; + maxUsedSearchResults?: number; + promptText?: string; + maxResponseCharacters?: number; + responseLanguage?: SummaryLanguage; + modelParameters?: { + maxTokens: number; + temperature: number; + frequencyPenalty: number; + presencePenalty: number; + }; + citations?: { + style: "none" | "numeric"; + } | { + style: "html" | "markdown"; + urlPattern: string; + textPattern: string; + }; + enableFactualConsistencyScore?: boolean; +}; +type Config = { + customerId: string; + apiKey?: string; + authToken?: string; + endpoint?: string; + query: string; + corpusKey: string; + search: { + metadataFilter: string; + lexicalInterpolation?: number; + customDimensions?: Record; + semantics?: "default" | "query" | "response"; + offset: number; + limit?: number; + contextConfiguration?: { + charactersBefore?: number; + charactersAfter?: number; + sentencesBefore?: number; + sentencesAfter?: number; + startTag?: string; + endTag?: string; + }; + reranker?: { + type: "none"; + } | { + type: "customer_reranker"; + rerankerId: string; + } | { + type: "mmr"; + diversityBias: number; + }; + }; + generation?: GenerationConfig; + chat?: { + store?: boolean; + conversationId?: string; + }; +}; + +const convertReranker = (reranker?: Config["search"]["reranker"]) => { + if (!reranker) return; + + if (reranker.type === "none") { + return { + type: reranker.type + }; + } + + if (reranker.type === "customer_reranker") { + return { + type: reranker.type, + reranker_id: reranker.rerankerId + }; + } + + if (reranker.type === "mmr") { + return { + type: reranker.type, + diversity_bias: reranker.diversityBias + }; + } +}; + +const convertCitations = (citations?: GenerationConfig["citations"]) => { + if (!citations) return; + + if (citations.style === "none" || citations.style === "numeric") { + return { + style: citations.style + }; + } + + if (citations.style === "html" || citations.style === "markdown") { + return { + style: citations.style, + url_pattern: citations.urlPattern, + text_pattern: citations.textPattern + }; + } +}; + + +/** + * Send a request to the query API. + */ +export const apiV2sendSearchRequest = async ({ + customerId, + corpusKey, + apiKey, + query, + endpoint, + search, + generation +}: Config) => { + const { + metadataFilter, + lexicalInterpolation, + customDimensions, + semantics, + offset, + limit, + contextConfiguration, + reranker + } = search + + const body: QueryBody = { + query, + search: { + corpora: corpusKey.split(",").map((key) => ( + { + corpus_key: key, + metadata_filter: metadataFilter ? `doc.source = '${metadataFilter}'`: undefined, + lexical_interpolation: lexicalInterpolation, + custom_dimensions: customDimensions, + semantics + } + )), + offset, + limit, + context_configuration: { + characters_before: contextConfiguration?.charactersBefore, + characters_after: contextConfiguration?.charactersAfter, + sentences_before: contextConfiguration?.sentencesBefore, + sentences_after: contextConfiguration?.sentencesAfter, + start_tag: contextConfiguration?.startTag, + end_tag: contextConfiguration?.endTag + }, + reranker: convertReranker(reranker) + } + }; + + if (generation) { + const { + promptName, + maxUsedSearchResults, + promptText, + maxResponseCharacters, + responseLanguage, + modelParameters, + citations, + enableFactualConsistencyScore + } = generation; + + body.generation = { + prompt_name: promptName, + max_used_search_results: maxUsedSearchResults, + prompt_text: promptText, + max_response_characters: maxResponseCharacters, + response_language: responseLanguage, + model_parameters: modelParameters && { + max_tokens: modelParameters.maxTokens, + temperature: modelParameters.temperature, + frequency_penalty: modelParameters.frequencyPenalty, + presence_penalty: modelParameters.presencePenalty + }, + citations: convertCitations(citations), + enable_factual_consistency_score: enableFactualConsistencyScore + }; + } + + const headers: QueryRequestHeaders = { + "customer-id": customerId, + "Content-Type": "application/json" + }; + + if (apiKey) headers["x-api-key"] = apiKey; + + const url = `https://${endpoint}/v2/query`; + const response = await fetch(url, { + method: "POST", + headers: headers, + body: JSON.stringify(body) + }); + + if (response.status === 400 || response.status === 403 || response.status === 404) { + const result = await response.json(); + throw new Error(`BAD REQUEST: ${result?.messages[0] ?? result.field_errors}`); + } + + if (response.status !== 200) throw new Error(response.status.toString()); + + return await response.json() +}; diff --git a/src/utils/deserializeSearchResponse.ts b/src/utils/deserializeSearchResponse.ts index 019d1237..b9e60d67 100644 --- a/src/utils/deserializeSearchResponse.ts +++ b/src/utils/deserializeSearchResponse.ts @@ -1,5 +1,11 @@ import { parseSnippet } from "./parseSnippet"; -import { DocMetadata, SearchResponse, DeserializedSearchResult } from "../views/search/types"; +import { + DocMetadata, + SearchResponse, + DeserializedSearchResult, + SearchResult +} from "../views/search/types"; +import { ApiV2 } from "@vectara/stream-query-client"; const convertMetadataToObject = (metadata: DocMetadata[]) => { const obj: Record = {}; @@ -20,33 +26,55 @@ const parseMetadata = (rawMetadata: DocMetadata[], matchingText: string) => { }; export const deserializeSearchResponse = ( - searchResponse?: SearchResponse + searchResponse?: SearchResponse | ApiV2.Query.SearchResult[] | undefined ): Array | undefined => { - if (!searchResponse) return undefined; + if (!searchResponse ) return undefined; const results: Array = []; - const { response: responses, document: documents } = searchResponse; - - responses.forEach((response) => { - const { documentIndex, text: rawText } = response; - const { pre, post, text } = parseSnippet(rawText); - const document = documents[Number(documentIndex)]; - const { id, metadata: rawMetadata } = document; - const { source, url, title, metadata } = parseMetadata(rawMetadata, text); - - results.push({ - id, - snippet: { - pre, - text, - post - }, - source, - url, - title, - metadata + + if ('response' in searchResponse) { + const { response: responses, document: documents } = searchResponse; + responses.forEach((response) => { + const { documentIndex, text: rawText } = response; + const { pre, post, text } = parseSnippet(rawText); + const document = documents[Number(documentIndex)]; + const { id, metadata: rawMetadata } = document; + const { source, url, title, metadata } = parseMetadata(rawMetadata, text); + + results.push({ + id, + snippet: { + pre, + text, + post + }, + source, + url, + title, + metadata + }); }); - }); + } + else { + + searchResponse.forEach((document:SearchResult) => { + const { pre, post, text } = parseSnippet(document.text); + + results.push({ + id: document.document_id, + snippet: { + pre, + text, + post + }, + source: document.document_metadata.source, + url: document.document_metadata.url, + title: document.document_metadata.title, + metadata: document.document_metadata + } as DeserializedSearchResult); + }); + } + return results; }; diff --git a/src/views/search/types.ts b/src/views/search/types.ts index 7a2b3d12..88b3be2e 100644 --- a/src/views/search/types.ts +++ b/src/views/search/types.ts @@ -35,12 +35,27 @@ export type SearchResponseSummary = { status?: string; }; -export type SearchResponse = { +export type ApiV1SearchResponse = { document: SearchResponseDoc[]; response: SearchResponseResult[]; summary: SearchResponseSummary[]; }; +export type SearchResult = { + document_id: string; + text: string; + score: number; + part_metadata: { + lang: string; + section: number; + offset: number; + len: number; + }; + document_metadata: Record; +}; + +export type SearchResponse = ApiV1SearchResponse & SearchResult[] + export type CombinedResult = { document: SearchResponseDoc; response: SearchResponseResult; @@ -173,12 +188,102 @@ export const UiText = (mode: FcsMode): string => { return codeToUiText[mode]; }; +export type NoneReranker = { type: "none" }; +export type CustomerSpecificReranker = { + type: "customer_reranker"; + reranker_id: string; +}; -export const normal_reranker_id = 272725717 +export type MmrReranker = { + type: "mmr"; + diversity_bias: number; +}; -export const slingshot_reranker_id = 272725719 +export type SearchConfiguration = { + corpora: { + corpus_key: string; + metadata_filter?: string; + lexical_interpolation?: number; + custom_dimensions?: Record; + semantics?: "default" | "query" | "response"; + }[]; + offset: number; + limit?: number; + context_configuration?: { + characters_before?: number; + characters_after?: number; + sentences_before?: number; + sentences_after?: number; + start_tag?: string; + end_tag?: string; + }; + reranker?: NoneReranker | CustomerSpecificReranker | MmrReranker; +}; + +export type NoneCitations = { + style: "none"; +}; + +export type NumericCitations = { + style: "numeric"; +}; + +export type HtmlCitations = { + style: "html"; + url_pattern: string; + text_pattern: string; +}; + +export type MarkdownCitations = { + style: "markdown"; + url_pattern: string; + text_pattern: string; +}; + +export type GenerationConfiguration = { + prompt_name?: string; + max_used_search_results?: number; + prompt_text?: string; + max_response_characters?: number; + response_language?: SummaryLanguage; + model_parameters?: { + max_tokens: number; + temperature: number; + frequency_penalty: number; + presence_penalty: number; + }; + citations?: NoneCitations | NumericCitations | HtmlCitations | MarkdownCitations; + enable_factual_consistency_score?: boolean; +}; + +export type QueryBody = { + query: string; + search: SearchConfiguration; + stream_response?: boolean; + generation?: GenerationConfiguration; +}; + +export type QueryRequestHeaders = { + ["customer-id"]: string; + ["Content-Type"]: string; + ["x-api-key"]?: string; + ["Authorization"]?: string; +}; + +export type ApiV2SearchResponse = { + search_results: SearchResult[]; + factual_consistency_score: number; + response_language: string; + summary: string; + document: SearchResponseDoc[]; + response: SearchResponseResult[]; +} + +export const normal_reranker_id = 272725717 export const mmr_reranker_id = 272725718 +export const slingshot_reranker_id = 272725719 + export type UxMode = "search" | "summary";