Skip to content
Open
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
.DS_Store
*.pem
.vscode
.idea/

# debug
npm-debug.log*
Expand All @@ -28,6 +29,7 @@ yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel
Expand All @@ -36,4 +38,4 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts

/public/assets
/public/assets
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ For RAG, we are using LangChain, Couchbase Vector Search & OpenAI. We fetch part

- #### Set the environment secrets

Copy the `.env.template` file in and rename it to `.env` (`.env.local` in case of local development) and replace the placeholders with the actual values for your environment
Copy the `.env.template` file in and rename it to `.env` (`.env.local` in case of local development) and replace the placeholders with the actual values for your environment. **Note: the DB_CONN_STR must start with couchbase:// and should not contain the port number.**

```
OPENAI_API_KEY=<open_ai_api_key>
Expand Down
49 changes: 35 additions & 14 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import {
CouchbaseVectorStore,
CouchbaseVectorStoreArgs,
} from "@langchain/community/vectorstores/couchbase";
CouchbaseSearchVectorStore,
} from "@langchain/community/vectorstores/couchbase_search";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { createCouchbaseCluster } from "@/lib/couchbase-connection";
import { Message as VercelChatMessage } from "ai";
import { HumanMessage, AIMessage, ChatMessage } from "@langchain/core/messages";
import { createHistoryAwareRetriever } from "langchain/chains/history_aware_retriever";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { createRetrievalChain } from "langchain/chains/retrieval";
import { createHistoryAwareRetriever } from "@langchain/classic/chains/history_aware_retriever";
import { createStuffDocumentsChain } from "@langchain/classic/chains/combine_documents";
import { createRetrievalChain } from "@langchain/classic/chains/retrieval";
import { Document } from '@langchain/core/documents';
import {NextResponse} from 'next/server';

const formatVercelMessages = (message: VercelChatMessage) => {
if (message.role === "user") {
Expand All @@ -26,6 +26,12 @@ const formatVercelMessages = (message: VercelChatMessage) => {
};

export async function POST(request: Request) {
// Load environment variables explicitly
const openaiApiKey = process.env.OPENAI_API_KEY;
if (!openaiApiKey) {
return NextResponse.json({ error: "OPENAI_API_KEY not set" }, { status: 500 });
}

const body = await request.json();
const messages = body.messages ?? [];
if (!messages.length) {
Expand All @@ -38,10 +44,12 @@ export async function POST(request: Request) {
const currentMessageContent = messages[messages.length - 1].content;
try {
const model = new ChatOpenAI({
model: "gpt-4"
model: "gpt-4",
openAIApiKey: openaiApiKey,
});
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
apiKey: openaiApiKey,
model: "text-embedding-3-small",
});

const historyAwarePrompt = ChatPromptTemplate.fromMessages([
Expand Down Expand Up @@ -78,7 +86,12 @@ export async function POST(request: Request) {
const scopedIndex = true;

const cluster = await createCouchbaseCluster();
const couchbaseConfig: CouchbaseVectorStoreArgs = {

if (!cluster) {
throw new Error("Couchbase cluster connection failed");
}
Comment on lines +90 to +92

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This check for !cluster is redundant. The createCouchbaseCluster function is designed to throw an error if the connection fails, which will be caught by the outer try...catch block in this POST handler. As a result, this if block is unreachable and can be safely removed.


const couchbaseConfig = {
cluster,
bucketName,
scopeName,
Expand All @@ -88,7 +101,7 @@ export async function POST(request: Request) {
embeddingKey,
scopedIndex,
};
const couchbaseVectorStore = await CouchbaseVectorStore.initialize(
const couchbaseSearchVectorStore = await CouchbaseSearchVectorStore.initialize(
embeddings,
couchbaseConfig
);
Expand All @@ -98,7 +111,9 @@ export async function POST(request: Request) {
resolveWithDocuments = resolve;
});

const retriever = couchbaseVectorStore.asRetriever({
const retriever = couchbaseSearchVectorStore.asRetriever({
searchType: "similarity",
searchKwargs: { k: 4 },
callbacks: [
{
handleRetrieverEnd(documents) {
Expand All @@ -108,15 +123,20 @@ export async function POST(request: Request) {
},
},
],
}
);
});

// Create the history-aware retriever chain
const historyAwareRetrieverChain = await createHistoryAwareRetriever({
llm: model,
retriever,
rephrasePrompt: historyAwarePrompt,
});

// Use history-aware retriever only when there's chat history
const retrieverToUse = formattedPreviousMessages.length > 0
? historyAwareRetrieverChain
: retriever;

// Create a chain that answers questions using retrieved relevant documents as context.
const documentChain = await createStuffDocumentsChain({
llm: model,
Expand All @@ -125,7 +145,7 @@ export async function POST(request: Request) {

// Create a chain that combines the above retriever and question answering chains.
const conversationalRetrievalChain = await createRetrievalChain({
retriever: historyAwareRetrieverChain,
retriever: retrieverToUse,
combineDocsChain: documentChain,
});

Expand Down Expand Up @@ -164,5 +184,6 @@ export async function POST(request: Request) {

} catch (err) {
console.log("Error Received ", err);
return NextResponse.json({ error: "Failed to process chat message" });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's good practice to include an HTTP status code with error responses. For a server-side failure like this, returning a 500 Internal Server Error status is more informative for clients and monitoring tools.

    return NextResponse.json({ error: "Failed to process chat message" }, { status: 500 });

}
}
16 changes: 10 additions & 6 deletions app/api/ingestPdf/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { NextResponse } from "next/server";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import {
CouchbaseVectorStore,
CouchbaseVectorStoreArgs,
} from "@langchain/community/vectorstores/couchbase";
CouchbaseSearchVectorStore, CouchbaseSearchVectorStoreArgs,
} from "@langchain/community/vectorstores/couchbase_search";
import { OpenAIEmbeddings } from "@langchain/openai";
import { createCouchbaseCluster } from "@/lib/couchbase-connection";
import { writeFile } from "fs/promises";
Expand Down Expand Up @@ -52,7 +51,12 @@ export async function POST(request: Request) {
const scopedIndex = true;

const cluster = await createCouchbaseCluster();
const couchbaseConfig: CouchbaseVectorStoreArgs = {

if (!cluster) {
throw new Error("Couchbase cluster connection failed");
}
Comment on lines +55 to +57

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This check for !cluster is redundant. The createCouchbaseCluster function will throw an error if the connection fails, and this error will be caught by the try...catch block in this POST handler. Therefore, this if block is unreachable and can be removed.


const couchbaseConfig: CouchbaseSearchVectorStoreArgs = {
cluster,
bucketName,
scopeName,
Expand All @@ -62,7 +66,7 @@ export async function POST(request: Request) {
embeddingKey,
scopedIndex,
};
await CouchbaseVectorStore.fromDocuments(docs, embeddings, couchbaseConfig);
await CouchbaseSearchVectorStore.fromDocuments(docs, embeddings, couchbaseConfig);

console.log("creating vector store...");
} catch (error) {
Expand Down
19 changes: 15 additions & 4 deletions app/chatPage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
TransformToolbarSlot,
} from "@react-pdf-viewer/toolbar";
import { toolbarPlugin } from "@react-pdf-viewer/toolbar";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, Suspense } from "react";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
import { useChat } from "ai/react";
Expand All @@ -16,7 +16,7 @@ import LoadingDots from "@/components/LoadingDots";
import "@react-pdf-viewer/core/lib/styles/index.css";
import "@react-pdf-viewer/default-layout/lib/styles/index.css";

const ChatPage = () => {
const ChatContent = ({ searchParams }: { searchParams: URLSearchParams }) => {
const [sourcesForMessages, setSourcesForMessages] = useState<
Record<string, any>
>({});
Expand All @@ -25,7 +25,7 @@ const ChatPage = () => {
const toolbarPluginInstance = toolbarPlugin();
const pageNavigationPluginInstance = pageNavigationPlugin();
const { renderDefaultToolbar, Toolbar } = toolbarPluginInstance;
const searchParams = useSearchParams();

useEffect(() => {
setPdfUrl(("/assets/" + searchParams.get("fileName")) as string);
}, [searchParams]);
Expand Down Expand Up @@ -237,4 +237,15 @@ const ChatPage = () => {
);
};

export default ChatPage;
export default function ChatPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ChatPageContentWrapper />
</Suspense>
);
}

function ChatPageContentWrapper() {
const searchParams = useSearchParams();
return <ChatContent searchParams={searchParams} />;
}
18 changes: 14 additions & 4 deletions components/PDFUploader.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
"use client";

import { useRouter, useSearchParams } from "next/navigation";
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useState, Suspense } from "react";
import { useDropzone } from "react-dropzone";
import { Loader } from "./Loader";

const PDFUploader = () => {
const PDFUploaderContent = ({ searchParams }: { searchParams: URLSearchParams }) => {
const router = useRouter();
const searchParams = useSearchParams();

const createQueryString = useCallback(
(name: string, value: string) => {
Expand Down Expand Up @@ -98,4 +97,15 @@ const PDFUploader = () => {
);
};

export default PDFUploader;
export default function PDFUploader() {
return (
<Suspense fallback={<div>Loading...</div>}>
<PDFUploaderWrapper />
</Suspense>
);
}

function PDFUploaderWrapper() {
const searchParams = useSearchParams();
return <PDFUploaderContent searchParams={searchParams} />;
}
19 changes: 12 additions & 7 deletions lib/couchbase-connection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { connect, Cluster } from "couchbase";

export async function createCouchbaseCluster(): Promise<Cluster> {
export async function createCouchbaseCluster(): Promise<Cluster | void> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The function's return type is declared as Promise<Cluster | void>, but it never resolves to void. It either returns a Promise<Cluster> on a successful connection or throws an error (resulting in a rejected promise). The return type should be Promise<Cluster> to accurately reflect the function's behavior.

Suggested change
export async function createCouchbaseCluster(): Promise<Cluster | void> {
export async function createCouchbaseCluster(): Promise<Cluster> {

const connectionString = process.env.DB_CONN_STR;
const databaseUsername = process.env.DB_USERNAME;
const databasePassword = process.env.DB_PASSWORD;
Expand All @@ -23,11 +23,16 @@ export async function createCouchbaseCluster(): Promise<Cluster> {
);
}

const cluster = await connect(connectionString, {
username: databaseUsername,
password: databasePassword,
configProfile: "wanDevelopment",
});
try {
return await connect(connectionString, {
username: databaseUsername,
password: databasePassword,
configProfile: "wanDevelopment",
});
} catch (e) {
throw new Error(
`Could not connect to the Couchbase cluster. Please check your DB_CONN_STR, DB_USERNAME, and DB_PASSWORD environment variables. \n${e}`,
)
}

return cluster;
}
Loading
Loading