diff --git a/bots/chatgpt.ts b/bots/chatgpt.ts index aa300ed..473e76c 100644 --- a/bots/chatgpt.ts +++ b/bots/chatgpt.ts @@ -4,6 +4,8 @@ import * as types from "./types.ts"; import * as vdb from "../vdb.ts"; +import { safeEval } from "../lib/eval.ts"; + if (!Deno.env.get("OPENAI_API_KEY")) { console.warn("No OpenAI API key provided! ChatGPT will be unavailable."); isEnabled = false; @@ -33,12 +35,30 @@ const tools: types.Tool[] = [{ required: ["query"], }, }, +}, { + type: "function", + function: { + name: "eval", + description: + "Evaluates JS code within a heavily limited sandbox (a worker with no access other then network). Times out after 10 seconds.", + parameters: { + type: "object", + properties: { + code: { + type: "string", + description: "Code to be evaluated", + }, + }, + required: ["code"], + }, + }, }]; async function doTools( oaires: types.Response, messages: types.Message[], -): Promise { + callback: Function, +): Promise { if (oaires.choices[0].finish_reason !== "tool_calls") { throw "What The Shit?"; } @@ -56,6 +76,13 @@ async function doTools( content: databaseResponse, tool_call_id: tool.id, }; + } else if (tool.function.name === "eval") { + const respons = await safeEval(JSON.parse(tool.function.arguments).code); + return { + role: "tool", + content: respons, + tool_call_id: tool.id, + }; } else { return { role: "tool", @@ -72,9 +99,7 @@ async function doTools( messages.push(result); }); - const newres = await send(messages, null, "tool_res"); - - console.log(newres); + const newres = await send(messages, null, "tool_res", callback); return newres; } @@ -83,7 +108,8 @@ export async function send( messages: types.Message[], prompt: string | null, userid: string, -): Promise { + callback: Function, +): Promise { // here we go if (!isEnabled) { @@ -93,7 +119,7 @@ export async function send( if (messages.length === 0) { messages.push({ role: "system", - content: "You are ChatGPT, an LLM by OpenAI.", + content: "You are ChatGPT, an LLM by OpenAI. You are running through a Discord bot named LLM Bot, by Eris.", }); } @@ -104,8 +130,6 @@ export async function send( }); } - console.log(messages); - const res = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { @@ -127,15 +151,18 @@ export async function send( throw resp.error.message; // well at least they know why the fuck it crashed?? } - let finalresp: response = { - oaires: resp, - messages, - }; + let finalresp = resp messages.push(resp.choices[0].message); if (resp.choices[0].finish_reason === "tool_calls") { - finalresp = await doTools(resp, messages); + callback("function", resp); + + finalresp = await doTools(resp, messages, callback); + } else { + + callback("complete", finalresp) + } return finalresp; diff --git a/bots/gemini.ts b/bots/gemini.ts index 962bb65..f433089 100644 --- a/bots/gemini.ts +++ b/bots/gemini.ts @@ -16,7 +16,9 @@ type response = { // const db = await Deno.openKv("./db.sqlite") -async function processGeminiMessages(messages: types.Message[]): Promise<(types.GeminiContentPartImage | types.GeminiContentPartText)[]> { +async function processGeminiMessages( + messages: types.Message[], +): Promise<(types.GeminiContentPartImage | types.GeminiContentPartText)[]> { const geminiFormattedMessages = []; for (const message of messages) { @@ -47,7 +49,7 @@ async function getImageData(url: string) { try { const response = await fetch(url); - const contentType = response.headers.get('Content-Type'); + const contentType = response.headers.get("Content-Type"); const blob = await response.blob(); @@ -63,20 +65,20 @@ async function getImageData(url: string) { // Step 6: Get the base64-encoded image data - const resultString = reader.result as string + const resultString = reader.result as string; - const base64ImageData = resultString.split(',')[1]; + const base64ImageData = resultString.split(",")[1]; return { contentType, base64ImageData }; } catch (error) { - console.error('Error:', error); + console.error("Error:", error); } } export async function send( messages: types.Message[], prompt: string | null, - images: string[] + images: string[], ): Promise { // here we go @@ -102,26 +104,26 @@ export async function send( images.forEach((image) => { messages.push({ role: "image", - content: image - }) - }) - + content: image, + }); + }); let useImageModel = false; - console.log(useImageModel) + console.log(useImageModel); -// Check if any object has the specified property set to the target value -for (let i = 0; i < messages.length; i++) { - if (messages[i].role === "image") { - useImageModel = true; - break; // Stop the loop since we found a match + // Check if any object has the specified property set to the target value + for (let i = 0; i < messages.length; i++) { + if (messages[i].role === "image") { + useImageModel = true; + break; // Stop the loop since we found a match + } } -} - let geminiFormattedMessages: (types.GeminiContentPartText | types.GeminiContentPartImage)[] = []; + let geminiFormattedMessages: + (types.GeminiContentPartText | types.GeminiContentPartImage)[] = []; - geminiFormattedMessages = await processGeminiMessages(messages) + geminiFormattedMessages = await processGeminiMessages(messages); // Gemini message system is a motherfucker and I hate it but we gotta deal with it. Messages look like this: @@ -135,9 +137,9 @@ for (let i = 0; i < messages.length; i++) { */ const res = await fetch( - `https://generativelanguage.googleapis.com/v1beta/models/${useImageModel === true ? 'gemini-pro-vision' : 'gemini-pro'}:generateContent?key=${ - Deno.env.get("GEMINI_API_KEY") - }`, + `https://generativelanguage.googleapis.com/v1beta/models/${ + useImageModel === true ? "gemini-pro-vision" : "gemini-pro" + }:generateContent?key=${Deno.env.get("GEMINI_API_KEY")}`, { method: "POST", headers: { diff --git a/bots/types.ts b/bots/types.ts index 88728e3..6e66706 100644 --- a/bots/types.ts +++ b/bots/types.ts @@ -141,13 +141,13 @@ export type GeminiContentPartText = { export type GeminiContentPartImage = { inlineData: { - mimeType: string; - data: string; - } -} + mimeType: string; + data: string; + }; +}; type GeminiContent = { - parts: GeminiContentPartText[] + parts: GeminiContentPartText[]; role: string; }; diff --git a/lib/eval.ts b/lib/eval.ts new file mode 100644 index 0000000..3383ff0 --- /dev/null +++ b/lib/eval.ts @@ -0,0 +1,46 @@ +export function safeEval(code: string): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker( + import.meta.resolve("./eval_worker.js"), + { + type: "module", + name, + deno: { + //@ts-ignore ignore the namespace annotation. Deno < 1.22 required this + namespace: false, + permissions: { + env: false, + hrtime: false, + net: true, + ffi: false, + read: false, + run: false, + write: false, + }, + }, + }, + ); + + let timeoutId: number; + + worker.onmessage = (msg) => { + console.log(msg.data); + clearTimeout(timeoutId); + if (typeof msg.data !== "string") { + worker.terminate(); + reject("Worker returned a corrupt message!"); + } else { + worker.terminate(); + resolve(msg.data); + } + }; + + worker.postMessage(code); + + timeoutId = setTimeout(() => { + console.log("early termination"); + worker.terminate(); // What's taking YOU so long, hmm? + reject("Worker did not respond in time!"); + }, 10000); + }); +} diff --git a/lib/eval_worker.js b/lib/eval_worker.js new file mode 100644 index 0000000..e120932 --- /dev/null +++ b/lib/eval_worker.js @@ -0,0 +1,14 @@ +// deno-lint-ignore no-global-assign +console = null + +self.onmessage = async (e) => { + try { + + const response = `${eval(e.data)}` + + postMessage(response) + + } catch (err) { + postMessage(`Error occured during code processing: ${err}`) + } +} \ No newline at end of file diff --git a/main.ts b/main.ts index f58d7cb..43dadc2 100644 --- a/main.ts +++ b/main.ts @@ -19,14 +19,14 @@ type gptresponse = { type geminiresponse = { res: types.geminiResponse; - messages: types.Message[] -} + messages: types.Message[]; +}; import "./slashcode.ts"; import client from "./client.ts"; -import { ChannelType, Message } from "npm:discord.js"; +import { AnyThreadChannel, ChannelType, Message } from "npm:discord.js"; const db = await Deno.openKv("./db.sqlite"); @@ -93,13 +93,15 @@ const getImagesFromMessage = async (message: Message) => { }); // Process URLs in message content - const regx = message.content.match(/\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig); + const regx = message.content.match( + /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig, + ); if (regx) { // Use Promise.all to wait for all asynchronous operations to complete const resultArray = await Promise.all(regx.map(async (link) => { const aeiou = await fetch(link); - const isImage = aeiou.headers.get('Content-Type')?.startsWith("image/"); + const isImage = aeiou.headers.get("Content-Type")?.startsWith("image/"); if (isImage) { console.log(link); message.content.replace(link, ""); @@ -108,12 +110,12 @@ const getImagesFromMessage = async (message: Message) => { return null; })); - const filteredImages: string[] = [] - + const filteredImages: string[] = []; + resultArray.forEach((link) => { - if (link !== null) filteredImages.push(link) + if (link !== null) filteredImages.push(link); }); - + images.push(...filteredImages); } @@ -198,10 +200,11 @@ client.on("messageCreate", async (message) => { await message.reply( "Started conversation! Use /wipe to reset this conversation.", ); + error = true } } - let messages; + let messages: messagedata[]; if (llm.startsWith("openrouter^")) { const llm_real = llm.split("^"); @@ -212,14 +215,14 @@ client.on("messageCreate", async (message) => { "conversations", "openrouter", llm_real[llm_real.length - 1], - ])).value; + ])).value!; } else { messages = (await db.get([ "users", message.author.id, "conversations", llm, - ])).value; + ])).value!; } if (messages === null) { @@ -302,43 +305,52 @@ client.on("messageCreate", async (message) => { return; } - try { - resp = await chatgpt.send( - curmsgs, - message.content, - message.author.id, - ); + let thread: AnyThreadChannel; - messages[curconv].messages = resp.messages; + let useThread = false - console.log(resp.messages); + // deno-lint-ignore no-inner-declarations + async function cbfunction(type: string, resp: types.Response) { + console.log(resp) + if (type === "function") { - await db.set( - ["users", message.author.id, "conversations", llm], - messages, - ); + if (!useThread) { + useThread = true - const messagechunks = splitStringIntoChunks( - resp.oaires.choices[0].message.content, - 2000, - ); + thread = await msg.startThread({ + name: crypto.randomUUID() + }) - let cvalue = 0; + msg.edit("Check the thread for the bot's response!") - messagechunks.forEach((chunk) => { - if (cvalue === 0) { - cvalue = 1; - isMessageProcessing = false; + } + + console.log(resp.choices[0].message.content) + + thread.send(`${(resp.choices[0].message.content !== null) ? `${resp.choices[0].message.content}\n[used function "${resp.choices[0].message.tool_calls![0].function.name}"]\n` : `[used function "${resp.choices[0].message.tool_calls![0].function.name}"]\n`}`) + } else if (type === "complete") { + if (useThread === true) { + thread.send(resp.choices[0].message.content!) + } else { + msg.edit(resp.choices[0].message.content!) + } + + isMessageProcessing = false; db.set( ["users", message.author.id, "messageWaiting"], isMessageProcessing, ); - msg.edit(chunk); - } else { - message.reply(chunk); - } - }); + } + } + + try { + await chatgpt.send( + curmsgs, + message.content, + message.author.id, + cbfunction, + ); } catch (err) { isMessageProcessing = false; @@ -415,7 +427,7 @@ client.on("messageCreate", async (message) => { return; } } else if (llm === "gpt4_v") { - const images: string[] = await getImagesFromMessage(message) + const images: string[] = await getImagesFromMessage(message); if (!gpt4.isEnabled) { msg.edit( @@ -477,7 +489,7 @@ client.on("messageCreate", async (message) => { return; } } else if (llm === "gemini") { - const images: string[] = await getImagesFromMessage(message) + const images: string[] = await getImagesFromMessage(message); if (!gemini.isEnabled) { msg.edit( @@ -486,7 +498,7 @@ client.on("messageCreate", async (message) => { return; } - console.log(images) + console.log(images); try { resp = await gemini.send( @@ -542,6 +554,6 @@ client.on("messageCreate", async (message) => { } else { msg.edit("No handler for this LLM! Switch to a different one."); return; - } + } } }); diff --git a/prototype/README.md b/prototype/README.md new file mode 100644 index 0000000..d22e463 --- /dev/null +++ b/prototype/README.md @@ -0,0 +1,9 @@ +This folder contains code that is being prototyped for use in LLM Bot. + +Expect: + +- Security vulns +- General shitty code +- Horribly horrendous usage of the database + +# RUNNING ANYTHING IN HERE IS AT. YOUR. OWN. RISK!!!