commented the backend code with it's limitations + optimised and removed a bunch of unnecessary queries

This commit is contained in:
Dhravya 2024-06-01 21:21:52 -05:00
parent 7dddaf56e3
commit f105a2fd92
11 changed files with 152 additions and 145 deletions

View file

@ -23,8 +23,11 @@ app.use("*", timing());
app.use("*", logger());
app.use("/api/", async (c, next) => {
const auth = bearerAuth({ token: c.env.SECURITY_KEY });
return auth(c, next);
if (c.env.NODE_ENV !== "development") {
const auth = bearerAuth({ token: c.env.SECURITY_KEY });
return auth(c, next);
}
return next();
});
// ------- MIDDLEWARES END -------
@ -71,6 +74,11 @@ app.get(
},
);
/* TODO: Eventually, we should not have to save each user's content in a seperate vector.
Lowkey, it makes sense. The user may save their own version of a page - like selected text from twitter.com url.
But, it's not scalable *enough*. How can we store the same vectors for the same content, without needing to duplicate for each uer?
Hard problem to solve, Vectorize doesn't have an OR filter, so we can't just filter by URL and user.
*/
app.post(
"/api/chat",
zValidator(
@ -99,23 +107,30 @@ app.post(
const sourcesOnly = query.sourcesOnly === "true";
const spaces = query.spaces?.split(",") || [undefined];
// Get the AI model maker and vector store
const { model, store } = await initQuery(c, query.model);
const filter: VectorizeVectorMetadataFilter = { user: query.user };
// Converting the query to a vector so that we can search for similar vectors
const queryAsVector = await store.embeddings.embedQuery(query.query);
const responses: VectorizeMatches = { matches: [], count: 0 };
for (const space of spaces) {
// SLICED to 5 to avoid too many queries
for (const space of spaces.slice(0, 5)) {
if (space !== undefined) {
// it's possible for space list to be [undefined] so we only add space filter conditionally
filter.space = space;
}
// Because there's no OR operator in the filter, we have to make multiple queries
const resp = await c.env.VECTORIZE_INDEX.query(queryAsVector, {
topK: query.topK,
filter,
returnMetadata: true,
});
// Basically recreating the response object
if (resp.count > 0) {
responses.matches.push(...resp.matches);
responses.count += resp.count;
@ -125,6 +140,8 @@ app.post(
const minScore = Math.min(...responses.matches.map(({ score }) => score));
const maxScore = Math.max(...responses.matches.map(({ score }) => score));
// We are "normalising" the scores - if all of them are on top, we want to make sure that
// we have a way to filter out the noise.
const normalizedData = responses.matches.map((data) => ({
...data,
normalizedScore:
@ -136,6 +153,9 @@ app.post(
let highScoreData = normalizedData.filter(
({ normalizedScore }) => normalizedScore > 50,
);
// If the normalsation is not done properly, we have a fallback to just get the
// top 3 scores
if (highScoreData.length === 0) {
highScoreData = normalizedData
.sort((a, b) => b.score - a.score)
@ -146,13 +166,16 @@ app.post(
(a, b) => b.normalizedScore - a.normalizedScore,
);
console.log(JSON.stringify(sortedHighScoreData));
// So this is kinda hacky, but the frontend needs to do 2 calls to get sources and chat.
// I think this is fine for now, but we can improve this later.
if (sourcesOnly) {
const idsAsStrings = sortedHighScoreData.map((dataPoint) =>
dataPoint.id.toString(),
);
// We are getting the content ID back, so that the frontend can show the actual sources properly.
// it IS a lot of DB calls, i completely agree.
// TODO: return metadata value here, so that the frontend doesn't have to re-fetch anything.
const storedContent = await Promise.all(
idsAsStrings.map(async (id) => await c.env.KV.get(id)),
);
@ -160,19 +183,21 @@ app.post(
return c.json({ ids: storedContent });
}
const vec = await c.env.VECTORIZE_INDEX.getByIds(
sortedHighScoreData.map(({ id }) => id),
);
const vec = responses.matches.map((data) => ({ metadata: data.metadata }));
const vecWithScores = vec.map((v, i) => ({
...v,
score: sortedHighScoreData[i].score,
normalisedScore: sortedHighScoreData[i].normalizedScore,
}));
const preparedContext = vecWithScores.map(({ metadata, score }) => ({
context: `Website title: ${metadata!.title}\nDescription: ${metadata!.description}\nURL: ${metadata!.url}\nContent: ${metadata!.text}`,
score,
}));
const preparedContext = vecWithScores.map(
({ metadata, score, normalisedScore }) => ({
context: `Website title: ${metadata!.title}\nDescription: ${metadata!.description}\nURL: ${metadata!.url}\nContent: ${metadata!.text}`,
score,
normalisedScore,
}),
);
const initialMessages: CoreMessage[] = [
{ role: "user", content: systemPrompt },
@ -193,7 +218,7 @@ app.post(
...((body.chatHistory || []) as CoreMessage[]),
userMessage,
],
temperature: 0.4,
// temperature: 0.4,
});
return response.toTextStreamResponse();

View file

@ -11,6 +11,7 @@ To generate your answer:
- Provide a direct answer to the question
- Use markdown formatting in your answer, including bold, italics, and bullet points as appropriate to improve readability and highlight key points
- Give detailed and accurate responses for things like 'write a blog' or long-form questions.
- The normalisedScore is a value in which the scores are 'balanced' to give a better representation of the relevance of the context, between 1 and 100, out of the top 10 results
Provide your justification between <justification> tags and your final answer between <answer> tags, formatting both in markdown.
@ -20,13 +21,14 @@ export const template = ({ contexts, question }) => {
// Map over contexts to generate the context and score parts
const contextParts = contexts
.map(
({ context, score }) => `
({ context, score, normalisedScore }) => `
<context>
${context}
</context>
<context_score>
${score}
score: ${score}
normalisedScore: ${normalisedScore}
</context_score>`,
)
.join("\n");

View file

@ -10,6 +10,7 @@ export type Env = {
KV: KVNamespace;
MYBROWSER: unknown;
ANTHROPIC_API_KEY: string;
NODE_ENV: string;
};
export interface TweetData {

View file

@ -1,7 +1,24 @@
import React from "react";
import { chatSearchParamsCache } from "../helpers/lib/searchParams";
import Menu from "../home/menu";
import Header from "../home/header";
import ChatWindow from "./chatWindow";
function Page() {
return <div>Page</div>;
function Page({
searchParams,
}: {
searchParams: Record<string, string | string[] | undefined>;
}) {
const { firstTime, q, spaces } = chatSearchParamsCache.parse(searchParams);
return (
<main className="h-screen flex flex-col p-4 relative">
<Menu />
<Header />
<ChatWindow q={q} spaces={spaces ?? []} />
</main>
);
}
export default Page;

View file

@ -1,48 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* :root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
} */
@media (prefers-color-scheme: dark) {
:root {
--foreground: rgba(179, 188, 197, 1);
--foreground-menu: rgba(106, 115, 125, 1);
--background: rgba(23, 27, 31, 1);
--secondary: rgba(31, 36, 40, 1);
--primary: rgba(54, 157, 253, 1);
--border: rgba(51, 57, 67, 1);
}
}
body {
color: var(--foreground);
background: var(--background);
font-size: 14px;
}
@layer base {
.all-center {
display: flex;
align-items: center;
justify-content: center;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
.gradient-background {
background: linear-gradient(
150deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0)
);
}

View file

@ -3,8 +3,15 @@ import {
parseAsInteger,
parseAsString,
parseAsBoolean,
parseAsArrayOf,
} from "nuqs/server";
export const homeSearchParamsCache = createSearchParamsCache({
firstTime: parseAsBoolean.withDefault(false),
});
export const chatSearchParamsCache = createSearchParamsCache({
firstTime: parseAsBoolean.withDefault(false),
q: parseAsString.withDefault(""),
spaces: parseAsArrayOf(parseAsInteger, ","),
});

View file

@ -1,7 +1 @@
"use server";
import { redirect } from "next/navigation";
export async function navigate(q: string) {
redirect(`/chat?q=${q}`);
}

View file

@ -9,6 +9,7 @@ function Page({
}: {
searchParams: Record<string, string | string[] | undefined>;
}) {
// TODO: use this to show a welcome page/modal
const { firstTime } = homeSearchParamsCache.parse(searchParams);
return (

View file

@ -1,88 +1,96 @@
"use client";
import { ArrowRightIcon, MemoriesIcon, SelectIcon } from "@repo/ui/icons";
import { ArrowRightIcon } from "@repo/ui/icons";
import Image from "next/image";
import React from "react";
import React, { useCallback, useState } from "react";
import Divider from "@repo/ui/shadcn/divider";
import { redirect } from "next/navigation";
import { navigate } from "./actions";
import { FilterSpaces } from "@repo/ui/components/filterSpaces";
import { MultipleSelector, Option } from "@repo/ui/shadcn/combobox";
import { AnimatePresence } from "framer-motion";
import { useRouter } from "next/navigation";
function QueryInput() {
const [q, setQ] = React.useState("");
const OPTIONS: Option[] = [
{ label: "nextjs", value: "0" },
{ label: "React", value: "1" },
{ label: "Remix", value: "2" },
{ label: "Vite", value: "3" },
{ label: "Nuxt", value: "4" },
{ label: "Vue", value: "5" },
{ label: "Svelte", value: "6" },
{ label: "Angular", value: "7" },
{ label: "Ember", value: "8" },
{ label: "Gatsby", value: "9" },
];
function QueryInput({
initialQuery = "",
initialSpaces = [],
disabled = false,
}: {
initialQuery?: string;
initialSpaces?: number[];
disabled?: boolean;
}) {
const [q, setQ] = useState(initialQuery);
const [selectedSpaces, setSelectedSpaces] = useState<number[]>(initialSpaces);
const { push } = useRouter();
const parseQ = () => {
const newQ =
"/chat?q=" +
encodeURI(q) +
(selectedSpaces ? "&spaces=" + selectedSpaces.join(",") : "");
const parseQ = React.useCallback(() => {
const newQ = q.replace(/\n/g, "\\n");
return newQ;
}, [q]);
const [selectedSpaces, setSelectedSpaces] = React.useState<number[]>([]);
};
return (
<div className="bg-secondary rounded-[24px] w-full mt-40">
{/* input and action button */}
<form action={async () => navigate(parseQ())} className="flex gap-4 p-3">
<textarea
name="q"
cols={30}
rows={4}
className="bg-transparent pt-2.5 text-base text-[#989EA4] focus:text-foreground duration-200 tracking-[3%] outline-none resize-none w-full p-4"
placeholder="Ask your second brain..."
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
if (!e.shiftKey) navigate(parseQ());
}
}}
onChange={(e) => setQ(e.target.value)}
value={q}
/>
<div>
<div className="bg-secondary rounded-t-[24px] w-full mt-40">
{/* input and action button */}
<form action={async () => push(parseQ())} className="flex gap-4 p-3">
<textarea
name="q"
cols={30}
rows={4}
className="bg-transparent pt-2.5 text-base text-[#989EA4] focus:text-foreground duration-200 tracking-[3%] outline-none resize-none w-full p-4"
placeholder="Ask your second brain..."
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
if (!e.shiftKey) push(parseQ());
}
}}
onChange={(e) => setQ(e.target.value)}
value={q}
disabled={disabled}
/>
<button
type="submit"
className="h-12 w-12 rounded-[14px] bg-[#21303D] all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90"
>
<Image src={ArrowRightIcon} alt="Right arrow icon" />
</button>
</form>
<Divider />
<button
type="submit"
disabled={disabled}
className="h-12 w-12 rounded-[14px] bg-[#21303D] all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90"
>
<Image src={ArrowRightIcon} alt="Right arrow icon" />
</button>
</form>
<Divider />
</div>
{/* selected sources */}
<div className="flex items-center gap-6 p-2">
{/* <button className="bg-[#2B3237] h-9 p-2 px-3 flex items-center gap-2 rounded-full">
<Image src={MemoriesIcon} alt="Memories icon" className="w-5" />
<span className="pr-3">Filters</span>
<Image src={SelectIcon} alt="Select icon" className="w-4" />
</button> */}
<FilterSpaces
name="Filters"
selectedSpaces={selectedSpaces}
setSelectedSpaces={setSelectedSpaces}
// side="top"
// align="start"
// className="mr-auto bg-[#252525] md:hidden"
spaces={[
{
name: "Nvidia",
id: 2,
},
{
name: "Open-source",
id: 3,
},
{
name: "Artificial Intelligence",
id: 4,
},
]}
<div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-[24px]">
<MultipleSelector
disabled={disabled}
defaultOptions={OPTIONS}
onChange={(e) => setSelectedSpaces(e.map((x) => parseInt(x.value)))}
placeholder="Focus on specific spaces..."
emptyIndicator={
<p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400">
no results found.
</p>
}
/>
<div className="flex gap-6 brightness-75">
<p>Nvidia</p>
<p>Open-source</p>
<p>Artificial Intelligence</p>
</div>
</div>
</div>
);

View file

@ -1,4 +1,3 @@
// import "./globals.css";
import "@repo/tailwind-config/globals.css";
import type { Metadata } from "next";

View file

@ -62,6 +62,7 @@
"compromise": "^14.13.0",
"drizzle-orm": "^0.30.10",
"framer-motion": "^11.2.6",
"lucide-react": "^0.379.0",
"next-app-theme": "^0.1.10",
"next-auth": "^5.0.0-beta.18",
"random-js": "^2.1.0",