mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
added backend route for telegram bot and others to be possible
This commit is contained in:
parent
0d069e8741
commit
fff8b7abd7
2 changed files with 169 additions and 2 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue