aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorDhravya <[email protected]>2024-06-01 21:21:52 -0500
committerDhravya <[email protected]>2024-06-01 21:21:52 -0500
commitf105a2fd92c25a75668c3615cd8328fa875050d8 (patch)
treedb5c8cd657623016029143b4123878ac6b025f2a /apps
parentstarted implmeneting filter (diff)
downloadsupermemory-f105a2fd92c25a75668c3615cd8328fa875050d8.tar.xz
supermemory-f105a2fd92c25a75668c3615cd8328fa875050d8.zip
commented the backend code with it's limitations + optimised and removed a bunch of unnecessary queries
Diffstat (limited to 'apps')
-rw-r--r--apps/cf-ai-backend/src/index.ts51
-rw-r--r--apps/cf-ai-backend/src/prompts/prompt1.ts6
-rw-r--r--apps/cf-ai-backend/src/types.ts1
-rw-r--r--apps/web/app/chat/page.tsx23
-rw-r--r--apps/web/app/globals.css48
-rw-r--r--apps/web/app/helpers/lib/searchParams.ts7
-rw-r--r--apps/web/app/home/actions.ts6
-rw-r--r--apps/web/app/home/page.tsx1
-rw-r--r--apps/web/app/home/queryinput.tsx152
-rw-r--r--apps/web/app/layout.tsx1
10 files changed, 151 insertions, 145 deletions
diff --git a/apps/cf-ai-backend/src/index.ts b/apps/cf-ai-backend/src/index.ts
index 3e65ac90..19770dec 100644
--- a/apps/cf-ai-backend/src/index.ts
+++ b/apps/cf-ai-backend/src/index.ts
@@ -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();
diff --git a/apps/cf-ai-backend/src/prompts/prompt1.ts b/apps/cf-ai-backend/src/prompts/prompt1.ts
index 6e1bdf7b..aa7694d3 100644
--- a/apps/cf-ai-backend/src/prompts/prompt1.ts
+++ b/apps/cf-ai-backend/src/prompts/prompt1.ts
@@ -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");
diff --git a/apps/cf-ai-backend/src/types.ts b/apps/cf-ai-backend/src/types.ts
index 3b6db589..bea4bf80 100644
--- a/apps/cf-ai-backend/src/types.ts
+++ b/apps/cf-ai-backend/src/types.ts
@@ -10,6 +10,7 @@ export type Env = {
KV: KVNamespace;
MYBROWSER: unknown;
ANTHROPIC_API_KEY: string;
+ NODE_ENV: string;
};
export interface TweetData {
diff --git a/apps/web/app/chat/page.tsx b/apps/web/app/chat/page.tsx
index fce4bfd1..bfe0c362 100644
--- a/apps/web/app/chat/page.tsx
+++ b/apps/web/app/chat/page.tsx
@@ -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;
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
deleted file mode 100644
index b1902464..00000000
--- a/apps/web/app/globals.css
+++ /dev/null
@@ -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)
- );
-}
diff --git a/apps/web/app/helpers/lib/searchParams.ts b/apps/web/app/helpers/lib/searchParams.ts
index a43a28fe..2e02aa3e 100644
--- a/apps/web/app/helpers/lib/searchParams.ts
+++ b/apps/web/app/helpers/lib/searchParams.ts
@@ -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, ","),
+});
diff --git a/apps/web/app/home/actions.ts b/apps/web/app/home/actions.ts
index 0bb2d051..908fe79e 100644
--- a/apps/web/app/home/actions.ts
+++ b/apps/web/app/home/actions.ts
@@ -1,7 +1 @@
"use server";
-
-import { redirect } from "next/navigation";
-
-export async function navigate(q: string) {
- redirect(`/chat?q=${q}`);
-}
diff --git a/apps/web/app/home/page.tsx b/apps/web/app/home/page.tsx
index d9025a9d..9908c017 100644
--- a/apps/web/app/home/page.tsx
+++ b/apps/web/app/home/page.tsx
@@ -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 (
diff --git a/apps/web/app/home/queryinput.tsx b/apps/web/app/home/queryinput.tsx
index c394d9c6..a0e25d83 100644
--- a/apps/web/app/home/queryinput.tsx
+++ b/apps/web/app/home/queryinput.tsx
@@ -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" },
+];
- const parseQ = React.useCallback(() => {
- const newQ = q.replace(/\n/g, "\\n");
- return newQ;
- }, [q]);
+function QueryInput({
+ initialQuery = "",
+ initialSpaces = [],
+ disabled = false,
+}: {
+ initialQuery?: string;
+ initialSpaces?: number[];
+ disabled?: boolean;
+}) {
+ const [q, setQ] = useState(initialQuery);
- const [selectedSpaces, setSelectedSpaces] = React.useState<number[]>([]);
+ const [selectedSpaces, setSelectedSpaces] = useState<number[]>(initialSpaces);
- 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}
- />
+ const { push } = useRouter();
- <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>
+ const parseQ = () => {
+ const newQ =
+ "/chat?q=" +
+ encodeURI(q) +
+ (selectedSpaces ? "&spaces=" + selectedSpaces.join(",") : "");
- <Divider />
+ return newQ;
+ };
+ return (
+ <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"
+ 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>
);
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 37541f04..8d7cd5ea 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,4 +1,3 @@
-// import "./globals.css";
import "@repo/tailwind-config/globals.css";
import type { Metadata } from "next";