Implement HyDE for QA (#690)

This commit is contained in:
Logan Yang 2024-09-30 14:03:35 -07:00 committed by GitHub
parent 4c54ab6352
commit 59068fa494
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 54 additions and 15 deletions

View file

@ -272,6 +272,7 @@ export default class ChainManager {
maxK: this.settings.maxSourceChunks, // The maximum number of docs (chunks) to retrieve
kIncrement: 2,
},
chatModel,
options.debug
);

View file

@ -134,7 +134,8 @@ class ChainFactory {
// and lose the follow up question altogether.
const condenseQuestionTemplate = `Given the following conversation and a follow up question,
summarize the conversation as context and keep the follow up question unchanged, in its original language.
Then combine the summary and the follow up question to construct a standalone question.
If the follow up question is unrelated to its preceding messages, return the question directly.
If it is related, then combine the summary and the follow up question to construct a standalone question.
Make sure to keep any [[]] wrapped note titles in the question unchanged.
Chat History:

View file

@ -361,7 +361,7 @@ ${chatContent}`;
};
if (currentChain === ChainType.LONG_NOTE_QA_CHAIN) {
setChain(ChainType.LONG_NOTE_QA_CHAIN, { noteFile });
setChain(ChainType.LONG_NOTE_QA_CHAIN, { noteFile, debug });
}
addMessage(activeNoteOnMessage);

View file

@ -818,7 +818,7 @@ export default class CopilotPlugin extends Plugin {
systemMessage: this.settings.userSystemPrompt || DEFAULT_SYSTEM_PROMPT,
chatContextTurns: Number(contextTurns),
chainType: ChainType.LLM_CHAIN, // Set LLM_CHAIN as default ChainType
options: { forceNewCreation: true } as SetChainOptions,
options: { forceNewCreation: true, debug: this.settings.debug } as SetChainOptions,
openAIProxyBaseUrl: this.settings.openAIProxyBaseUrl,
openAIEmbeddingProxyBaseUrl: this.settings.openAIEmbeddingProxyBaseUrl,
};

View file

@ -2,7 +2,9 @@ import { extractNoteTitles, getNoteFileFromTitle } from "@/utils";
import VectorDBManager from "@/vectorDBManager";
import { BaseRetriever } from "@langchain/core/retrievers";
import { VectorStore } from "@langchain/core/vectorstores";
import { BaseLanguageModel } from "langchain/base_language";
import { Document } from "langchain/document";
import { ChatPromptTemplate } from "langchain/prompts";
import {
ScoreThresholdRetriever,
ScoreThresholdRetrieverInput,
@ -12,13 +14,21 @@ import { Vault } from "obsidian";
export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
public lc_namespace = ["hybrid_retriever"];
private llm: BaseLanguageModel;
private queryRewritePrompt: ChatPromptTemplate;
constructor(
private db: PouchDB.Database,
private vault: Vault,
private options: ScoreThresholdRetrieverInput<V>,
llm: BaseLanguageModel,
private debug?: boolean
) {
super();
this.llm = llm;
this.queryRewritePrompt = ChatPromptTemplate.fromTemplate(
"Please write a passage to answer the question.\nQuestion: {question}\nPassage:"
);
}
async getRelevantDocuments(query: string): Promise<Document[]> {
@ -26,20 +36,11 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
const noteTitles = extractNoteTitles(query);
// Retrieve chunks for explicitly mentioned note titles
const explicitChunks = await this.getExplicitChunks(noteTitles);
if (this.debug) {
console.log(
"*** HYBRID RETRIEVER DEBUG INFO: ***",
"\nHybrid Retriever Query: ",
query,
"\nNote Titles extracted: ",
noteTitles,
"\nExplicit Chunks:",
explicitChunks
);
}
// Generate a hypothetical answer passage
const rewrittenQuery = await this.rewriteQuery(query);
// Perform vector similarity search using ScoreThresholdRetriever
const vectorChunks = await this.getVectorChunks(query);
const vectorChunks = await this.getVectorChunks(rewrittenQuery);
// Combine explicit and vector chunks, removing duplicates while maintaining order
const uniqueChunks = new Set<string>(explicitChunks.map((chunk) => chunk.pageContent));
@ -53,10 +54,46 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
}
}
if (this.debug) {
console.log(
"*** HyDE HYBRID RETRIEVER DEBUG INFO: ***",
"\nOriginal Query: ",
query,
"\n\nRewritten Query: ",
rewrittenQuery,
"\n\nNote Titles extracted: ",
noteTitles,
"\n\nExplicit Chunks:",
explicitChunks,
"\n\nVector Chunks:",
vectorChunks,
"\n\nCombined Chunks:",
combinedChunks
);
}
// Make sure the combined chunks are at most maxK
return combinedChunks.slice(0, this.options.maxK);
}
private async rewriteQuery(query: string): Promise<string> {
try {
const promptResult = await this.queryRewritePrompt.format({ question: query });
const rewrittenQueryObject = await this.llm.invoke(promptResult);
// Directly return the content assuming it's structured as expected
if (rewrittenQueryObject && "content" in rewrittenQueryObject) {
return rewrittenQueryObject.content;
}
console.warn("Unexpected rewrittenQuery format. Falling back to original query.");
return query;
} catch (error) {
console.error("Error in rewriteQuery:", error);
// If there's an error, return the original query
return query;
}
}
private async getExplicitChunks(noteTitles: string[]): Promise<Document[]> {
const explicitChunks: Document[] = [];
for (const noteTitle of noteTitles) {