diff --git a/docs/src/index.tsx b/docs/src/index.tsx index b7d9c5d..062e1fe 100644 --- a/docs/src/index.tsx +++ b/docs/src/index.tsx @@ -7,13 +7,19 @@ import { ApiV2, } from "@vectara/stream-query-client"; -const CUSTOMER_ID = "1526022105"; -const API_KEY = "zqt_WvU_2ewh7ZGRwq8LdL2SV8B9RJmVGyUm1VAuOw"; -const CORPUS_NAME = "ofer-bm-moma-docs"; -const CORPUS_ID = "232"; +// TODO: Switch back to prod values before merging +const CUSTOMER_ID = "3099635174"; +const API_KEY = "zqt_uMCt5uGR7CXARu7QHg7GDYNG5Q5v58HOpvQO0A"; +const CORPUS_NAME = "markdown"; +const CORPUS_ID = "203"; + +// const CUSTOMER_ID = "1526022105"; +// const API_KEY = "zqt_WvU_2ewh7ZGRwq8LdL2SV8B9RJmVGyUm1VAuOw"; +// const CORPUS_NAME = "ofer-bm-moma-docs"; +// const CORPUS_ID = "232"; const App = () => { - const [questionV1, setQuestionV1] = useState(""); + const [questionV1, setQuestionV1] = useState("What is Vectara?"); const [answerV1, setAnswerV1] = useState(); const [conversationIdV1, setConversationIdV1] = useState(); @@ -49,7 +55,7 @@ const App = () => { streamQueryV1(configurationOptions, onStreamUpdate); }; - const [questionV2, setQuestionV2] = useState(""); + const [questionV2, setQuestionV2] = useState("markdown"); const [answerV2, setAnswerV2] = useState(); const [conversationIdV2, setConversationIdV2] = useState(); @@ -57,16 +63,17 @@ const App = () => { const configurationOptions: ApiV2.StreamQueryConfig = { customerId: CUSTOMER_ID, apiKey: API_KEY, - query: questionV1, + query: questionV2, + corpusKey: `${CORPUS_NAME}_${CORPUS_ID}`, search: { offset: 0, - corpora: [ - { - corpusKey: `${CORPUS_NAME}_${CORPUS_ID}`, - metadataFilter: "", - }, - ], - limit: 5, + metadataFilter: "", + limit: 10, + lexicalInterpolation: 0, + contextConfiguration: { + sentencesBefore: 2, + sentencesAfter: 2, + }, }, generation: { maxUsedSearchResults: 5, @@ -78,15 +85,19 @@ const App = () => { store: true, conversationId: conversationIdV2, }, + stream_response: true, }; const onStreamUpdate = (update: ApiV2.StreamUpdate) => { console.log(update); - const { updatedText, details } = update; - if (details?.chat) { - setConversationIdV2(details.chat.conversationId); + const { updatedText, chatId } = update; + if (chatId) { + setConversationIdV2(chatId); + } + + if (updatedText) { + setAnswerV2(updatedText); } - setAnswerV2(updatedText); }; streamQueryV2(configurationOptions, onStreamUpdate); diff --git a/src/apiV1/client.ts b/src/apiV1/client.ts index fe05571..bbedcff 100644 --- a/src/apiV1/client.ts +++ b/src/apiV1/client.ts @@ -6,10 +6,10 @@ import { StreamUpdateHandler, } from "./types"; import { deserializeSearchResponse } from "./deserializeSearchResponse"; -import { DEFAULT_DOMAIN } from "../common/constants"; +import { processStreamChunk } from "./processStreamChunk"; import { SNIPPET_START_TAG, SNIPPET_END_TAG } from "./constants"; -import { generateStream } from "common/generateStream"; -import { processStreamChunk } from "common/processStreamPart"; +import { DEFAULT_DOMAIN } from "../common/constants"; +import { generateStream } from "../common/generateStream"; export const streamQueryV1 = async ( config: StreamQueryConfig, diff --git a/src/common/processStreamPart.ts b/src/apiV1/processStreamChunk.ts similarity index 100% rename from src/common/processStreamPart.ts rename to src/apiV1/processStreamChunk.ts diff --git a/src/apiV2/apiTypes.ts b/src/apiV2/apiTypes.ts index f208e71..00dc248 100644 --- a/src/apiV2/apiTypes.ts +++ b/src/apiV2/apiTypes.ts @@ -11,14 +11,14 @@ export type MmrReranker = { }; export type SearchConfiguration = { - offset: number; corpora: { corpus_key: string; - metadata_filter: string; + metadata_filter?: string; lexical_interpolation?: number; custom_dimensions?: Record; semantics?: "default" | "query" | "response"; }[]; + offset: number; limit?: number; context_configuration?: { characters_before?: number; @@ -83,3 +83,16 @@ export type QueryBody = { generation?: GenerationConfiguration; chat?: ChatConfiguration; }; + +export type SearchResult = { + document_id: string; + text: string; + score: number; + part_metadata: { + lang: string; + section: number; + offset: number; + len: number; + }; + document_metadata: Record; +}; diff --git a/src/apiV2/client.ts b/src/apiV2/client.ts index 7824775..71da2e2 100644 --- a/src/apiV2/client.ts +++ b/src/apiV2/client.ts @@ -1,15 +1,12 @@ import { - Chat, GenerationConfig, - ParsedResult, StreamQueryConfig, - StreamUpdate, StreamUpdateHandler, } from "./types"; import { QueryBody } from "./apiTypes"; +import { processStreamChunk } from "./processStreamChunk"; import { DEFAULT_DOMAIN } from "../common/constants"; -import { generateStream } from "common/generateStream"; -import { processStreamChunk } from "common/processStreamPart"; +import { generateStream } from "../common/generateStream"; const convertReranker = ( reranker?: StreamQueryConfig["search"]["reranker"] @@ -57,8 +54,18 @@ export const streamQueryV2 = async ( customerId, apiKey, endpoint, + corpusKey, query, - search: { offset, corpora, limit, contextConfiguration, reranker }, + search: { + metadataFilter, + lexicalInterpolation, + customDimensions, + semantics, + offset, + limit, + contextConfiguration, + reranker, + }, generation: { promptName, maxUsedSearchResults, @@ -71,28 +78,19 @@ export const streamQueryV2 = async ( chat, } = config; - const body: QueryBody = { + let body: QueryBody = { query, search: { - offset, - corpora: corpora.map( - ({ - corpusKey, - metadataFilter, - lexicalInterpolation, - customDimensions, - semantics, - }) => ({ + corpora: [ + { corpus_key: corpusKey, metadata_filter: metadataFilter, lexical_interpolation: lexicalInterpolation, - custom_dimensions: customDimensions?.reduce( - (acc, { name, weight }) => ({ ...acc, [name]: weight }), - {} as Record - ), + custom_dimensions: customDimensions, semantics, - }) - ), + }, + ], + offset, limit, context_configuration: { characters_before: contextConfiguration?.charactersBefore, @@ -121,109 +119,94 @@ export const streamQueryV2 = async ( chat: chat && { store: chat.store, }, + stream_response: true, }; + let path; + + if (!chat) { + path = `/v2/query`; + } else { + if (chat.conversationId) { + path = `/v2/chats/${chat.conversationId}/turns`; + } else { + path = "/v2/chats"; + } + } + const headers = { "x-api-key": apiKey, "customer-id": customerId, "Content-Type": "application/json", }; - const path = !chat - ? "/v2/query" - : chat.conversationId - ? `/v2/chats/${chat.conversationId}/turns` - : "/v2/chats"; - const url = `${endpoint ?? DEFAULT_DOMAIN}${path}`; - const stream = await generateStream(headers, JSON.stringify(body), url); - - let previousAnswerText = ""; - - for await (const chunk of stream) { - try { - processStreamChunk(chunk, (part: string) => { - const dataObj = JSON.parse(part); - - if (!dataObj.result) return; - - const details: StreamUpdate["details"] = {}; - - // TODO: Add back once debug has been added back to API v2. - // const summaryDetail = getSummaryDetail(config, dataObj.result); - // if (summaryDetail) { - // details.summary = summaryDetail; - // } - - const chatDetail = getChatDetail(dataObj.result); - if (chatDetail) { - details.chat = chatDetail; - } - - const fcsDetail = getFactualConsistencyDetail(dataObj.result); - if (fcsDetail) { - details.factualConsistency = fcsDetail; - } - - const streamUpdate: StreamUpdate = { - responseSet: dataObj.result.responseSet ?? undefined, - details, - updatedText: getUpdatedText(dataObj.result, previousAnswerText), - isDone: dataObj.result.summary?.done ?? false, - }; - - previousAnswerText = streamUpdate.updatedText ?? ""; - - onStreamUpdate(streamUpdate); - }); - } catch (error) {} + try { + const stream = await generateStream(headers, JSON.stringify(body), url); + + let updatedText = ""; + + for await (const chunk of stream) { + try { + processStreamChunk(chunk, (part: string) => { + // Trim the "data:" prefix to get the JSON. + const data = part.slice(5, part.length); + const dataObj = JSON.parse(data); + + const { + type, + search_results, + chat_id, + turn_id, + factual_consistency_score, + generation_chunk, + } = dataObj; + + switch (type) { + case "search_results": + onStreamUpdate({ + type: "searchResults", + searchResults: search_results, + }); + break; + + case "chat_info": + onStreamUpdate({ + type: "chatInfo", + chatId: chat_id, + turnId: turn_id, + }); + break; + + case "generation_chunk": + updatedText += generation_chunk; + onStreamUpdate({ + type: "generationChunk", + updatedText, + generationChunk: generation_chunk, + }); + break; + + case "factual_consistency_score": + onStreamUpdate({ + type: "factualConsistencyScore", + factualConsistencyScore: factual_consistency_score, + }); + break; + + case "end": + onStreamUpdate({ + type: "end", + }); + break; + } + }); + } catch (error) { + console.log("error", error); + } + } + } catch (error) { + console.log("error", error); } }; - -// TODO: Add back once debug has been added back to API v2. -// const getSummaryDetail = ( -// config: StreamQueryConfig, -// parsedResult: ParsedResult -// ) => { -// if (!parsedResult.summary) return; - -// if (config.debug && parsedResult.summary.prompt) { -// return { -// prompt: parsedResult.summary.prompt, -// }; -// } -// }; - -const getChatDetail = ( - // config: StreamQueryConfig, - parsedResult: ParsedResult -) => { - if (!parsedResult.summary?.chat) return; - - const chatDetail: Chat = { - conversationId: parsedResult.summary.chat.conversationId, - turnId: parsedResult.summary.chat.turnId, - }; - - // TODO: Add back once debug has been added back to API v2. - // if (config.debug && parsedResult.summary.chat.rephrasedQuery) { - // chatDetail.rephrasedQuery = parsedResult.summary.chat.rephrasedQuery; - // } - - return chatDetail; -}; - -const getFactualConsistencyDetail = (parsedResult: ParsedResult) => { - if (!parsedResult.summary || !parsedResult.summary.factualConsistency) return; - - return { - score: parsedResult.summary.factualConsistency.score, - }; -}; - -const getUpdatedText = (parsedResult: ParsedResult, previousText: string) => { - if (!parsedResult.summary) return; - - return `${previousText}${parsedResult.summary.text}`; -}; diff --git a/src/apiV2/processStreamChunk.ts b/src/apiV2/processStreamChunk.ts new file mode 100644 index 0000000..bed54cc --- /dev/null +++ b/src/apiV2/processStreamChunk.ts @@ -0,0 +1,12 @@ +export const processStreamChunk = ( + chunk: string, + callback: (part: string) => void +) => { + const parts = chunk.split("\n"); + + parts + .filter((part: string) => { + return part.indexOf("data:") === 0; + }) + .forEach(callback); +}; diff --git a/src/apiV2/types.ts b/src/apiV2/types.ts index 3da496e..08d5135 100644 --- a/src/apiV2/types.ts +++ b/src/apiV2/types.ts @@ -1,4 +1,5 @@ import { SummaryLanguage } from "../common/types"; +import { SearchResult } from "./apiTypes"; export type GenerationConfig = { // The preferred prompt to use, if applicable @@ -42,18 +43,17 @@ export type StreamQueryConfig = { // The query to send to the API. This is the user input. query: string; + corpusKey: string; + search: { + metadataFilter: string; + // A number from 0.0 -> 1.0 that determines how much to leverage neural search and keyword search. + // A value of 0.0 is purely neural search, where a value of 1.0 is purely keyword search. + // Numbers in between are a combination of the two, leaning one way or another. + lexicalInterpolation?: number; + customDimensions?: Record; + semantics?: "default" | "query" | "response"; offset: number; - corpora: Array<{ - corpusKey: string; - metadataFilter: string; - // A number from 0.0 -> 1.0 that determines how much to leverage neural search and keyword search. - // A value of 0.0 is purely neural search, where a value of 1.0 is purely keyword search. - // Numbers in between are a combination of the two, leaning one way or another. - lexicalInterpolation?: number; - customDimensions?: Array<{ name: string; weight: number }>; - semantics?: "default" | "query" | "response"; - }>; limit?: number; contextConfiguration?: { charactersBefore?: number; @@ -87,6 +87,8 @@ export type StreamQueryConfig = { store?: boolean; conversationId?: string; }; + + stream_response: boolean; }; export type Summary = { @@ -104,48 +106,20 @@ export type FactualConsistency = { score: number; }; -// A subset of the Vectara query response, in parsed form. -// This types only data relevant to stream processing -export type ParsedResult = { - responseSet: { - document: Array<{ - id: string; - metadata: Array; - }>; - response: Array<{ - corpusKey: { corpusId: number }; - documentIndex: number; - score: number; - text: string; - }>; - }; - summary?: { - chat?: Chat; - factualConsistency?: FactualConsistency; - done: boolean; - text: string; - // Provided only when debug: true - prompt?: string; - }; -}; - export type StreamUpdate = { - // A list of references that apply to the query response. - responseSet?: ParsedResult["responseSet"]; - - // A concatenation of all text chunks the streaming API has returned so far. - // Use this when updating your UI text display. + type: + | "searchResults" + | "chatInfo" + | "generationChunk" + | "factualConsistencyScore" + | "end"; + searchResults?: SearchResult[]; + chatInfo?: string; + chatId?: string; + turnId?: string; updatedText?: string; - - // true, if streaming has completed. - isDone: boolean; - - // Any additional details that apply to the query response. - details?: { - summary?: Summary; - chat?: Chat; - factualConsistency?: FactualConsistency; - }; + generationChunk?: string; + factualConsistencyScore?: number; }; export type StreamUpdateHandler = (update: StreamUpdate) => void; diff --git a/src/common/constants.ts b/src/common/constants.ts index 85dd70c..e65ecc4 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -1 +1,2 @@ -export const DEFAULT_DOMAIN = "https://api.vectara.io"; +// TODO: Switch back to prod values before merging +export const DEFAULT_DOMAIN = "https://api.vectara.dev";