Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion backend/src/ingestion_graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { RunnableConfig } from '@langchain/core/runnables';
import { StateGraph, END, START } from '@langchain/langgraph';
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
import fs from 'fs/promises';

import { IndexStateAnnotation } from './state.js';
Expand All @@ -14,6 +15,11 @@ import {
} from './configuration.js';
import { reduceDocs } from '../shared/state.js';

const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});

async function ingestDocs(
state: typeof IndexStateAnnotation.State,
config?: RunnableConfig,
Expand All @@ -37,8 +43,11 @@ async function ingestDocs(
docs = reduceDocs([], docs);
}

// Split documents into smaller chunks for better retrieval
const splitDocs = await textSplitter.splitDocuments(docs);

const retriever = await makeRetriever(config);
await retriever.addDocuments(docs);
await retriever.addDocuments(splitDocs);

return { docs: 'delete' };
}
Expand Down
40 changes: 37 additions & 3 deletions backend/src/retrieval_graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { makeRetriever } from '../shared/retrieval.js';
import { formatDocs } from './utils.js';
import { HumanMessage } from '@langchain/core/messages';
import { z } from 'zod';
import { RESPONSE_SYSTEM_PROMPT, ROUTER_SYSTEM_PROMPT } from './prompts.js';
import {
RESPONSE_SYSTEM_PROMPT,
ROUTER_SYSTEM_PROMPT,
MULTI_QUERY_PROMPT,
} from './prompts.js';
import { RunnableConfig } from '@langchain/core/runnables';
import {
AgentConfigurationAnnotation,
Expand Down Expand Up @@ -74,10 +78,40 @@ async function retrieveDocuments(
state: typeof AgentStateAnnotation.State,
config: RunnableConfig,
): Promise<typeof AgentStateAnnotation.Update> {
const configuration = ensureAgentConfiguration(config);
const model = await loadChatModel(configuration.queryModel);
const retriever = await makeRetriever(config);
const response = await retriever.invoke(state.query);

return { documents: response };
// Generate multiple query variants for better retrieval coverage
const formattedPrompt = await MULTI_QUERY_PROMPT.invoke({
query: state.query,
});
const queryResponse = await model.invoke(formattedPrompt.toString());
const queries = [
state.query,
...String(queryResponse.content)
.split('\n')
.map((q) => q.trim())
.filter((q) => q.length > 0),
];

// Retrieve documents for all queries in parallel
const allResults = await Promise.all(
queries.map((q) => retriever.invoke(q)),
);

// Deduplicate by page content, preserving order (original query first)
const seen = new Set<string>();
const uniqueDocs = allResults.flat().filter((doc) => {
const key = doc.pageContent;
if (seen.has(key)) return false;
seen.add(key);
return true;
});

// Cap results — original query docs are first, so most relevant are kept
const maxDocs = configuration.k * 2;
return { documents: uniqueDocs.slice(0, maxDocs) };
}

async function generateResponse(
Expand Down
14 changes: 13 additions & 1 deletion backend/src/retrieval_graph/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,16 @@ const RESPONSE_SYSTEM_PROMPT = ChatPromptTemplate.fromMessages([
],
]);

export { ROUTER_SYSTEM_PROMPT, RESPONSE_SYSTEM_PROMPT };
const MULTI_QUERY_PROMPT = ChatPromptTemplate.fromMessages([
[
'system',
`You are an AI assistant that generates multiple search queries to improve document retrieval.
Given a user question, generate 3 different versions of the question that capture different aspects or phrasings.
Each query should approach the question from a different angle to maximize the chance of finding relevant documents.

Return ONLY the 3 queries, one per line, with no numbering or prefixes.`,
],
['human', '{query}'],
]);

export { ROUTER_SYSTEM_PROMPT, RESPONSE_SYSTEM_PROMPT, MULTI_QUERY_PROMPT };
22 changes: 18 additions & 4 deletions frontend/components/chat-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface ChatMessageProps {
export function ChatMessage({ message }: ChatMessageProps) {
const isUser = message.role === 'user';
const [copied, setCopied] = useState(false);
const [expandedSource, setExpandedSource] = useState<number | null>(null);
const isLoading = message.role === 'assistant' && message.content === '';

const handleCopy = async () => {
Expand Down Expand Up @@ -78,17 +79,30 @@ export function ChatMessage({ message }: ChatMessageProps) {
{message.sources?.map((source, index) => (
<Card
key={index}
className="bg-background/50 transition-all duration-200 hover:bg-background hover:shadow-md hover:scale-[1.02] cursor-pointer"
className="bg-background/50 transition-all duration-200 hover:bg-background hover:shadow-md cursor-pointer"
onClick={() =>
setExpandedSource(
expandedSource === index ? null : index,
)
}
>
<CardContent className="p-3">
<p className="text-sm font-medium truncate">
{source.metadata?.source ||
source.metadata?.filename ||
'N/A'}
{source.metadata?.filename ||
(source.metadata?.source
? source.metadata.source
.split(/[/\\]/)
.pop()
: 'N/A')}
</p>
<p className="text-sm text-muted-foreground">
Page {source.metadata?.loc?.pageNumber || 'N/A'}
</p>
{expandedSource === index && (
<p className="text-xs text-muted-foreground mt-2 whitespace-pre-wrap border-t pt-2">
{source.pageContent || 'No content available'}
</p>
)}
</CardContent>
</Card>
))}
Expand Down
Loading