diff --git a/README.md b/README.md index d90e406..c8631f6 100644 --- a/README.md +++ b/README.md @@ -62,17 +62,23 @@ make dev ``` **NOTE:** `make dev` Requires Node.js environment installed, or at least have `node_modules` specified in `package.json` installed on your server. Please see [Local Machine](#local-machine) section. -### Setup in your server -1. Download this repo into your host machine. -2. Open [generate_production_env.html](./generate_production_env.html) in your browser and - * Set fields you want to make change according to instructions - * Click `Generate Env File` button on the bottom - * Rename the downloaded file to `.env.production` and copy-paste it into root folder of this project -3. Make sure you installed `docker` and `make` in your host machine. -4. Run command `make up` to build & run, find this app on your host machine's port `8000`. - - -**Hint**: Check [docker-compose.yaml](./docker-compose.yaml), [Makefile](./Makefile) and [plugin.js](./tools/plugin.js) to make it fits your own preference. +## Embed your chatbot into website +To embed a chatbot to your website, simply add +```html + +``` +into the bottom of your html body element. So easy! +Please change the `http://localhost:8000` and the base_url in request query to your real host to make sure it works. + +If you want to hide the real link, in your javascript code you can do +```js +const chatbox_script = await (await fetch("http://localhost:8000/chatbox?base_url=http%3A%2F%2Flocalhost%3A8000")).blob(); +const chatbox_url = URL.createObjectURL(chatbox_script); +const script_elem = document.createElement('script'); +script_elem.src = chatbox_url; +document.body.append(script_elem); +``` +And remember to use `URL.revokeObjectURL(chatbox_url)` if you don't need it anymore. ## Lint To start lint your code, simply run diff --git a/generate_production_env.html b/generate_production_env.html deleted file mode 100644 index 1abd35b..0000000 --- a/generate_production_env.html +++ /dev/null @@ -1,376 +0,0 @@ - - - - - - - Generate Voyager Production Env - - - - -
-
-

Allow Origin

-
Allow only one origin can access the APIs
-
-
Allow origin name:
- -
-
-
-

Https Features

-
- Enable https and specify path to Key and Certificate and/or CA files
- Only with pem extension is available.
- The CA part is optional and other parts are required, otherwise this function won't work. -
-
-
Enable Https:
- -
-
-
Path to Key:
- -
-
-
Path to Certificate:
- -
-
-
Path to CA:
- -
-
-
-

Enable Plugin

-
- Enables plugin to modify the action of /v1/chat/completions.
- Please enable this and write your own plugin in /tools/plugin.js -
-
-
Enables plugins:
- -
-
-
-

System Instruction

-
- Specify the default instruction for AI, won't work when use OpenAI like request format. -
-
-
System instruction:
- -
-
-
-

Default Dataset

-
- Set load default dataset or not, and specify dataset name.
- Please implement your own dataset generator in /tools/plugin.js use the function loadDefaultDataset() -
-
-
Enable load default dataset:
- -
-
Set default dataset name:
- -
-
-
-

Static API-Key Enabled

-
- If enabled, instead of query api-key from database, a text comparation will be conducted on provided API key.
- Remember to include API key in your server global environment named STATIC_API_KEY.
- It should be sent through header in the format of Authorization: Bearer <your-api-key>
-
-
Enable static API-Key:
- -
-
-
-

APIs Available

-
Set available APIs in your server.
-
-
Enable All APIs
- -
-
-
Enable All Index APIs
- -
-
-
Enable Route Docs
- -
-
-
Enable Server Stats
- -
-
-
Enable Healthy Check
- -
-
-
Enable All Inference APIs
- -
-
-
Enable Inference Completions
- -
-
-
Enable RAG Inference Completions
- -
-
-
Enable Token API
- -
-
-
Enable Embedding API
- -
-
-
Enable Calculate Embeddings
- -
-
-
Enable Dataset Upload
- -
-
-
Enable Version API
- -
-
-
-

Generate & Download Env File

