Skip to content

Commit 657e99d

Browse files
committed
WIP: RAG
1 parent 24c0156 commit 657e99d

17 files changed

Lines changed: 195 additions & 50 deletions

File tree

backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"langchain": "^1.2.29",
4242
"pino": "^10.3.1",
4343
"pino-pretty": "^13.1.3",
44-
"prom-client": "^15.1.3"
44+
"prom-client": "^15.1.3",
45+
"zod": "^4.3.6"
4546
},
4647
"devDependencies": {
4748
"@types/node": "^25.3.3",

backend/src/config/groq.config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,18 @@ export const groqChatAgent = (model: string, temperature = 0) => {
3333
});
3434
};
3535

36-
export const createGroqAgent = (model: string, systemPrompt: string) => {
36+
export const createGroqAgent = (model: string, systemPrompt: string, isRag: boolean = false) => {
3737
const llm = getLLM(model);
3838
const summarizerLLM = getSummarizer();
3939

4040
const modelConfig = MODELS.find((m) => model === m.id);
4141
const triggerTokens = modelConfig ? Math.floor(modelConfig.tpm * 0.5) : 3000;
4242

43+
const agentTools = isRag ? tools : tools.filter((t) => t.name !== "search_uploaded_documents");
44+
4345
return createAgent({
4446
model: llm,
45-
tools,
47+
tools: agentTools,
4648
systemPrompt,
4749
checkpointer,
4850
middleware: [

backend/src/controllers/message.controllers.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { streamText as honoStreamText } from "hono/streaming";
66
import logger from "../utils/logger.utils.js";
77
import { streamLoading, streamText, streamError } from "../utils/stream.utils.js";
88
import { type UploadedFile } from "@app/shared/src/schemas/message.schema.js";
9-
import { vectorStore } from "../config/vectorStore.config.js";
9+
1010

1111
export async function handleUserMessageResponse(c: Context) {
1212
const requestStartTime = new Date();
@@ -33,30 +33,14 @@ export async function handleUserMessageResponse(c: Context) {
3333
return;
3434
}
3535

36-
if (chat.isRag && query.trim() !== "") {
37-
try {
38-
await streamLoading(stream, "Searching documents...");
39-
40-
const results = await vectorStore.similaritySearch(query, 4, {
41-
chatId: chat.id,
42-
});
43-
44-
if (results.length > 0) {
45-
const context = results.map((r) => r.pageContent).join("\n\n---\n\n");
46-
finalQuery = `Context Information:\n${context}\n\nUser Query: ${finalQuery}\n\nPlease answer the user query using the context information provided above if it is relevant.`;
47-
}
48-
} catch (err) {
49-
logger.error({ message: "Vector search failed", error: err, chatId });
50-
}
51-
}
52-
5336
const aiStream = generateAIResponse({
5437
query: finalQuery,
5538
threadId: chatId,
5639
modelName: model,
5740
preferences,
5841
memories,
5942
timezone,
43+
isRag: chat.isRag,
6044
});
6145

6246
let isFirstChunk = true;

backend/src/tools/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { InternetSearch } from "./internetSearch.tools.js";
22
// import { getCurrentTime } from "./time.tools.js";
33
import { getDailyWeatherForecast, getCurrentWeather } from "./weather.tools.js";
4+
import { searchUploadedDocuments } from "./ragSearch.tools.js";
45

5-
export default [InternetSearch, getCurrentWeather, getDailyWeatherForecast];
6+
export default [InternetSearch, getCurrentWeather, getDailyWeatherForecast, searchUploadedDocuments];
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { tool } from "@langchain/core/tools";
2+
import { z } from "@app/shared/src/index.js";
3+
import { vectorStore } from "../config/vectorStore.config.js";
4+
import logger from "../utils/logger.utils.js";
5+
import { RunnableConfig } from "@langchain/core/runnables";
6+
7+
const searchUploadedDocumentsSchema = z.object({
8+
query: z.string().describe("The search query to find relevant information from the uploaded documents in the current chat."),
9+
});
10+
11+
export const searchUploadedDocuments = tool(
12+
async (input, config?: RunnableConfig) => {
13+
try {
14+
const chatId = config?.configurable?.thread_id;
15+
16+
if (!chatId) {
17+
logger.warn("search_uploaded_documents tool called without a thread_id in config.");
18+
return "Error: Could not determine the current chat context to search documents.";
19+
}
20+
21+
logger.info({ message: "Executing AI-driven vector search", query: input.query, chatId });
22+
23+
const results = await vectorStore.similaritySearch(input.query, 4, {
24+
chatId: chatId,
25+
});
26+
27+
if (!results || results.length === 0) {
28+
return "No relevant information found in the uploaded documents for this query.";
29+
}
30+
31+
const context = results.map((r) => r.pageContent).join("\n\n---\n\n");
32+
return `Context Information from uploaded documents: \n\n${context} `;
33+
34+
} catch (error) {
35+
logger.error({ message: "AI vector search failed", error, query: input.query });
36+
return "An error occurred while searching the documents. Please try again or inform the user.";
37+
}
38+
},
39+
{
40+
name: "search_uploaded_documents",
41+
description: "Search the uploaded documents within the current chat for specific information or context. Use this tool whenever the user asks about the content of their uploaded files, PDFs, presentations, or documents.",
42+
schema: searchUploadedDocumentsSchema,
43+
}
44+
);

backend/src/utils/model.utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ type Props = {
2020
preferences: UserPreference;
2121
memories: Memories[];
2222
timezone: string;
23+
isRag: boolean;
2324
};
2425

25-
export async function* generateAIResponse({ query, threadId, modelName, preferences, memories, timezone }: Props) {
26-
const agent = createGroqAgent(modelName, systemPrompt(preferences, memories, timezone));
26+
export async function* generateAIResponse({ query, threadId, modelName, preferences, memories, timezone, isRag }: Props) {
27+
const agent = createGroqAgent(modelName, systemPrompt(preferences, memories, timezone), isRag);
2728

2829
const config = { configurable: { thread_id: threadId } };
2930

frontend/src/components/containers/InputContainer.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export default function InputContainer({ sendMessage, isGenerating, selectedMode
134134
return;
135135
}
136136

137-
if (isGlobalUploading) return;
137+
if (isGlobalUploading || isRagProcessing) return;
138138

139139
const successfulUploads = attachments.filter((a) => a.status === "success" && a.uploadData).map((a) => a.uploadData!);
140140

@@ -228,10 +228,10 @@ export default function InputContainer({ sendMessage, isGenerating, selectedMode
228228

229229
const isGlobalUploading = attachments.some((a) => a.status === "uploading");
230230
const hasContent = message?.trim().length > 0 || attachments.length > 0;
231-
const isBusy = isGenerating || isGlobalUploading || isRagProcessing;
231+
const isBusy = isGenerating || isGlobalUploading;
232232

233233
return (
234-
<div className="w-full flex justify-center items-center px-4 pb-4 bg-transparent relative">
234+
<div className="w-full flex justify-center items-center px-4 pb-6 sm:pb-4 bg-transparent relative">
235235
{isDragging && (
236236
<div className="fixed inset-0 z-99 flex items-center justify-center bg-black/40 backdrop-blur-sm pointer-events-none transition-all duration-200">
237237
<div className="absolute inset-4 sm:inset-6 md:inset-8 border-4 border-dashed border-white/60 rounded-3xl z-0" />
@@ -246,8 +246,8 @@ export default function InputContainer({ sendMessage, isGenerating, selectedMode
246246
<div className="w-full max-w-svw sm:max-w-180 transition-all duration-300 ease-in-out">
247247
<form onSubmit={handleSubmit}>
248248
<div className="bg-white rounded-2xl border border-primary px-3 py-2 transition-all duration-200 ease-in-out shadow-md">
249-
{attachments.length > 0 && (
250-
<div className="flex flex-wrap gap-2 mb-2 pl-1">
249+
{(attachments.length > 0 || isRagProcessing) && (
250+
<div className="flex flex-wrap gap-2 mb-2 pl-1 items-center">
251251
{attachments.map((att) => (
252252
<div key={att.id} className="relative group">
253253
{att.file.type.startsWith("image/") ? (
@@ -299,6 +299,13 @@ export default function InputContainer({ sendMessage, isGenerating, selectedMode
299299
)}
300300
</div>
301301
))}
302+
303+
{isRagProcessing && (
304+
<div className="flex items-center gap-2 text-xs text-primary bg-primary/5 px-3 py-1.5 rounded-lg border border-primary/10 font-medium">
305+
<Loader2 size={14} className="animate-spin" />
306+
<span>Processing document...</span>
307+
</div>
308+
)}
302309
</div>
303310
)}
304311

@@ -311,7 +318,7 @@ export default function InputContainer({ sendMessage, isGenerating, selectedMode
311318
onKeyDown={handleKeyDown}
312319
onPaste={handlePaste}
313320
disabled={isBusy}
314-
placeholder={isRagProcessing ? "Document is processing..." : "Ask Anything"}
321+
placeholder="Ask Anything"
315322
rows={1}
316323
className="w-full custom-scroll custom-scroll-xs py-2 px-1.5 text-primary text-sm focus:outline-none resize-none overflow-y-auto max-h-28 transition-all duration-200 ease-in-out"
317324
/>
@@ -334,7 +341,7 @@ export default function InputContainer({ sendMessage, isGenerating, selectedMode
334341
}`}
335342
title="Attach files"
336343
>
337-
<Paperclip size={16} strokeWidth={1.8} />
344+
<Paperclip size={isMobile ? 20 : 16} strokeWidth={1.8} />
338345
</label>
339346
</div>
340347

@@ -343,7 +350,7 @@ export default function InputContainer({ sendMessage, isGenerating, selectedMode
343350

344351
<button
345352
type="submit"
346-
disabled={(!hasContent && !isGenerating) || isGlobalUploading}
353+
disabled={(!hasContent && !isGenerating) || isGlobalUploading || isRagProcessing}
347354
className={`
348355
flex items-center justify-center h-8 w-8 rounded-full transition-all duration-200
349356
${isBusy || hasContent
@@ -352,7 +359,7 @@ export default function InputContainer({ sendMessage, isGenerating, selectedMode
352359
}
353360
`}
354361
>
355-
{isGenerating || isGlobalUploading ? <Loader2 size={16} className="animate-spin" /> : <ArrowUp size={18} strokeWidth={2.5} />}
362+
{isGenerating || isGlobalUploading || isRagProcessing ? <Loader2 size={isMobile ? 20 : 16} className="animate-spin" /> : <ArrowUp size={isMobile ? 22 : 18} strokeWidth={2.5} />}
356363
</button>
357364
</div>
358365
</div>

frontend/src/components/containers/MessagesContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default function MessagesContainer({ messages, onResend, isGenerating, is
5050
const isCopied = copiedId === message.id;
5151

5252
return (
53-
<div className="px-4 mx-auto w-full max-w-svw sm:max-w-180 py-3" style={{ contain: "content" }}>
53+
<div className="px-5 sm:px-4 mx-auto w-full max-w-svw sm:max-w-180 py-4 sm:py-3" style={{ contain: "content" }}>
5454
{message.role === "ASSISTANT" ? (
5555
<ModelMessage
5656
key={message.id}

frontend/src/components/messages/ModelMessage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ type Props = {
5959
export default memo(function ModelMessage({ text, status, isCopied, onCopy, onResend, hideToolTip, isGenerating }: Props) {
6060

6161
return (
62-
<div className="max-w-full text-base md:text-sm py-4 rounded-xl">
62+
<div className="max-w-full text-[15px] sm:text-sm py-2 sm:py-4 rounded-xl">
6363
{isGenerating && text.length === 0 && !status ? (
6464
<span className="inline-block animate-pulse font-bold"></span>
6565
) : (

frontend/src/components/messages/UserMessage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default memo(function UserMessage({ message, isCopied, onCopy }: Props) {
3737
return (
3838
<div className="flex flex-col gap-2 justify-end items-end group">
3939
{hasFiles && (
40-
<div className="mb-1 w-full max-w-xs sm:max-w-lg flex flex-wrap justify-end gap-2">
40+
<div className="mb-1 w-full max-w-[85%] sm:max-w-lg flex flex-wrap justify-end gap-2">
4141
{messageFiles.map((fileItem, index) => {
4242
const { fileUrl, fileName, fileType } = fileItem;
4343
const isImage = fileType?.startsWith("image/") || (fileUrl ? isImageUrl(fileUrl) : false);
@@ -71,7 +71,7 @@ export default memo(function UserMessage({ message, isCopied, onCopy }: Props) {
7171
)}
7272

7373
{text && (
74-
<div className="user-message-color text-primary border border-gray-300/20 px-3 py-2.5 rounded-tr-none rounded-2xl max-w-xs sm:max-w-lg relative transition-all">
74+
<div className="user-message-color text-primary border border-gray-300/20 px-4 py-3 sm:px-3 sm:py-2.5 rounded-tr-none rounded-2xl max-w-[85%] sm:max-w-lg relative transition-all">
7575
{isLongText && (
7676
<button
7777
onClick={() => setIsExpanded(!isExpanded)}
@@ -89,7 +89,7 @@ export default memo(function UserMessage({ message, isCopied, onCopy }: Props) {
8989
</button>
9090
)}
9191

92-
<p className="text-base md:text-sm leading-normal whitespace-pre-wrap break-words">{displayedText}</p>
92+
<p className="text-[15px] sm:text-sm leading-relaxed sm:leading-normal whitespace-pre-wrap break-words">{displayedText}</p>
9393
</div>
9494
)}
9595

0 commit comments

Comments
 (0)