diff --git a/src/LLMProviders/chainManager.ts b/src/LLMProviders/chainManager.ts index d8120dec..66358840 100644 --- a/src/LLMProviders/chainManager.ts +++ b/src/LLMProviders/chainManager.ts @@ -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 ); diff --git a/src/chainFactory.ts b/src/chainFactory.ts index c0f76510..7e46997f 100644 --- a/src/chainFactory.ts +++ b/src/chainFactory.ts @@ -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: diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index e6ce856f..77129cff 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -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); diff --git a/src/main.ts b/src/main.ts index a269a0c8..011b7cd7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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, }; diff --git a/src/search/hybridRetriever.ts b/src/search/hybridRetriever.ts index 4a0dd753..1f345c23 100644 --- a/src/search/hybridRetriever.ts +++ b/src/search/hybridRetriever.ts @@ -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 extends BaseRetriever { public lc_namespace = ["hybrid_retriever"]; + private llm: BaseLanguageModel; + private queryRewritePrompt: ChatPromptTemplate; + constructor( private db: PouchDB.Database, private vault: Vault, private options: ScoreThresholdRetrieverInput, + 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 { @@ -26,20 +36,11 @@ export class HybridRetriever 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(explicitChunks.map((chunk) => chunk.pageContent)); @@ -53,10 +54,46 @@ export class HybridRetriever 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 { + 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 { const explicitChunks: Document[] = []; for (const noteTitle of noteTitles) {