Skip to content

Commit

Permalink
feat: add chat AI (#12)
Browse files Browse the repository at this point in the history
* feat: create getFileUploadStatus endpoint

* feat: refactor chat wrapper component and add chat input and messages components

* feat: add chevron icons

* feat: add button back to dashboard

* feat: add component textarea from shadcn-ui

* feat: add chat input and send button

* feat: update .env.example with empty values for KINDE_ISSUER_URL, PINECONE_API_KEY, OPENAI_API_KEY

* chore: add pinecone and langchain dependencies

* feat: add Message model and its relations

* feat: add route for sending messages and chat context for handling messages

* feat: add file upload functionality and process the uploaded file

* chore: add pdf-parser

* chore: add openAI and ai from vercel

* feat: add OpenAI and Pinecone clients
  • Loading branch information
vagnermaltauro authored Dec 17, 2023
1 parent 2d18668 commit df047a2
Show file tree
Hide file tree
Showing 9 changed files with 455 additions and 26 deletions.
81 changes: 81 additions & 0 deletions app/api/message/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { NextRequest } from 'next/server';
import { db } from '@/db';
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server';
import { OpenAIStream, StreamingTextResponse } from 'ai';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { PineconeStore } from 'langchain/vectorstores/pinecone';

import { openai } from '@/lib/openai';
import { getPineconeClient } from '@/lib/pinecone';
import { SendMessageValidator } from '@/lib/validators/send-message-validator';

export async function POST(req: NextRequest) {
Expand Down Expand Up @@ -31,4 +36,80 @@ export async function POST(req: NextRequest) {
fileId,
},
});

const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});
const pinecone = await getPineconeClient();
const pineconeIndex = pinecone.Index('doc-quest');

const vectorStore = await PineconeStore.fromExistingIndex(embeddings, {
//@ts-ignore
pineconeIndex,
namespace: file.id,
});

const results = await vectorStore.similaritySearch(message, 4);

const prevMessages = await db.message.findMany({
where: {
fileId,
},
orderBy: {
createdAt: 'asc',
},
take: 6,
});

const formattedPrevMessages = prevMessages.map((msg) => ({
role: msg.isUserMessage ? ('user' as const) : ('assistant' as const),
content: msg.text,
}));

const response = await openai.chat.completions.create({
model: 'gpt-3.5-turbo-16k',
temperature: 0,
stream: true,
messages: [
{
role: 'system',
content:
'Use the following pieces of context (or previous conversaton if needed) to answer the users question in markdown format.',
},
{
role: 'user',
content: `Use the following pieces of context (or previous conversaton if needed) to answer the users question in markdown format. \nIf you don't know the answer, just say that you don't know, don't try to make up an answer.
\n----------------\n
PREVIOUS CONVERSATION:
${formattedPrevMessages.map((message) => {
if (message.role === 'user') return `User: ${message.content}\n`;
return `Assistant: ${message.content}\n`;
})}
\n----------------\n
CONTEXT:
${results.map((r) => r.pageContent).join('\n\n')}
USER INPUT: ${message}`,
},
],
});

const stream = OpenAIStream(response, {
async onCompletion(completion) {
await db.message.create({
data: {
text: completion,
isUserMessage: false,
fileId,
userId,
},
});
},
});

return new StreamingTextResponse(stream);
}
3 changes: 2 additions & 1 deletion app/api/uploadthing/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { createUploadthing, type FileRouter } from 'uploadthing/next';

import { pinecone } from '@/lib/pinecone';
import { getPineconeClient } from '@/lib/pinecone';

const f = createUploadthing();

