aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDhravya <[email protected]>2024-06-23 18:47:28 -0500
committerDhravya <[email protected]>2024-06-23 18:47:28 -0500
commitfff8b7abd79b58cddeffbac835ffb706cd529480 (patch)
tree60625fe65f27836b389a5135c2d7a7e27c7d225b
parentMerge branch 'codetorso' of https://github.com/Dhravya/supermemory into codet... (diff)
downloadsupermemory-fff8b7abd79b58cddeffbac835ffb706cd529480.tar.xz
supermemory-fff8b7abd79b58cddeffbac835ffb706cd529480.zip
added backend route for telegram bot and others to be possible
-rw-r--r--apps/cf-ai-backend/src/helper.ts16
-rw-r--r--apps/cf-ai-backend/src/index.ts155
2 files changed, 169 insertions, 2 deletions
diff --git a/apps/cf-ai-backend/src/helper.ts b/apps/cf-ai-backend/src/helper.ts
index cef781be..44dba383 100644
--- a/apps/cf-ai-backend/src/helper.ts
+++ b/apps/cf-ai-backend/src/helper.ts
@@ -106,6 +106,20 @@ export async function deleteDocument({
}
}
+function sanitizeKey(key: string): string {
+ if (!key) throw new Error("Key cannot be empty");
+
+ // Remove or replace invalid characters
+ let sanitizedKey = key.replace(/[.$"]/g, "_");
+
+ // Ensure key does not start with $
+ if (sanitizedKey.startsWith("$")) {
+ sanitizedKey = sanitizedKey.substring(1);
+ }
+
+ return sanitizedKey;
+}
+
export async function batchCreateChunksAndEmbeddings({
store,
body,
@@ -172,7 +186,7 @@ export async function batchCreateChunksAndEmbeddings({
type: body.type ?? "page",
content: newPageContent,
- [`user-${body.user}`]: 1,
+ [sanitizeKey(`user-${body.user}`)]: 1,
...body.spaces?.reduce((acc, space) => {
acc[`space-${body.user}-${space}`] = 1;
return acc;
diff --git a/apps/cf-ai-backend/src/index.ts b/apps/cf-ai-backend/src/index.ts
index 224f2a42..f2c69246 100644
--- a/apps/cf-ai-backend/src/index.ts
+++ b/apps/cf-ai-backend/src/index.ts
@@ -1,6 +1,6 @@
import { z } from "zod";
import { Hono } from "hono";
-import { CoreMessage, streamText } from "ai";
+import { CoreMessage, generateText, streamText, tool } from "ai";
import { chatObj, Env, vectorObj } from "./types";
import {
batchCreateChunksAndEmbeddings,
@@ -169,6 +169,159 @@ app.get(
},
);
+// This is a special endpoint for our "chatbot-only" solutions.
+// It does both - adding content AND chatting with it.
+app.post(
+ "/api/autoChatOrAdd",
+ zValidator(
+ "query",
+ z.object({
+ query: z.string(),
+ user: z.string(),
+ }),
+ ),
+ zValidator("json", chatObj),
+ async (c) => {
+ const { query, user } = c.req.valid("query");
+ const { chatHistory } = c.req.valid("json");
+
+ const { store, model } = await initQuery(c);
+
+ let task: "add" | "chat" = "chat";
+ let thingToAdd: "page" | "image" | "text" | undefined = undefined;
+ let addContent: string | undefined = undefined;
+
+ // This is a "router". this finds out if the user wants to add a document, or chat with the AI to get a response.
+ const routerQuery = await generateText({
+ model,
+ system: `You are Supermemory chatbot. You can either add a document to the supermemory database, or return a chat response. Based on this query,
+ You must determine what to do. Basically if it feels like a "question", then you should intiate a chat. If it feels like a "command" or feels like something that could be forwarded to the AI, then you should add a document.
+ You must also extract the "thing" to add and what type of thing it is.`,
+ prompt: `Question from user: ${query}`,
+ tools: {
+ decideTask: tool({
+ description:
+ "Decide if the user wants to add a document or chat with the AI",
+ parameters: z.object({
+ generatedTask: z.enum(["add", "chat"]),
+ contentToAdd: z.object({
+ thing: z.enum(["page", "image", "text"]),
+ content: z.string(),
+ }),
+ }),
+ execute: async ({ generatedTask, contentToAdd }) => {
+ task = generatedTask;
+ thingToAdd = contentToAdd.thing;
+ addContent = contentToAdd.content;
+ },
+ }),
+ },
+ });
+
+ if ((task as string) === "add") {
+ // addString is the plaintext string that the user wants to add to the database
+ let addString: string = addContent;
+
+ if (thingToAdd === "page") {
+ // TODO: Sometimes this query hangs, and errors out. we need to do proper error management here.
+ const response = await fetch("https://md.dhr.wtf/?url=" + addContent, {
+ headers: {
+ Authorization: "Bearer " + c.env.SECURITY_KEY,
+ },
+ });
+
+ addString = await response.text();
+ }
+
+ // At this point, we can just go ahead and create the embeddings!
+ await batchCreateChunksAndEmbeddings({
+ store,
+ body: {
+ url: addContent,
+ user,
+ type: thingToAdd,
+ pageContent: addString,
+ title: `${addString.slice(0, 30)}... (Added from chatbot)`,
+ },
+ chunks: chunkText(addString, 1536),
+ context: c,
+ });
+
+ return c.json({
+ status: "ok",
+ response:
+ "I added the document to your personal second brain! You can now use it to answer questions or chat with me.",
+ contentAdded: {
+ type: thingToAdd,
+ content: addString,
+ url:
+ thingToAdd === "page"
+ ? addContent
+ : `https://supermemory.ai/note/${Date.now()}`,
+ },
+ });
+ } else {
+ const filter: VectorizeVectorMetadataFilter = {
+ [`user-${user}`]: 1,
+ };
+
+ const queryAsVector = await store.embeddings.embedQuery(query);
+
+ const resp = await c.env.VECTORIZE_INDEX.query(queryAsVector, {
+ topK: 5,
+ filter,
+ returnMetadata: true,
+ });
+
+ const minScore = Math.min(...resp.matches.map(({ score }) => score));
+ const maxScore = Math.max(...resp.matches.map(({ score }) => score));
+
+ // This entire chat part is basically just a dumb down version of the /api/chat endpoint.
+ const normalizedData = resp.matches.map((data) => ({
+ ...data,
+ normalizedScore:
+ maxScore !== minScore
+ ? 1 + ((data.score - minScore) / (maxScore - minScore)) * 98
+ : 50,
+ }));
+
+ const preparedContext = normalizedData.map(
+ ({ metadata, score, normalizedScore }) => ({
+ context: `Website title: ${metadata!.title}\nDescription: ${metadata!.description}\nURL: ${metadata!.url}\nContent: ${metadata!.text}`,
+ score,
+ normalizedScore,
+ }),
+ );
+
+ const prompt = template({
+ contexts: preparedContext,
+ question: query,
+ });
+
+ const initialMessages: CoreMessage[] = [
+ {
+ role: "system",
+ content: `You are an AI chatbot called "Supermemory.ai". When asked a question by a user, you must take all the context provided to you and give a good, small, but helpful response.`,
+ },
+ { role: "assistant", content: "Hello, how can I help?" },
+ ];
+
+ const userMessage: CoreMessage = { role: "user", content: prompt };
+
+ const response = await generateText({
+ model,
+ messages: [
+ ...initialMessages,
+ ...((chatHistory || []) as CoreMessage[]),
+ userMessage,
+ ],
+ });
+
+ return c.json({ status: "ok", response: response.text });
+ }
+ },
+);
+
/* 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?