-
- Remember to rename your env file to .env.production
- (The . at the most beginning might disappear due to browser security policy)
- And replace the existed .env.production file in root dir.
- -
-
- - \ No newline at end of file diff --git a/routes/embedding.js b/routes/embedding.js index 87229db..95da940 100644 --- a/routes/embedding.js +++ b/routes/embedding.js @@ -20,7 +20,7 @@ import { isRouteEnabled } from "../tools/enabledApiDecoder.js"; export default function embeddingRoute() { const router = Router(); - isRouteEnabled("embedding", "index") && router.post("/", embeddings); + isRouteEnabled("embedding", "calculate") && router.post("/", embeddings); isRouteEnabled("embedding", "dataset") && router.post("/dataset", uploadDataset); return router; diff --git a/routes/index.js b/routes/index.js index 20e5863..04e64e5 100644 --- a/routes/index.js +++ b/routes/index.js @@ -23,6 +23,7 @@ import embeddingRoute from "./embedding.js"; // import decoderRoute from "./decoder.js"; import versionRoute from "./version.js"; import { isRouteEnabled } from "../tools/enabledApiDecoder.js"; +import { generateScript } from "../tools/web_embed.js"; function indexRoute() { const router = Router(); @@ -33,6 +34,13 @@ function indexRoute() { }) } + if(isRouteEnabled("index", "chatbox")) { + router.get('/chatbox', (req, res)=>{ + res.setHeader("Content-Type", "application/json; charset=utf-8") + res.send(generateScript(req.query.base_url)); + }) + } + return router; } diff --git a/setup/config.h b/setup/config.h index b5b3bed..6a48c69 100644 --- a/setup/config.h +++ b/setup/config.h @@ -34,6 +34,7 @@ #define API_INDEX_DOC_ENABLED 1 #define API_INDEX_STATS_ENABLED 1 #define API_INDEX_HEALTHY_ENABLED 1 +#define API_INDEX_CHATBOX_ENABLED 1 #define API_INFERENCE_COMP_ENABLED 1 #define API_INFERENCE_RAG_ENABLED 1 #define API_TOKEN_ENABLED 1 diff --git a/setup/default_config.h b/setup/default_config.h index 1d01dd8..e9fc66b 100644 --- a/setup/default_config.h +++ b/setup/default_config.h @@ -34,6 +34,7 @@ #define DEFAULT_API_INDEX_DOC_ENABLED 1 #define DEFAULT_API_INDEX_STATS_ENABLED 1 #define DEFAULT_API_INDEX_HEALTHY_ENABLED 1 +#define DEFAULT_API_INDEX_CHATBOX_ENABLED 1 #define DEFAULT_API_INFERENCE_COMP_ENABLED 1 #define DEFAULT_API_INFERENCE_RAG_ENABLED 1 #define DEFAULT_API_TOKEN_ENABLED 1 diff --git a/setup/setup.c b/setup/setup.c index 6fbf4fb..7c8c540 100644 --- a/setup/setup.c +++ b/setup/setup.c @@ -31,6 +31,7 @@ char* default_dataset_name = DEFAULT_DATASET_NAME; int api_index_doc_enabled = API_INDEX_DOC_ENABLED; int api_index_stats_enabled = API_INDEX_STATS_ENABLED; int api_index_healthy_enabled = API_INDEX_HEALTHY_ENABLED; +int api_index_chatbox_enabled = API_INDEX_CHATBOX_ENABLED; int api_inference_comp_enabled = API_INFERENCE_COMP_ENABLED; int api_inference_rag_enabled = API_INFERENCE_RAG_ENABLED; int api_token_enabled = API_TOKEN_ENABLED; @@ -465,6 +466,7 @@ void logAPIMenu() { printf(API_INDEX_DOC, api_index_doc_enabled?ENABLED:DISABLED); printf(API_INDEX_STATS, api_index_stats_enabled?ENABLED:DISABLED); printf(API_INDEX_HEALTHY, api_index_healthy_enabled?ENABLED:DISABLED); + printf(API_INDEX_CHATBOX, api_index_chatbox_enabled?ENABLED:DISABLED); printf(API_INFERENCE_COMP, api_inference_comp_enabled?ENABLED:DISABLED); printf(API_INFERENCE_RAG, api_inference_rag_enabled?ENABLED:DISABLED); printf(API_TOKEN, api_token_enabled?ENABLED:DISABLED); @@ -484,16 +486,18 @@ void apiSettings() { case 'c': case 'C': api_index_healthy_enabled = !api_index_healthy_enabled; break; case 'd': case 'D': - api_inference_comp_enabled = !api_inference_comp_enabled; break; + api_index_chatbox_enabled = !api_index_chatbox_enabled; break; case 'e': case 'E': - api_inference_rag_enabled = !api_inference_rag_enabled; break; + api_inference_comp_enabled = !api_inference_comp_enabled; break; case 'f': case 'F': - api_token_enabled = !api_token_enabled; break; + api_inference_rag_enabled = !api_inference_rag_enabled; break; case 'g': case 'G': - api_embedding_calc_enabled = !api_embedding_calc_enabled; break; + api_token_enabled = !api_token_enabled; break; case 'h': case 'H': - api_embedding_ds_enabled = !api_embedding_ds_enabled; break; + api_embedding_calc_enabled = !api_embedding_calc_enabled; break; case 'i': case 'I': + api_embedding_ds_enabled = !api_embedding_ds_enabled; break; + case 'j': case 'J': api_version_enabled = !api_version_enabled; break; case 'q': case 'Q': case ESC: return; @@ -501,6 +505,7 @@ void apiSettings() { api_index_doc_enabled = 1; api_index_stats_enabled = 1; api_index_healthy_enabled = 1; + api_index_chatbox_enabled = 1; api_inference_comp_enabled = 1; api_inference_rag_enabled = 1; api_token_enabled = 1; @@ -512,6 +517,7 @@ void apiSettings() { api_index_doc_enabled = 0; api_index_stats_enabled = 0; api_index_healthy_enabled = 0; + api_index_chatbox_enabled = 0; api_inference_comp_enabled = 0; api_inference_rag_enabled = 0; api_token_enabled = 0; @@ -578,6 +584,7 @@ void saveSettings(int show_message) { api_index_doc_enabled, api_index_stats_enabled, api_index_healthy_enabled, + api_index_chatbox_enabled, api_inference_comp_enabled, api_inference_rag_enabled, api_token_enabled, @@ -605,6 +612,7 @@ void saveSettings(int show_message) { api_index_doc_enabled && api_index_stats_enabled && api_index_healthy_enabled && + api_index_chatbox_enabled && api_inference_comp_enabled && api_inference_rag_enabled && api_token_enabled && @@ -619,6 +627,7 @@ void saveSettings(int show_message) { api_index_doc_enabled, api_index_stats_enabled, api_index_healthy_enabled, + api_index_chatbox_enabled, api_inference_comp_enabled, api_inference_rag_enabled, api_token_enabled, @@ -736,6 +745,7 @@ void saveDefaultSettings() { api_index_doc_enabled = DEFAULT_API_INDEX_DOC_ENABLED; api_index_stats_enabled = DEFAULT_API_INDEX_STATS_ENABLED; api_index_healthy_enabled = DEFAULT_API_INDEX_HEALTHY_ENABLED; + api_index_chatbox_enabled = DEFAULT_API_INDEX_CHATBOX_ENABLED; api_inference_comp_enabled = DEFAULT_API_INFERENCE_COMP_ENABLED; api_inference_rag_enabled = DEFAULT_API_INFERENCE_RAG_ENABLED; api_token_enabled = DEFAULT_API_TOKEN_ENABLED; diff --git a/setup/setup_types.h b/setup/setup_types.h index 95682fa..41c6951 100644 --- a/setup/setup_types.h +++ b/setup/setup_types.h @@ -392,23 +392,26 @@ #define API_INDEX_HEALTHY \ "c. %s Healthy..............(/healthy)\n" +#define API_INDEX_CHATBOX \ + "d. %s Chatbox Embed........(/chatbox)\n" + #define API_INFERENCE_COMP \ - "d. %s Inference............(/v1/chat/completions)\n" + "e. %s Inference............(/v1/chat/completions)\n" #define API_INFERENCE_RAG \ - "e. %s RAG Inference........(/v1/chat/rag-completions)\n" + "f. %s RAG Inference........(/v1/chat/rag-completions)\n" #define API_TOKEN \ - "f. %s API Key..............(/v1/token/api-key)\n" + "g. %s API Key..............(/v1/token/api-key)\n" #define API_EMBEDDING_CALC \ - "g. %s Calcualte Embedding..(/v1/embeddings)\n" + "h. %s Calcualte Embedding..(/v1/embeddings)\n" #define API_EMBEDDING_DS \ - "h. %s Upload Dataset.......(/v1/embeddings/dataset)\n" + "i. %s Upload Dataset.......(/v1/embeddings/dataset)\n" #define API_VERSION \ - "i. %s Version..............(/v1/version)\n" + "j. %s Version..............(/v1/version)\n" #define API_MENU_END \ "y. Enable All\n"\ @@ -488,6 +491,7 @@ "#define API_INDEX_DOC_ENABLED %d\n"\ "#define API_INDEX_STATS_ENABLED %d\n"\ "#define API_INDEX_HEALTHY_ENABLED %d\n"\ +"#define API_INDEX_CHATBOX_ENABLED %d\n"\ "#define API_INFERENCE_COMP_ENABLED %d\n"\ "#define API_INFERENCE_RAG_ENABLED %d\n"\ "#define API_TOKEN_ENABLED %d\n"\ @@ -588,7 +592,7 @@ "SYSTEM_INSTRUCTION=\"%s\"\n"\ "AVAILABLE_APIS=\"%s\"" -#define AVAILABLE_APIS_FORMAT "%d%d%d.%d%d.%d.%d%d.%d" +#define AVAILABLE_APIS_FORMAT "%d%d%d%d.%d%d.%d.%d%d.%d" #define DOCKERFILE \ "FROM node:20.15.1-slim\n"\ diff --git a/swagger.json b/swagger.json index 38c0838..9a18c22 100644 --- a/swagger.json +++ b/swagger.json @@ -87,6 +87,29 @@ } } }, + "/chatbox": { + "get": { + "tags": ["Index"], + "summary" : "Route to embed chatbot", + "description": "Please see this [README](https://github.com/SkywardAI/voyager?tab=readme-ov-file#embed-your-chatbot-into-website).", + "parameters": [ + { + "in": "query", + "name": "base_url", + "type": "string", + "description": "The base url of request, usually the url of this page you are looking at. Default http://localhost:8000." + } + ], + "responses": { + "200": { + "description": "Returns the load script, usually load with script tag in HTML", + "content":{ + "application/json; charset=utf-8": {} + } + } + } + } + }, "/v1/chat/completions": { "post": { "tags": [ diff --git a/tools/enabledApiDecoder.js b/tools/enabledApiDecoder.js index 28f35a0..2341ec3 100644 --- a/tools/enabledApiDecoder.js +++ b/tools/enabledApiDecoder.js @@ -17,7 +17,8 @@ const allow_paths = { index: { docs: false, stats: false, - healthy: false + healthy: false, + chatbox: false }, inference: { completions: false, @@ -71,7 +72,7 @@ export function decodeEnabledAPIs() { /** * check if provided route enabled * @param {"index"|"inference"|"embedding"|"version"|"token"} index_path_name name of route index - * @param {"docs"|"stats"|"healthy"|"completions"|"rag"} api_name name of specific api + * @param {"docs"|"stats"|"healthy"|"chatbox"|"completions"|"rag"|"calculate"|"dataset"} api_name name of specific api * @returns {Boolean} */ export function isRouteEnabled(index_path_name, api_name = null) { diff --git a/tools/web_embed.js b/tools/web_embed.js new file mode 100644 index 0000000..c84cc6d --- /dev/null +++ b/tools/web_embed.js @@ -0,0 +1,358 @@ +/** + * Generates the script with base_url, to simply embed a chatbot in web page + * @param {String} base_url The url to send request, default `http://localhost:8000` + */ +export function generateScript(base_url = 'http://localhost:8000') { + return ` +(function() { + 'use strict'; + + const BASE_URL="${base_url}"; + +const styles = \` + +\` + + // styles + document.head.insertAdjacentHTML("beforeend", styles); + + const chat_icon = \` + + + + \` + + const cancel_icon = \` + + + \` + + const send_icon = \` + + + \` + + const circle_icon = \` + + + \` + + document.body.insertAdjacentHTML("beforeend", + \`
+
\${chat_icon}
+
+
\${cancel_icon}
+ +
+ +
\${send_icon}
+
+
+
\`) + + function submitMessage(evt) { + evt.preventDefault(); + const message = evt.target.message.value; + if(message) inference(message); + evt.target.message.value = ''; + } + + function createElement(tagName, className, textContent) { + const elem = document.createElement(tagName); + elem.className = className; + elem.textContent = textContent; + return elem; + } + + async function inference(message) { + pending_conversation.before(createElement('div', 'bubble user', message)); + conversation_main.scrollTo({behavior: "smooth", top: conversation_main.scrollHeight}) + + pending_conversation.innerHTML = \`\${circle_icon}\${circle_icon}\${circle_icon}\` + pending_conversation.classList.remove('empty') + + const resp = await fetch(\`\${BASE_URL}/v1/chat/completions\`, { + method: "POST", + headers: { + "Content-Type": "application/json", + authorization: "Bearer example-chatbot" + }, + body: JSON.stringify({ + messages: [ + {role: 'system', content: "You are a helpful assistant solves problem"}, + {role: 'user', content: message} + ], + stream: true + }) + }); + if(resp.ok) { + const reader = resp.body.pipeThrough(new TextDecoderStream()).getReader(); + let response = '' + while(true) { + const { value, done } = await reader.read(); + if(done) break; + try { + value.split("\\n\\n").forEach(json_str => { + if(json_str) { + const { choices } = JSON.parse(json_str); + const content = choices[0].delta.content + response += content; + pending_conversation.textContent = response; + conversation_main.scrollTo({ + behavior: "smooth", + top: conversation_main.scrollHeight + }) + } + }) + } catch(error) { + console.error(error, value) + } + } + pending_conversation.textContent = ''; + pending_conversation.classList.add('empty'); + pending_conversation.before(createElement('div', 'bubble assistant', response)); + } + } + + const chatbox = document.getElementById('voyager-chatbox'); + const pending_conversation = document.getElementById('pending-conversation'); + const conversation_main = document.getElementById('conversation-main'); + + document.querySelector('#voyager-chatbox > .expand-btn').onclick = + ()=>chatbox.classList.replace('folded', 'expanded'); + document.querySelector('#voyager-chatbox .icon.cancel').onclick = + ()=>chatbox.classList.replace('expanded', 'folded'); + document.querySelector('#voyager-chatbox form.input-message-form').onsubmit = submitMessage; +})();` +} \ No newline at end of file