Expand Down Expand Up @@ -39,6 +39,7 @@ export const ourFileRouter = {
const pageLevelDocs = await loader.load();
const pagesAmt = pageLevelDocs.length;

const pinecone = await getPineconeClient();
const pineconeIndex = pinecone.Index('doc-quest');
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
Expand Down
29 changes: 28 additions & 1 deletion components/chat/chat-input.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import * as React from 'react';

import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { ChatContext } from '@/components/chat/chat-context';
import { Icons } from '@/components/icons';

interface ChatInputProps {
isDisabled?: boolean;
}

export function ChatInput({ isDisabled }: ChatInputProps) {
const { addMessage, handleInputChange, isLoading, message } = React.useContext(ChatContext);
const textareaRef = React.useRef<HTMLTextAreaElement>(null);

return (
<div className="absolute bottom-0 left-0 w-full">
<form className="mx-2 flex flex-row gap-3 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-2xl xl:max-w-3xl">
Expand All @@ -15,12 +21,33 @@ export function ChatInput({ isDisabled }: ChatInputProps) {
<div className="relative">
<Textarea
rows={1}
ref={textareaRef}
maxRows={4}
autoFocus
onChange={handleInputChange}
value={message}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();

addMessage();

textareaRef.current?.focus();
}
}}
placeholder="Enter your question..."
className="resize-none pr-12 text-base py-3 scrollbar-thumb-black scrollbar-thumb-rounded scrollbar-track-black-lighter scrollbar-w-2 scrolling-touch"
/>
<Button className="absolute bottom-1.5 right-[8px]" aria-label="send message">
<Button
className="absolute bottom-1.5 right-[8px]"
aria-label="send message"
disabled={isLoading || isDisabled}
onClick={() => {
addMessage();

textareaRef.current?.focus();
}}
>
<Icons.send className="h-4 w-4" />
</Button>
</div>
Expand Down
13 changes: 8 additions & 5 deletions components/chat/chat-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Link from 'next/link';

import { buttonVariants } from '@/components/ui/button';
import { ChatContextProvider } from '@/components/chat/chat-context';
import { ChatInput } from '@/components/chat/chat-input';
import { Messages } from '@/components/chat/messages';
import { Icons } from '@/components/icons';
Expand Down Expand Up @@ -78,11 +79,13 @@ export function ChatWrapper({ fileId }: ChatWrapperProps) {
);

return (
<div className="relative min-h-full bg-zinc-50 flex divide-y divide-zinc-200 flex-col justify-between gap-2">
<div className="flex-1 justify-between flex flex-col mb-28">
<Messages />
<ChatContextProvider fileId={fileId}>
<div className="relative min-h-full bg-zinc-50 flex divide-y divide-zinc-200 flex-col justify-between gap-2">
<div className="flex-1 justify-between flex flex-col mb-28">
<Messages />
</div>
<ChatInput />
</div>
<ChatInput />
</div>
</ChatContextProvider>
);
}
4 changes: 3 additions & 1 deletion components/upload-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ function UploadDropzone() {
>
{({ getRootProps, getInputProps, acceptedFiles }) => (
<div
{...getRootProps()}
{...getRootProps({
onClick: (e) => e.preventDefault(),
})}
className="border h-64 m-4 border-dashed border-gray-300 rounded-lg"
>
<div className="flex items-center justify-center h-full w-full">
Expand Down
5 changes: 5 additions & 0 deletions lib/openai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import OpenAI from 'openai';

export const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
11 changes: 7 additions & 4 deletions lib/pinecone.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Pinecone } from '@pinecone-database/pinecone';

export const pinecone = new Pinecone({
apiKey: process.env.PINECONE_API_KEY!,
environment: 'gcp-starter',
});
export const getPineconeClient = async () => {
const client = new Pinecone({
environment: 'gcp-starter',
apiKey: process.env.PINECONE_API_KEY!,
});
return client;
};
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"dependencies": {
"@hookform/resolvers": "^3.3.2",
"@kinde-oss/kinde-auth-nextjs": "^1.8.23",
"@pinecone-database/pinecone": "^1.1.2",
"@pinecone-database/pinecone": "^1.0.1",
"@prisma/client": "5.4.2",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
Expand All @@ -31,26 +31,28 @@
"@trpc/next": "^10.38.4",
"@trpc/react-query": "^10.38.4",
"@trpc/server": "^10.38.4",
"@uploadthing/react": "^5.7.0",
"@uploadthing/react": "^5.6.1",
"ai": "^2.2.13",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"langchain": "^0.0.208",
"lucide-react": "^0.287.0",
"next": "13.5.4",
"openai": "^4.10.0",
"pdf-parse": "^1.1.1",
"react": "^18",
"react-dom": "^18",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.48.2",
"react-loading-skeleton": "^3.3.1",
"react-pdf": "^7.5.1",
"react-pdf": "^7.3.3",
"react-resize-detector": "^9.1.0",
"react-textarea-autosize": "^8.5.3",
"simplebar-react": "^3.2.4",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uploadthing": "^5.7.2",
"uploadthing": "^5.6.1",
"zod": "^3.21.4"
},
"devDependencies": {
Expand Down
Loading

0 comments on commit df047a2

Please sign in to comment.