diff options
| author | Dhravya <[email protected]> | 2024-06-23 18:47:28 -0500 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-06-23 18:47:28 -0500 |
| commit | fff8b7abd79b58cddeffbac835ffb706cd529480 (patch) | |
| tree | 60625fe65f27836b389a5135c2d7a7e27c7d225b | |
| parent | Merge branch 'codetorso' of https://github.com/Dhravya/supermemory into codet... (diff) | |
| download | supermemory-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.ts | 16 | ||||
| -rw-r--r-- | apps/cf-ai-backend/src/index.ts | 155 |
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? |