mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-22 06:53:57 +00:00
commented the backend code with it's limitations + optimised and removed a bunch of unnecessary queries
This commit is contained in:
parent
7dddaf56e3
commit
f105a2fd92
11 changed files with 152 additions and 145 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export type Env = {
|
|||
KV: KVNamespace;
|
||||
MYBROWSER: unknown;
|
||||
ANTHROPIC_API_KEY: string;
|
||||
NODE_ENV: string;
|
||||
};
|
||||
|
||||
export interface TweetData {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
@ -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, ","),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1 @@
|
|||
"use server";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export async function navigate(q: string) {
|
||||
redirect(`/chat?q=${q}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// import "./globals.css";
|
||||
import "@repo/tailwind-config/globals.css";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue