diff --git a/app/ui/src/services/api.ts b/app/ui/src/services/api.ts index 3c7e5e65..3c7f9715 100644 --- a/app/ui/src/services/api.ts +++ b/app/ui/src/services/api.ts @@ -1,52 +1,53 @@ -import axios from 'axios'; -import { getToken } from './cookie'; +import axios from "axios"; +import { getToken } from "./cookie"; -export const baseURL = import.meta.env.VITE_API_URL || '/api/v1'; +export const baseURL = import.meta.env.VITE_API_URL || "/api/v1"; const instance = axios.create({ - baseURL, - headers: { - "Content-Type": "application/json", - }, + baseURL, + headers: { + "Content-Type": "application/json", + }, }); instance.interceptors.request.use( - (config) => { - const token = getToken() - if (token) { - config.headers!.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => { - return Promise.reject(error); + (config) => { + const token = getToken(); + if (token) { + config.headers!.Authorization = `Bearer ${token}`; } + return config; + }, + (error) => { + return Promise.reject(error); + }, ); instance.interceptors.response.use( - (res) => { - return res; - }, - async (err) => { - const originalConfig = err.config; - if (err.response) { - if (err.response.status === 401 && !originalConfig._retry) { - originalConfig._retry = true; - try { - return instance(originalConfig); - } catch (_error) { - - return Promise.reject(_error); - } - } - - if (err.response.status === 403 && err.response.data) { - return Promise.reject(err.response.data); - } + (res) => { + return res; + }, + async (err) => { + const originalConfig = err.config; + if (err.response) { + if (err.response.status === 401 && !originalConfig._retry) { + originalConfig._retry = true; + try { + return instance(originalConfig); + } catch (_error) { + return Promise.reject(_error); } + } else if (err.response.status === 401 && originalConfig._retry) { + window.location.href = "/#/login"; + } - return Promise.reject(err); + if (err.response.status === 403 && err.response.data) { + return Promise.reject(err.response.data); + } } + + return Promise.reject(err); + }, ); -export default instance; \ No newline at end of file +export default instance; diff --git a/docker/.env b/docker/.env index 535c0a78..8d522af2 100644 --- a/docker/.env +++ b/docker/.env @@ -16,4 +16,8 @@ GOOGLE_API_KEY="" # Eleven labs API Key -> https://elevenlabs.io/ ELEVENLABS_API_KEY="" # Dialoqbase Q Concurency -DB_QUEUE_CONCURRENCY=1 \ No newline at end of file +DB_QUEUE_CONCURRENCY=1 +# Dialoqbase Session Secret +DB_SESSION_SECRET="super-secret-key" +# Dialoqbase Session Secure +DB_SESSION_SECURE="false" diff --git a/server/package.json b/server/package.json index 0557e20e..5a59620b 100644 --- a/server/package.json +++ b/server/package.json @@ -23,6 +23,7 @@ "license": "MIT", "dependencies": { "@fastify/autoload": "^5.0.0", + "@fastify/cookie": "^9.1.0", "@fastify/cors": "^8.3.0", "@fastify/jwt": "^7.0.0", "@fastify/multipart": "^7.6.0", diff --git a/server/prisma/migrations/q_14/migration.sql b/server/prisma/migrations/q_14/migration.sql new file mode 100644 index 00000000..5549442a --- /dev/null +++ b/server/prisma/migrations/q_14/migration.sql @@ -0,0 +1,8 @@ +-- AlterTable +ALTER TABLE "Bot" ADD COLUMN "options" JSON DEFAULT '{}', +ADD COLUMN "use_rag" BOOLEAN NOT NULL DEFAULT false; + +ALTER TABLE "Bot" ADD COLUMN "bot_protect" BOOLEAN NOT NULL DEFAULT false; + + +ALTER TABLE "Bot" ADD COLUMN "bot_api_key" TEXT NULL DEFAULT NULL; \ No newline at end of file diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index ae3a4a7a..380fcc48 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -31,6 +31,10 @@ model Bot { text_to_voice_type_metadata Json @default("{}") @db.Json use_hybrid_search Boolean @default(false) haveDataSourcesBeenAdded Boolean @default(false) + use_rag Boolean @default(false) + bot_protect Boolean @default(false) + bot_api_key String? + options Json? @default("{}") @db.Json BotAppearance BotAppearance[] document BotDocument[] BotIntegration BotIntegration[] @@ -103,12 +107,12 @@ model BotIntegration { } model BotTelegramHistory { - id Int @id @default(autoincrement()) - chat_id Int? + id Int @id @default(autoincrement()) + chat_id Int? new_chat_id String? - identifier String? - human String? - bot String? + identifier String? + human String? + bot String? } model BotDiscordHistory { diff --git a/server/src/app.ts b/server/src/app.ts index 85ec4a2f..bae34591 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -5,6 +5,15 @@ import cors from "@fastify/cors"; import fastifyStatic from "@fastify/static"; import fastifyMultipart from "@fastify/multipart"; import { FastifySSEPlugin } from "@waylaidwanderer/fastify-sse-v2"; +import fastifyCookie from "@fastify/cookie"; +import fastifySession from "@fastify/session"; +import { getSessionSecret, isCookieSecure } from "./utils/session"; + +declare module "fastify" { + interface Session { + is_bot_allowed: boolean; + } +} export type AppOptions = {} & Partial; @@ -40,13 +49,21 @@ const app: FastifyPluginAsync = async ( preCompressed: true, }); - await fastify.register(import('fastify-raw-body'), { - field: 'rawBody', // change the default request.rawBody property name + fastify.register(fastifyCookie); + fastify.register(fastifySession, { + secret: getSessionSecret(), + cookie: { + secure: isCookieSecure(), + }, + }); + + await fastify.register(import("fastify-raw-body"), { + field: "rawBody", // change the default request.rawBody property name global: false, // add the rawBody to every request. **Default true** - encoding: 'utf8', // set it to false to set rawBody as a Buffer **Default utf8** + encoding: "utf8", // set it to false to set rawBody as a Buffer **Default utf8** runFirst: true, // get the body before any preParsing hook change/uncompress it. **Default false** - routes: [] // array of routes, **`global`** will be ignored, wildcard routes not supported - }) + routes: [], // array of routes, **`global`** will be ignored, wildcard routes not supported + }); }; export default app; diff --git a/server/src/routes/bot/handlers/get.handler.ts b/server/src/routes/bot/handlers/get.handler.ts index b5ee1dab..07e19b2c 100644 --- a/server/src/routes/bot/handlers/get.handler.ts +++ b/server/src/routes/bot/handlers/get.handler.ts @@ -1,6 +1,5 @@ import { FastifyReply, FastifyRequest } from "fastify"; -import { ChatStyleRequest } from "./types"; - +import { ChatStyleRequest } from "./types"; export const getChatStyleByIdHandler = async ( request: FastifyRequest, @@ -51,7 +50,7 @@ export const getChatStyleByIdHandler = async ( }, }; } - + request.session.is_bot_allowed = true; return { data: { background_color: "#ffffff", diff --git a/server/src/routes/bot/handlers/post.handler.ts b/server/src/routes/bot/handlers/post.handler.ts index 5fcbe6fb..ffe3ee7b 100644 --- a/server/src/routes/bot/handlers/post.handler.ts +++ b/server/src/routes/bot/handlers/post.handler.ts @@ -9,22 +9,134 @@ export const chatRequestHandler = async ( request: FastifyRequest, reply: FastifyReply, ) => { - const public_id = request.params.id; - const { message, history, history_id } = request.body; + try { + const public_id = request.params.id; - const prisma = request.server.prisma; + const prisma = request.server.prisma; - const bot = await prisma.bot.findFirst({ - where: { - publicId: public_id, - }, - }); + const bot = await prisma.bot.findFirst({ + where: { + publicId: public_id, + }, + }); + + if (!bot) { + return { + bot: { + text: "You are in the wrong place, buddy.", + sourceDocuments: [], + }, + history: [ + ...history, + { + type: "human", + text: message, + }, + { + type: "ai", + text: "You are in the wrong place, buddy.", + }, + ], + }; + } - if (!bot) { + if (bot.bot_protect) { + if (!request.session.get("is_bot_allowed")) { + return { + bot: { + text: "You are not allowed to chat with this bot.", + sourceDocuments: [], + }, + history: [ + ...history, + { + type: "human", + text: message, + }, + { + type: "ai", + text: "You are not allowed to chat with this bot.", + }, + ], + }; + } + } + + const temperature = bot.temperature; + + const sanitizedQuestion = message.trim().replaceAll("\n", " "); + const embeddingModel = embeddings(bot.embedding); + + const vectorstore = await DialoqbaseVectorStore.fromExistingIndex( + embeddingModel, + { + botId: bot.id, + sourceId: null, + }, + ); + + const model = chatModelProvider(bot.provider, bot.model, temperature); + + const chain = ConversationalRetrievalQAChain.fromLLM( + model, + vectorstore.asRetriever(), + { + qaTemplate: bot.qaPrompt, + questionGeneratorTemplate: bot.questionGeneratorPrompt, + returnSourceDocuments: true, + }, + ); + + const chat_history = history + .map((chatMessage: any) => { + if (chatMessage.type === "human") { + return `Human: ${chatMessage.text}`; + } else if (chatMessage.type === "ai") { + return `Assistant: ${chatMessage.text}`; + } else { + return `${chatMessage.text}`; + } + }) + .join("\n"); + + const response = await chain.call({ + question: sanitizedQuestion, + chat_history: chat_history, + }); + + await prisma.botWebHistory.create({ + data: { + chat_id: history_id, + bot_id: bot.id, + bot: response.text, + human: message, + metadata: { + ip: request?.ip, + user_agent: request?.headers["user-agent"], + }, + sources: response?.sources, + }, + }); + + return { + bot: response, + history: [ + ...history, + { + type: "human", + text: message, + }, + { + type: "ai", + text: response.text, + }, + ], + }; + } catch (e) { return { bot: { - text: "You are in the wrong place, buddy.", + text: "There was an error processing your request.", sourceDocuments: [], }, history: [ @@ -35,84 +147,11 @@ export const chatRequestHandler = async ( }, { type: "ai", - text: "You are in the wrong place, buddy.", + text: "There was an error processing your request.", }, ], }; } - - const temperature = bot.temperature; - - const sanitizedQuestion = message.trim().replaceAll("\n", " "); - const embeddingModel = embeddings(bot.embedding); - - const vectorstore = await DialoqbaseVectorStore.fromExistingIndex( - embeddingModel, - { - botId: bot.id, - sourceId: null, - }, - ); - - const model = chatModelProvider(bot.provider, bot.model, temperature); - - const chain = ConversationalRetrievalQAChain.fromLLM( - model, - vectorstore.asRetriever(), - { - qaTemplate: bot.qaPrompt, - questionGeneratorTemplate: bot.questionGeneratorPrompt, - returnSourceDocuments: true, - }, - ); - - const chat_history = history - .map((chatMessage: any) => { - if (chatMessage.type === "human") { - return `Human: ${chatMessage.text}`; - } else if (chatMessage.type === "ai") { - return `Assistant: ${chatMessage.text}`; - } else { - return `${chatMessage.text}`; - } - }) - .join("\n"); - - console.log(chat_history); - - const response = await chain.call({ - question: sanitizedQuestion, - chat_history: chat_history, - }); - - await prisma.botWebHistory.create({ - data: { - chat_id: history_id, - bot_id: bot.id, - bot: response.text, - human: message, - metadata: { - ip: request?.ip, - user_agent: request?.headers["user-agent"], - }, - sources: response?.sources, - }, - }); - - return { - bot: response, - history: [ - ...history, - { - type: "human", - text: message, - }, - { - type: "ai", - text: response.text, - }, - ], - }; }; function nextTick() { @@ -123,19 +162,19 @@ export const chatRequestStreamHandler = async ( request: FastifyRequest, reply: FastifyReply, ) => { + const { message, history, history_id } = request.body; + try { const public_id = request.params.id; // get user meta info from request // const meta = request.headers["user-agent"]; // ip address - const { message, history, history_id } = request.body; // const history = JSON.parse(chatHistory) as { // type: string; // text: string; // }[]; - console.log("history", history); const prisma = request.server.prisma; const bot = await prisma.bot.findFirst({ @@ -164,6 +203,33 @@ export const chatRequestStreamHandler = async ( }; } + if (bot.bot_protect) { + if (!request.session.get("is_bot_allowed")) { + console.log("not allowed"); + return reply.sse({ + event: "result", + id: "", + data: JSON.stringify({ + bot: { + text: "You are not allowed to chat with this bot.", + sourceDocuments: [], + }, + history: [ + ...history, + { + type: "human", + text: message, + }, + { + type: "ai", + text: "You are not allowed to chat with this bot.", + }, + ], + }), + }); + } + } + const temperature = bot.temperature; const sanitizedQuestion = message.trim().replaceAll("\n", " "); @@ -243,13 +309,11 @@ export const chatRequestStreamHandler = async ( console.log("Waiting for response..."); - response = await chain.call({ question: sanitizedQuestion, chat_history: chat_history, }); - await prisma.botWebHistory.create({ data: { chat_id: history_id, @@ -286,6 +350,26 @@ export const chatRequestStreamHandler = async ( return reply.raw.end(); } catch (e) { console.log(e); - return reply.status(500).send(e); + return reply.sse({ + event: "result", + id: "", + data: JSON.stringify({ + bot: { + text: "There was an error processing your request.", + sourceDocuments: [], + }, + history: [ + ...history, + { + type: "human", + text: message, + }, + { + type: "ai", + text: "There was an error processing your request.", + }, + ], + }), + }); } }; diff --git a/server/src/utils/session.ts b/server/src/utils/session.ts new file mode 100644 index 00000000..aa4b8427 --- /dev/null +++ b/server/src/utils/session.ts @@ -0,0 +1,21 @@ +export const getSessionSecret = () => { + if (!process.env.DB_SESSION_SECRET) { + return "a8F2h6T9j4Kl0Pz8W7eX3rB5y1VcQ6mN"; + } + + if (process.env.DB_SESSION_SECRET.length < 32) { + console.warn("WARNING: Session secret should be 32 characters long."); + } + + return process.env.DB_SESSION_SECRET; +}; + + + +export const isCookieSecure = () => { + if (!process.env.DB_SESSION_SECURE) { + return false; + } + + return process.env.DB_SESSION_SECURE === "true"; +} \ No newline at end of file diff --git a/server/yarn.lock b/server/yarn.lock index 25b1564c..c6801011 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -373,6 +373,14 @@ dependencies: text-decoding "^1.0.0" +"@fastify/cookie@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@fastify/cookie/-/cookie-9.1.0.tgz#b9ca95fcb934a21915ab6f228a63dd73013738df" + integrity sha512-w/LlQjj7cmYlQNhEKNm4jQoLkFXCL73kFu1Jy3aL7IFbYEojEKur0f7ieCKUxBBaU65tpaWC83UM8xW7AzY6uw== + dependencies: + cookie "^0.5.0" + fastify-plugin "^4.0.0" + "@fastify/cors@^8.3.0": version "8.3.0" resolved "https://registry.yarnpkg.com/@fastify/cors/-/cors-8.3.0.tgz#f03d745731b770793a1a15344da7220ca0d19619"