├── .env.example
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── app
├── (preview)
│ ├── api
│ │ └── chat
│ │ │ └── route.ts
│ ├── globals.css
│ ├── layout.tsx
│ ├── opengraph-image.png
│ ├── page.tsx
│ └── uncut-sans.woff2
└── favicon.ico
├── components.json
├── components
├── icons.tsx
├── project-overview.tsx
└── ui
│ ├── button.tsx
│ ├── input.tsx
│ └── label.tsx
├── drizzle.config.ts
├── lib
├── actions
│ └── resources.ts
├── ai
│ └── embedding.ts
├── db
│ ├── index.ts
│ ├── migrate.ts
│ ├── migrations
│ │ ├── 0000_yielding_bloodaxe.sql
│ │ └── meta
│ │ │ ├── 0000_snapshot.json
│ │ │ └── _journal.json
│ └── schema
│ │ ├── embeddings.ts
│ │ └── resources.ts
├── env.mjs
└── utils.ts
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── public
├── next.svg
└── vercel.svg
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgres://postgres:postgres@localhost:5432/{DB_NAME}
2 | OPENAI_API_KEY=sk***
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Vercel Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AI SDK RAG Template
2 |
3 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnicoalbanese%2Fai-sdk-rag-template&env=OPENAI_API_KEY&envDescription=You%20will%20need%20an%20OPENAI%20API%20Key.&project-name=ai-sdk-rag&repository-name=ai-sdk-rag&stores=%5B%7B%22type%22%3A%22postgres%22%7D%5D&skippable-integrations=1)
4 |
5 | A [Next.js](https://nextjs.org/) application, powered by the Vercel AI SDK, that uses retrieval-augmented generation (RAG) to reason and respond with information outside of the model's training data.
6 |
7 | ## Features
8 |
9 | - Information retrieval and addition through tool calls using the [`streamText`](https://sdk.vercel.ai/docs/reference/ai-sdk-core/stream-text) function
10 | - Real-time streaming of model responses to the frontend using the [`useChat`](https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-chat) hook
11 | - Vector embedding storage with [DrizzleORM](https://orm.drizzle.team/) and [PostgreSQL](https://www.postgresql.org/)
12 | - Animated UI with [Framer Motion](https://www.framer.com/motion/)
13 |
14 | ## Getting Started
15 |
16 | To get the project up and running, follow these steps:
17 |
18 | 1. Install dependencies:
19 |
20 | ```bash
21 | npm install
22 | ```
23 |
24 | 2. Copy the example environment file:
25 |
26 | ```bash
27 | cp .env.example .env
28 | ```
29 |
30 | 3. Add your OpenAI API key and PostgreSQL connection string to the `.env` file:
31 |
32 | ```
33 | OPENAI_API_KEY=your_api_key_here
34 | DATABASE_URL=your_postgres_connection_string_here
35 | ```
36 |
37 | 4. Migrate the database schema:
38 |
39 | ```bash
40 | npm run db:migrate
41 | ```
42 |
43 | 5. Start the development server:
44 | ```bash
45 | npm run dev
46 | ```
47 |
48 | Your project should now be running on [http://localhost:3000](http://localhost:3000).
49 |
--------------------------------------------------------------------------------
/app/(preview)/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { createResource } from "@/lib/actions/resources";
2 | import { findRelevantContent } from "@/lib/ai/embedding";
3 | import { openai } from "@ai-sdk/openai";
4 | import { generateObject, streamText, tool } from "ai";
5 | import { z } from "zod";
6 |
7 | // Allow streaming responses up to 30 seconds
8 | export const maxDuration = 30;
9 |
10 | export async function POST(req: Request) {
11 | const { messages } = await req.json();
12 |
13 | const result = streamText({
14 | model: openai("gpt-4o"),
15 | messages,
16 | system: `You are a helpful assistant acting as the users' second brain.
17 | Use tools on every request.
18 | Be sure to getInformation from your knowledge base before answering any questions.
19 | If the user presents infromation about themselves, use the addResource tool to store it.
20 | If a response requires multiple tools, call one tool after another without responding to the user.
21 | If a response requires information from an additional tool to generate a response, call the appropriate tools in order before responding to the user.
22 | ONLY respond to questions using information from tool calls.
23 | if no relevant information is found in the tool calls, respond, "Sorry, I don't know."
24 | Be sure to adhere to any instructions in tool calls ie. if they say to responsd like "...", do exactly that.
25 | If the relevant information is not a direct match to the users prompt, you can be creative in deducing the answer.
26 | Keep responses short and concise. Answer in a single sentence where possible.
27 | If you are unsure, use the getInformation tool and you can use common sense to reason based on the information you do have.
28 | Use your abilities as a reasoning machine to answer questions based on the information you do have.
29 | `,
30 | tools: {
31 | addResource: tool({
32 | description: `add a resource to your knowledge base.
33 | If the user provides a random piece of knowledge unprompted, use this tool without asking for confirmation.`,
34 | parameters: z.object({
35 | content: z
36 | .string()
37 | .describe("the content or resource to add to the knowledge base"),
38 | }),
39 | execute: async ({ content }) => createResource({ content }),
40 | }),
41 | getInformation: tool({
42 | description: `get information from your knowledge base to answer questions.`,
43 | parameters: z.object({
44 | question: z.string().describe("the users question"),
45 | similarQuestions: z.array(z.string()).describe("keywords to search"),
46 | }),
47 | execute: async ({ similarQuestions }) => {
48 | const results = await Promise.all(
49 | similarQuestions.map(
50 | async (question) => await findRelevantContent(question),
51 | ),
52 | );
53 | // Flatten the array of arrays and remove duplicates based on 'name'
54 | const uniqueResults = Array.from(
55 | new Map(results.flat().map((item) => [item?.name, item])).values(),
56 | );
57 | return uniqueResults;
58 | },
59 | }),
60 | understandQuery: tool({
61 | description: `understand the users query. use this tool on every prompt.`,
62 | parameters: z.object({
63 | query: z.string().describe("the users query"),
64 | toolsToCallInOrder: z
65 | .array(z.string())
66 | .describe(
67 | "these are the tools you need to call in the order necessary to respond to the users query",
68 | ),
69 | }),
70 | execute: async ({ query }) => {
71 | const { object } = await generateObject({
72 | model: openai("gpt-4o"),
73 | system:
74 | "You are a query understanding assistant. Analyze the user query and generate similar questions.",
75 | schema: z.object({
76 | questions: z
77 | .array(z.string())
78 | .max(3)
79 | .describe("similar questions to the user's query. be concise."),
80 | }),
81 | prompt: `Analyze this query: "${query}". Provide the following:
82 | 3 similar questions that could help answer the user's query`,
83 | });
84 | return object.questions;
85 | },
86 | }),
87 | },
88 | });
89 |
90 | return result.toDataStreamResponse();
91 | }
92 |
--------------------------------------------------------------------------------
/app/(preview)/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 0 0% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 0 0% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 0 0% 3.9%;
15 |
16 | --primary: 0 0% 9%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 0 0% 96.1%;
20 | --secondary-foreground: 0 0% 9%;
21 |
22 | --muted: 0 0% 96.1%;
23 | --muted-foreground: 0 0% 45.1%;
24 |
25 | --accent: 0 0% 96.1%;
26 | --accent-foreground: 0 0% 9%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 0 0% 89.8%;
32 | --input: 0 0% 89.8%;
33 | --ring: 0 0% 3.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 0 0% 3.9%;
40 | --foreground: 0 0% 98%;
41 |
42 | --card: 0 0% 3.9%;
43 | --card-foreground: 0 0% 98%;
44 |
45 | --popover: 0 0% 3.9%;
46 | --popover-foreground: 0 0% 98%;
47 |
48 | --primary: 0 0% 98%;
49 | --primary-foreground: 0 0% 9%;
50 |
51 | --secondary: 0 0% 14.9%;
52 | --secondary-foreground: 0 0% 98%;
53 |
54 | --muted: 0 0% 14.9%;
55 | --muted-foreground: 0 0% 63.9%;
56 |
57 | --accent: 0 0% 14.9%;
58 | --accent-foreground: 0 0% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 0 0% 98%;
62 |
63 | --border: 0 0% 14.9%;
64 | --input: 0 0% 14.9%;
65 | --ring: 0 0% 83.1%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
77 |
78 | .no-scrollbar-gutter {
79 | /* Remove the scrollbar gutter */
80 | scrollbar-gutter: auto;
81 |
82 | /* Enable vertical scrolling */
83 | overflow-y: scroll;
84 |
85 | /* Hide scrollbar for IE, Edge and Firefox */
86 | /* -ms-overflow-style: none; /* IE and Edge */
87 | /* scrollbar-width: none; /* Firefox */
88 | }
89 |
90 | /* Hide scrollbar for Chrome, Safari and Opera */
91 | .no-scrollbar-gutter::-webkit-scrollbar {
92 | display: none;
93 | }
94 |
95 | @font-face {
96 | font-family: "uncut sans";
97 | src: url("./uncut-sans.woff2") format("woff2");
98 | }
99 |
100 | * {
101 | font-family: "uncut sans", sans-serif;
102 | }
103 |
104 | #markdown a {
105 | @apply text-blue-500;
106 | }
--------------------------------------------------------------------------------
/app/(preview)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 |
3 | import "./globals.css";
4 |
5 | export const metadata: Metadata = {
6 | metadataBase: new URL("https://ai-sdk-preview-rag.vercel.app"),
7 | title: "Retrieval Augmented Generation Preview",
8 | description:
9 | "Augment language model generations with vector based retrieval using the Vercel AI SDK",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: Readonly<{
15 | children: React.ReactNode;
16 | }>) {
17 | return (
18 |
19 |
{children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/app/(preview)/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/ai-sdk-preview-rag/6f3418b5482f9f01626701b4a18de959721eed13/app/(preview)/opengraph-image.png
--------------------------------------------------------------------------------
/app/(preview)/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Input } from "@/components/ui/input";
4 | import { Message } from "ai";
5 | import { useChat } from "ai/react";
6 | import { useEffect, useMemo, useState } from "react";
7 | import { AnimatePresence, motion } from "framer-motion";
8 | import ReactMarkdown, { Options } from "react-markdown";
9 | import React from "react";
10 | import ProjectOverview from "@/components/project-overview";
11 | import { LoadingIcon } from "@/components/icons";
12 | import { cn } from "@/lib/utils";
13 | import { toast } from "sonner";
14 |
15 | export default function Chat() {
16 | const [toolCall, setToolCall] = useState();
17 | const { messages, input, handleInputChange, handleSubmit, isLoading } =
18 | useChat({
19 | maxSteps: 4,
20 | onToolCall({ toolCall }) {
21 | setToolCall(toolCall.toolName);
22 | },
23 | onError: (error) => {
24 | toast.error("You've been rate limited, please try again later!");
25 | },
26 | });
27 |
28 | const [isExpanded, setIsExpanded] = useState(false);
29 |
30 | useEffect(() => {
31 | if (messages.length > 0) setIsExpanded(true);
32 | }, [messages]);
33 |
34 | const currentToolCall = useMemo(() => {
35 | const tools = messages?.slice(-1)[0]?.toolInvocations;
36 | if (tools && toolCall === tools[0].toolName) {
37 | return tools[0].toolName;
38 | } else {
39 | return undefined;
40 | }
41 | }, [toolCall, messages]);
42 |
43 | const awaitingResponse = useMemo(() => {
44 | if (
45 | isLoading &&
46 | currentToolCall === undefined &&
47 | messages.slice(-1)[0].role === "user"
48 | ) {
49 | return true;
50 | } else {
51 | return false;
52 | }
53 | }, [isLoading, currentToolCall, messages]);
54 |
55 | const userQuery: Message | undefined = messages
56 | .filter((m) => m.role === "user")
57 | .slice(-1)[0];
58 |
59 | const lastAssistantMessage: Message | undefined = messages
60 | .filter((m) => m.role !== "user")
61 | .slice(-1)[0];
62 |
63 | return (
64 |
65 |
66 |
67 |
83 |
84 |
94 |
100 |
101 | {awaitingResponse || currentToolCall ? (
102 |
103 |
104 | {userQuery.content}
105 |
106 |
107 |
108 | ) : lastAssistantMessage ? (
109 |
110 |
111 | {userQuery.content}
112 |
113 |
114 |
115 | ) : null}
116 |
117 |
118 |
119 |
120 |
121 |
122 | );
123 | }
124 |
125 | const AssistantMessage = ({ message }: { message: Message | undefined }) => {
126 | if (message === undefined) return "HELLO";
127 |
128 | return (
129 |
130 |
138 |
141 | {message.content}
142 |
143 |
144 |
145 | );
146 | };
147 |
148 | const Loading = ({ tool }: { tool?: string }) => {
149 | const toolName =
150 | tool === "getInformation"
151 | ? "Getting information"
152 | : tool === "addResource"
153 | ? "Adding information"
154 | : "Thinking";
155 |
156 | return (
157 |
158 |
165 |
166 |
167 |
168 |
169 |
170 | {toolName}...
171 |
172 |
173 |
174 |
175 | );
176 | };
177 |
178 | const MemoizedReactMarkdown: React.FC = React.memo(
179 | ReactMarkdown,
180 | (prevProps, nextProps) =>
181 | prevProps.children === nextProps.children &&
182 | prevProps.className === nextProps.className,
183 | );
184 |
--------------------------------------------------------------------------------
/app/(preview)/uncut-sans.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/ai-sdk-preview-rag/6f3418b5482f9f01626701b4a18de959721eed13/app/(preview)/uncut-sans.woff2
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/ai-sdk-preview-rag/6f3418b5482f9f01626701b4a18de959721eed13/app/favicon.ico
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/components/icons.tsx:
--------------------------------------------------------------------------------
1 | export const VercelIcon = ({ size = 17 }) => {
2 | return (
3 |
17 | );
18 | };
19 |
20 | export const InformationIcon = ({ size = 17 }) => {
21 | return (
22 |
36 | );
37 | };
38 |
39 | export const LoadingIcon = () => {
40 | return (
41 |
111 | );
112 | };
113 |
--------------------------------------------------------------------------------
/components/project-overview.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 | import Link from "next/link";
4 | import { InformationIcon, VercelIcon } from "./icons";
5 |
6 | const ProjectOverview = () => {
7 | return (
8 |
14 |
15 |
16 |
17 | +
18 |
19 |
20 |
21 | The{" "}
22 |
26 | useChat
27 | {" "}
28 | hook along with the{" "}
29 |
33 | streamText
34 | {" "}
35 | function allows you to build applications with retrieval augmented
36 | generation (RAG) capabilities. Data is stored as vector embeddings
37 | using DrizzleORM and PostgreSQL.
38 |
39 |
40 | Learn how to build this project by following this{" "}
41 |
46 | guide
47 |
48 | .
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default ProjectOverview;
56 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "drizzle-kit";
2 | import { env } from "@/lib/env.mjs";
3 |
4 | export default {
5 | schema: "./lib/db/schema",
6 | dialect: "postgresql",
7 | out: "./lib/db/migrations",
8 | dbCredentials: {
9 | url: env.DATABASE_URL,
10 | }
11 | } satisfies Config;
--------------------------------------------------------------------------------
/lib/actions/resources.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import {
4 | NewResourceParams,
5 | insertResourceSchema,
6 | resources,
7 | } from "@/lib/db/schema/resources";
8 | import { generateEmbeddings } from "../ai/embedding";
9 | import { db } from "../db";
10 | import { embeddings as embeddingsTable } from "../db/schema/embeddings";
11 |
12 | export const createResource = async (input: NewResourceParams) => {
13 | try {
14 | const { content } = insertResourceSchema.parse(input);
15 |
16 | const [resource] = await db
17 | .insert(resources)
18 | .values({ content })
19 | .returning();
20 |
21 | const embeddings = await generateEmbeddings(content);
22 | await db.insert(embeddingsTable).values(
23 | embeddings.map((embedding) => ({
24 | resourceId: resource.id,
25 | ...embedding,
26 | })),
27 | );
28 | return "Resource successfully created and embedded.";
29 | } catch (error) {
30 | return error instanceof Error && error.message.length > 0
31 | ? error.message
32 | : "Error, please try again.";
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/lib/ai/embedding.ts:
--------------------------------------------------------------------------------
1 | import { embed, embedMany } from "ai";
2 | import { openai } from "@ai-sdk/openai";
3 | import { cosineDistance, desc, gt, sql } from "drizzle-orm";
4 | import { embeddings } from "../db/schema/embeddings";
5 | import { db } from "../db";
6 |
7 | const embeddingModel = openai.embedding("text-embedding-ada-002");
8 |
9 | const generateChunks = (input: string): string[] => {
10 | return input
11 | .trim()
12 | .split(".")
13 | .filter((i) => i !== "");
14 | };
15 |
16 | export const generateEmbeddings = async (
17 | value: string,
18 | ): Promise> => {
19 | const chunks = generateChunks(value);
20 | const { embeddings } = await embedMany({
21 | model: embeddingModel,
22 | values: chunks,
23 | });
24 | return embeddings.map((e, i) => ({ content: chunks[i], embedding: e }));
25 | };
26 |
27 | export const generateEmbedding = async (value: string): Promise => {
28 | const input = value.replaceAll("\n", " ");
29 | const { embedding } = await embed({
30 | model: embeddingModel,
31 | value: input,
32 | });
33 | return embedding;
34 | };
35 |
36 | export const findRelevantContent = async (userQuery: string) => {
37 | const userQueryEmbedded = await generateEmbedding(userQuery);
38 | const similarity = sql`1 - (${cosineDistance(embeddings.embedding, userQueryEmbedded)})`;
39 | const similarGuides = await db
40 | .select({ name: embeddings.content, similarity })
41 | .from(embeddings)
42 | .where(gt(similarity, 0.3))
43 | .orderBy((t) => desc(t.similarity))
44 | .limit(4);
45 | return similarGuides;
46 | };
47 |
--------------------------------------------------------------------------------
/lib/db/index.ts:
--------------------------------------------------------------------------------
1 | import { drizzle } from "drizzle-orm/postgres-js";
2 | import postgres from "postgres";
3 | import { env } from "@/lib/env.mjs";
4 |
5 | const client = postgres(env.DATABASE_URL!);
6 | export const db = drizzle(client);
7 |
8 |
--------------------------------------------------------------------------------
/lib/db/migrate.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@/lib/env.mjs";
2 |
3 | import { drizzle } from "drizzle-orm/postgres-js";
4 | import { migrate } from "drizzle-orm/postgres-js/migrator";
5 | import postgres from "postgres";
6 | import "dotenv/config";
7 |
8 | const runMigrate = async () => {
9 | if (!env.DATABASE_URL) {
10 | throw new Error("DATABASE_URL is not defined");
11 | }
12 |
13 | const connection = postgres(env.DATABASE_URL, { max: 1 });
14 |
15 | const db = drizzle(connection);
16 |
17 | console.log("⏳ Running migrations...");
18 |
19 | const start = Date.now();
20 |
21 | await migrate(db, { migrationsFolder: "lib/db/migrations" });
22 |
23 | const end = Date.now();
24 |
25 | console.log("✅ Migrations completed in", end - start, "ms");
26 |
27 | process.exit(0);
28 | };
29 |
30 | runMigrate().catch((err) => {
31 | console.error("❌ Migration failed");
32 | console.error(err);
33 | process.exit(1);
34 | });
35 |
--------------------------------------------------------------------------------
/lib/db/migrations/0000_yielding_bloodaxe.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS vector;
2 |
3 | CREATE TABLE IF NOT EXISTS "resources" (
4 | "id" varchar(191) PRIMARY KEY NOT NULL,
5 | "content" text NOT NULL,
6 | "created_at" timestamp DEFAULT now() NOT NULL,
7 | "updated_at" timestamp DEFAULT now() NOT NULL
8 | );
9 |
10 | CREATE TABLE IF NOT EXISTS "embeddings" (
11 | "id" varchar(191) PRIMARY KEY NOT NULL,
12 | "resource_id" varchar(191),
13 | "content" text NOT NULL,
14 | "embedding" vector(1536) NOT NULL
15 | );
16 | --> statement-breakpoint
17 | DO $$ BEGIN
18 | ALTER TABLE "embeddings" ADD CONSTRAINT "embeddings_resource_id_resources_id_fk" FOREIGN KEY ("resource_id") REFERENCES "public"."resources"("id") ON DELETE cascade ON UPDATE no action;
19 | EXCEPTION
20 | WHEN duplicate_object THEN null;
21 | END $$;
22 | --> statement-breakpoint
23 | CREATE INDEX IF NOT EXISTS "embeddingIndex" ON "embeddings" USING hnsw ("embedding" vector_cosine_ops);
--------------------------------------------------------------------------------
/lib/db/migrations/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "ab270498-0532-4efe-aac3-bb80ffc07ce2",
3 | "prevId": "00000000-0000-0000-0000-000000000000",
4 | "version": "7",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.resources": {
8 | "name": "resources",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "varchar(191)",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "content": {
18 | "name": "content",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "created_at": {
24 | "name": "created_at",
25 | "type": "timestamp",
26 | "primaryKey": false,
27 | "notNull": true,
28 | "default": "now()"
29 | },
30 | "updated_at": {
31 | "name": "updated_at",
32 | "type": "timestamp",
33 | "primaryKey": false,
34 | "notNull": true,
35 | "default": "now()"
36 | }
37 | },
38 | "indexes": {},
39 | "foreignKeys": {},
40 | "compositePrimaryKeys": {},
41 | "uniqueConstraints": {}
42 | }
43 | },
44 | "enums": {},
45 | "schemas": {},
46 | "_meta": {
47 | "columns": {},
48 | "schemas": {},
49 | "tables": {}
50 | }
51 | }
--------------------------------------------------------------------------------
/lib/db/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {"version":"7","dialect":"postgresql","entries":[{"idx":0,"version":"7","when":1719997972944,"tag":"0000_yielding_bloodaxe","breakpoints":true}]}
--------------------------------------------------------------------------------
/lib/db/schema/embeddings.ts:
--------------------------------------------------------------------------------
1 | import { nanoid } from "@/lib/utils";
2 | import { index, pgTable, text, varchar, vector } from "drizzle-orm/pg-core";
3 | import { resources } from "./resources";
4 |
5 | export const embeddings = pgTable(
6 | "embeddings",
7 | {
8 | id: varchar("id", { length: 191 })
9 | .primaryKey()
10 | .$defaultFn(() => nanoid()),
11 | resourceId: varchar("resource_id", { length: 191 }).references(
12 | () => resources.id,
13 | { onDelete: "cascade" },
14 | ),
15 | content: text("content").notNull(),
16 | embedding: vector("embedding", { dimensions: 1536 }).notNull(),
17 | },
18 | (table) => ({
19 | embeddingIndex: index("embeddingIndex").using(
20 | "hnsw",
21 | table.embedding.op("vector_cosine_ops"),
22 | ),
23 | }),
24 | );
25 |
--------------------------------------------------------------------------------
/lib/db/schema/resources.ts:
--------------------------------------------------------------------------------
1 | import { sql } from "drizzle-orm";
2 | import { text, varchar, timestamp, pgTable } from "drizzle-orm/pg-core";
3 | import { createSelectSchema } from "drizzle-zod";
4 | import { z } from "zod";
5 |
6 | import { nanoid } from "@/lib/utils";
7 |
8 | export const resources = pgTable("resources", {
9 | id: varchar("id", { length: 191 })
10 | .primaryKey()
11 | .$defaultFn(() => nanoid()),
12 | content: text("content").notNull(),
13 |
14 | createdAt: timestamp("created_at")
15 | .notNull()
16 | .default(sql`now()`),
17 | updatedAt: timestamp("updated_at")
18 | .notNull()
19 | .default(sql`now()`),
20 | });
21 |
22 | // Schema for resources - used to validate API requests
23 | export const insertResourceSchema = createSelectSchema(resources)
24 | .extend({})
25 | .omit({
26 | id: true,
27 | createdAt: true,
28 | updatedAt: true,
29 | });
30 |
31 | // Type for resources - used to type API request params and within Components
32 | export type NewResourceParams = z.infer;
33 |
--------------------------------------------------------------------------------
/lib/env.mjs:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 | import "dotenv/config";
4 |
5 | export const env = createEnv({
6 | server: {
7 | NODE_ENV: z
8 | .enum(["development", "test", "production"])
9 | .default("development"),
10 | DATABASE_URL: z.string().min(1),
11 | },
12 | client: {
13 | // NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
14 | },
15 | // If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually
16 | runtimeEnv: {
17 | DATABASE_URL: process.env.DATABASE_URL,
18 | // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
19 | },
20 | // For Next.js >= 13.4.4, you only need to destructure client variables:
21 | experimental__runtimeEnv: {
22 | // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
23 | DATABASE_URL: process.env.DATABASE_URL,
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from "nanoid";
2 | import { clsx, type ClassValue } from "clsx";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs));
7 | }
8 |
9 | export const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789");
10 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-application",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "tsx lib/db/migrate.ts && next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "db:generate": "drizzle-kit generate",
11 | "db:migrate": "tsx lib/db/migrate.ts",
12 | "db:drop": "drizzle-kit drop",
13 | "db:pull": "drizzle-kit introspect",
14 | "db:push": "drizzle-kit push",
15 | "db:studio": "drizzle-kit studio",
16 | "db:check": "drizzle-kit check"
17 | },
18 | "dependencies": {
19 | "@ai-sdk/openai": "^1.0.8",
20 | "@radix-ui/react-avatar": "^1.1.1",
21 | "@radix-ui/react-dropdown-menu": "^2.1.2",
22 | "@radix-ui/react-label": "^2.1.0",
23 | "@radix-ui/react-slot": "^1.1.0",
24 | "@t3-oss/env-nextjs": "^0.11.1",
25 | "ai": "^4.0.16",
26 | "class-variance-authority": "^0.7.1",
27 | "clsx": "^2.1.1",
28 | "drizzle-orm": "^0.38.1",
29 | "drizzle-zod": "^0.6.0",
30 | "framer-motion": "^11.14.1",
31 | "geist": "^1.3.1",
32 | "lucide-react": "^0.468.0",
33 | "nanoid": "^5.0.9",
34 | "next": "15.1.0",
35 | "postgres": "^3.4.5",
36 | "react": "^19.0.0",
37 | "react-dom": "^19.0.0",
38 | "react-markdown": "^9.0.1",
39 | "sonner": "^1.7.1",
40 | "tailwind-merge": "^2.5.5",
41 | "tailwindcss-animate": "^1.0.7",
42 | "zod": "^3.24.1"
43 | },
44 | "devDependencies": {
45 | "@types/node": "^22.10.2",
46 | "@types/react": "^19.0.1",
47 | "@types/react-dom": "^19.0.2",
48 | "dotenv": "^16.4.7",
49 | "drizzle-kit": "^0.30.0",
50 | "eslint": "^9.16.0",
51 | "eslint-config-next": "15.1.0",
52 | "pg": "^8.13.1",
53 | "postcss": "^8.4.49",
54 | "tailwindcss": "^3.4.16",
55 | "tsx": "^4.19.2",
56 | "typescript": "^5.7.2"
57 | }
58 | }
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require("tailwindcss/defaultTheme")
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
6 | theme: {
7 | container: {
8 | center: true,
9 | padding: "2rem",
10 | screens: {
11 | "2xl": "1400px",
12 | },
13 | },
14 | extend: {
15 | colors: {
16 | border: "hsl(var(--border))",
17 | input: "hsl(var(--input))",
18 | ring: "hsl(var(--ring))",
19 | background: "hsl(var(--background))",
20 | foreground: "hsl(var(--foreground))",
21 | primary: {
22 | DEFAULT: "hsl(var(--primary))",
23 | foreground: "hsl(var(--primary-foreground))",
24 | },
25 | secondary: {
26 | DEFAULT: "hsl(var(--secondary))",
27 | foreground: "hsl(var(--secondary-foreground))",
28 | },
29 | destructive: {
30 | DEFAULT: "hsl(var(--destructive))",
31 | foreground: "hsl(var(--destructive-foreground))",
32 | },
33 | muted: {
34 | DEFAULT: "hsl(var(--muted))",
35 | foreground: "hsl(var(--muted-foreground))",
36 | },
37 | accent: {
38 | DEFAULT: "hsl(var(--accent))",
39 | foreground: "hsl(var(--accent-foreground))",
40 | },
41 | popover: {
42 | DEFAULT: "hsl(var(--popover))",
43 | foreground: "hsl(var(--popover-foreground))",
44 | },
45 | card: {
46 | DEFAULT: "hsl(var(--card))",
47 | foreground: "hsl(var(--card-foreground))",
48 | },
49 | },
50 | borderRadius: {
51 | lg: `var(--radius)`,
52 | md: `calc(var(--radius) - 2px)`,
53 | sm: "calc(var(--radius) - 4px)",
54 | },
55 | fontFamily: {
56 | sans: ["var(--font-sans)", ...fontFamily.sans],
57 | },
58 | keyframes: {
59 | "accordion-down": {
60 | from: { height: 0 },
61 | to: { height: "var(--radix-accordion-content-height)" },
62 | },
63 | "accordion-up": {
64 | from: { height: "var(--radix-accordion-content-height)" },
65 | to: { height: 0 },
66 | },
67 | },
68 | animation: {
69 | "accordion-down": "accordion-down 0.2s ease-out",
70 | "accordion-up": "accordion-up 0.2s ease-out",
71 | },
72 | },
73 | },
74 | plugins: [require("tailwindcss-animate")],
75 | }
76 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./*"
27 | ]
28 | },
29 | "target": "esnext",
30 | "baseUrl": "./"
31 | },
32 | "include": [
33 | "next-env.d.ts",
34 | "**/*.ts",
35 | "**/*.tsx",
36 | ".next/types/**/*.ts"
37 | ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
--------------------------------------------------------------------------------