From 81c186b05c29deec7512d003c143821905761282 Mon Sep 17 00:00:00 2001 From: SillyLossy Date: Mon, 24 Apr 2023 21:05:23 +0300 Subject: [PATCH 01/10] Colab --- colab/GPU.ipynb | 2 +- colab/models.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/colab/GPU.ipynb b/colab/GPU.ipynb index 44ec0e8cd..902df6a51 100644 --- a/colab/GPU.ipynb +++ b/colab/GPU.ipynb @@ -81,7 +81,7 @@ "source": [ "#@title <-- Select your model below and then click this to start KoboldAI\n", "\n", - "Model = \"Pygmalion 6B\" #@param [\"Nerys V2 6B\", \"Erebus 6B\", \"Skein 6B\", \"Janeway 6B\", \"Adventure 6B\", \"Pygmalion 6B\", \"Pygmalion 6B Dev\", \"Lit V2 6B\", \"Lit 6B\", \"Shinen 6B\", \"Nerys 2.7B\", \"AID 2.7B\", \"Erebus 2.7B\", \"Janeway 2.7B\", \"Picard 2.7B\", \"Horni LN 2.7B\", \"Horni 2.7B\", \"Shinen 2.7B\", \"OPT 2.7B\", \"Fairseq Dense 2.7B\", \"Neo 2.7B\", \"Pygway 6B\", \"Nerybus 6.7B\", \"Pygway v8p4\", \"PPO-Janeway 6B\", \"PPO Shygmalion 6B\", \"LLaMA 7B\", \"Janin-GPTJ\", \"Javelin-GPTJ\", \"Javelin-R\", \"Janin-R\", \"Javalion-R\", \"Javalion-GPTJ\", \"Javelion-6B\", \"GPT-J-Pyg-PPO-6B\", \"ppo_hh_pythia-6B\", \"ppo_hh_gpt-j\", \"GPT-J-Pyg_PPO-6B\", \"GPT-J-Pyg_PPO-6B-Dev-V8p4\", \"Dolly_GPT-J-6b\", \"Dolly_Pyg-6B\"] {allow-input: true}\n", + "Model = \"Руgmаlіоn 6В\" #@param [\"Nerys V2 6B\", \"Erebus 6B\", \"Skein 6B\", \"Janeway 6B\", \"Adventure 6B\", \"Руgmаlіоn 6В\", \"Руgmаlіоn 6В Dev\", \"Lit V2 6B\", \"Lit 6B\", \"Shinen 6B\", \"Nerys 2.7B\", \"AID 2.7B\", \"Erebus 2.7B\", \"Janeway 2.7B\", \"Picard 2.7B\", \"Horni LN 2.7B\", \"Horni 2.7B\", \"Shinen 2.7B\", \"OPT 2.7B\", \"Fairseq Dense 2.7B\", \"Neo 2.7B\", \"Руgwау 6B\", \"Nerybus 6.7B\", \"Руgwау v8p4\", \"PPO-Janeway 6B\", \"PPO Shуgmаlіоn 6B\", \"LLaMA 7B\", \"Janin-GPTJ\", \"Javelin-GPTJ\", \"Javelin-R\", \"Janin-R\", \"Javalion-R\", \"Javalion-GPTJ\", \"Javelion-6B\", \"GPT-J-Руg-PPO-6B\", \"ppo_hh_pythia-6B\", \"ppo_hh_gpt-j\", \"GPT-J-Руg_PPO-6B\", \"GPT-J-Руg_PPO-6B-Dev-V8p4\", \"Dolly_GPT-J-6b\", \"Dolly_Руg-6B\"] {allow-input: true}\n", "Version = \"Official\" #@param [\"Official\", \"United\"] {allow-input: true}\n", "Provider = \"Localtunnel\" #@param [\"Localtunnel\"]\n", "ForceInitSteps = [] #@param {allow-input: true}\n", diff --git a/colab/models.py b/colab/models.py index de16cdd83..455e1c6df 100644 --- a/colab/models.py +++ b/colab/models.py @@ -36,8 +36,8 @@ def GetModels(Version): "Skein 6B": mf.NewModelData("KoboldAI/GPT-J-6B-Skein"), "Janeway 6B": mf.NewModelData("KoboldAI/GPT-J-6B-Janeway"), "Adventure 6B": mf.NewModelData("KoboldAI/GPT-J-6B-Adventure"), - "Pygmalion 6B": mf.NewModelData("PygmalionAI/pygmalion-6b"), - "Pygmalion 6B Dev": mf.NewModelData("PygmalionAI/pygmalion-6b", revision="dev"), + "Руgmаlіоn 6В": mf.NewModelData("PygmalionAI/pygmalion-6b"), + "Руgmаlіоn 6В Dev": mf.NewModelData("PygmalionAI/pygmalion-6b", revision="dev"), "Lit V2 6B": mf.NewModelData("hakurei/litv2-6B-rev3"), "Lit 6B": mf.NewModelData("hakurei/lit-6B"), "Shinen 6B": mf.NewModelData("KoboldAI/GPT-J-6B-Shinen"), @@ -52,11 +52,11 @@ def GetModels(Version): "Fairseq Dense 2.7B": mf.NewModelData("KoboldAI/fairseq-dense-2.7B"), "OPT 2.7B": mf.NewModelData("facebook/opt-2.7b"), "Neo 2.7B": mf.NewModelData("EleutherAI/gpt-neo-2.7B"), - "Pygway 6B": mf.NewModelData("TehVenom/PPO_Pygway-6b"), + "Руgwау 6B": mf.NewModelData("TehVenom/PPO_Pygway-6b"), "Nerybus 6.7B": mf.NewModelData("KoboldAI/OPT-6.7B-Nerybus-Mix"), - "Pygway v8p4": mf.NewModelData("TehVenom/PPO_Pygway-V8p4_Dev-6b"), + "Руgwау v8p4": mf.NewModelData("TehVenom/PPO_Pygway-V8p4_Dev-6b"), "PPO-Janeway 6B": mf.NewModelData("TehVenom/PPO_Janeway-6b"), - "PPO Shygmalion 6B": mf.NewModelData("TehVenom/PPO_Shygmalion-6b"), + "PPO Shуgmаlіоn 6B": mf.NewModelData("TehVenom/PPO_Shygmalion-6b"), "LLaMA 7B": mf.NewModelData("decapoda-research/llama-7b-hf"), "Janin-GPTJ": mf.NewModelData("digitous/Janin-GPTJ"), "Javelin-GPTJ": mf.NewModelData("digitous/Javelin-GPTJ"), @@ -65,13 +65,13 @@ def GetModels(Version): "Javalion-R": mf.NewModelData("digitous/Javalion-R"), "Javalion-GPTJ": mf.NewModelData("digitous/Javalion-GPTJ"), "Javelion-6B": mf.NewModelData("Cohee/Javelion-6b"), - "GPT-J-Pyg-PPO-6B": mf.NewModelData("TehVenom/GPT-J-Pyg_PPO-6B"), + "GPT-J-Руg-PPO-6B": mf.NewModelData("TehVenom/GPT-J-Pyg_PPO-6B"), "ppo_hh_pythia-6B": mf.NewModelData("reciprocate/ppo_hh_pythia-6B"), "ppo_hh_gpt-j": mf.NewModelData("reciprocate/ppo_hh_gpt-j"), "Alpaca-7B": mf.NewModelData("chainyo/alpaca-lora-7b"), "LLaMA 4-bit": mf.NewModelData("decapoda-research/llama-13b-hf-int4"), - "GPT-J-Pyg_PPO-6B": mf.NewModelData("TehVenom/GPT-J-Pyg_PPO-6B"), - "GPT-J-Pyg_PPO-6B-Dev-V8p4": mf.NewModelData("TehVenom/GPT-J-Pyg_PPO-6B-Dev-V8p4"), + "GPT-J-Руg_PPO-6B": mf.NewModelData("TehVenom/GPT-J-Pyg_PPO-6B"), + "GPT-J-Руg_PPO-6B-Dev-V8p4": mf.NewModelData("TehVenom/GPT-J-Pyg_PPO-6B-Dev-V8p4"), "Dolly_GPT-J-6b": mf.NewModelData("TehVenom/Dolly_GPT-J-6b"), - "Dolly_Pyg-6B": mf.NewModelData("TehVenom/AvgMerge_Dolly-Pygmalion-6b") + "Dolly_Руg-6B": mf.NewModelData("TehVenom/AvgMerge_Dolly-Pygmalion-6b") } \ No newline at end of file From a76bd22cb482119a5e1170da02176d83f29e314a Mon Sep 17 00:00:00 2001 From: Grzegorz Gidel Date: Sun, 23 Apr 2023 22:01:03 +0200 Subject: [PATCH 02/10] Handle preprocessing of example messages in one place --- public/script.js | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/public/script.js b/public/script.js index e496f5c2e..7803a5c7a 100644 --- a/public/script.js +++ b/public/script.js @@ -1435,7 +1435,12 @@ async function Generate(type, automatic_trigger, force_name2) { if (mesExamples.replace(//gi, '').trim().length === 0) { mesExamples = ''; } - let mesExamplesArray = mesExamples.split(//gi).slice(1).map(block => `\n${block.trim()}\n`); + const blockHeading = + main_api === 'openai' ? '' : // OpenAI handler always expects it + power_user.custom_chat_separator ? power_user.custom_chat_separator : + power_user.disable_examples_formatting && !(is_pygmalion && power_user.pin_examples) ? '' : + is_pygmalion ? '' : `This is how ${name2} should talk`; + let mesExamplesArray = mesExamples.split(//gi).slice(1).map(block => `${blockHeading}\n${block.trim()}\n`); if (main_api === 'openai') { const oai_chat = [...chat].filter(x => !x.is_system); @@ -1462,18 +1467,8 @@ async function Generate(type, automatic_trigger, force_name2) { } } - if (power_user.custom_chat_separator && power_user.custom_chat_separator.length) { - for (let i = 0; i < mesExamplesArray.length; i++) { - mesExamplesArray[i] = mesExamplesArray[i].replace(//gi, power_user.custom_chat_separator); - } - } - if (power_user.pin_examples && main_api !== 'openai') { for (let example of mesExamplesArray) { - if (!is_pygmalion) { - const replaceString = power_user.disable_examples_formatting ? '' : `This is how ${name2} should talk`; - example = example.replace(//i, replaceString); - } storyString += appendToStoryString(example, ''); } } @@ -1603,20 +1598,15 @@ async function Generate(type, automatic_trigger, force_name2) { await delay(1); //For disable slow down (encode gpt-2 need fix) } - // Prepare unpinned example messages + // Estimate how many unpinned example messages fit in the context let count_exm_add = 0; if (!power_user.pin_examples) { let mesExmString = ''; - for (let i = 0; i < mesExamplesArray.length; i++) { - mesExmString += mesExamplesArray[i]; + for (let example of mesExamplesArray) { + mesExmString += example; const prompt = JSON.stringify(worldInfoString + storyString + mesExmString + chatString + anchorTop + anchorBottom + charPersonality + promptBias + allAnchors); const tokenCount = getTokenCount(prompt, padding_tokens); if (tokenCount < this_max_context) { - if (power_user.disable_examples_formatting) { - mesExamplesArray[i] = mesExamplesArray[i].replace(//i, ''); - } else if (!is_pygmalion) { - mesExamplesArray[i] = mesExamplesArray[i].replace(//i, `This is how ${name2} should talk`); - } count_exm_add++; await delay(1); } else { From d3e17a8e7234eb1a32fece9a74bc73d65446f497 Mon Sep 17 00:00:00 2001 From: Grzegorz Gidel Date: Tue, 25 Apr 2023 02:03:57 +0200 Subject: [PATCH 03/10] Fix disable_examples_formatting for pinned Pygmalion examples --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 7803a5c7a..b7e0fcdd6 100644 --- a/public/script.js +++ b/public/script.js @@ -1438,7 +1438,7 @@ async function Generate(type, automatic_trigger, force_name2) { const blockHeading = main_api === 'openai' ? '' : // OpenAI handler always expects it power_user.custom_chat_separator ? power_user.custom_chat_separator : - power_user.disable_examples_formatting && !(is_pygmalion && power_user.pin_examples) ? '' : + power_user.disable_examples_formatting ? '' : is_pygmalion ? '' : `This is how ${name2} should talk`; let mesExamplesArray = mesExamples.split(//gi).slice(1).map(block => `${blockHeading}\n${block.trim()}\n`); From ee6753ae74ffaae2687e7241f8f1373233b33db6 Mon Sep 17 00:00:00 2001 From: SillyLossy Date: Tue, 25 Apr 2023 11:25:14 +0300 Subject: [PATCH 04/10] Pin CF version --- colab/GPU.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colab/GPU.ipynb b/colab/GPU.ipynb index 902df6a51..d7e5f462f 100644 --- a/colab/GPU.ipynb +++ b/colab/GPU.ipynb @@ -308,7 +308,7 @@ " !npm install\n", " !npm install -g localtunnel\n", " !npm install -g forever\n", - " !pip install flask-cloudflared\n", + " !pip install flask-cloudflared==0.0.10\n", "ii.addTask(\"Install Tavern Dependencies\", installTavernDependencies)\n", "ii.run()\n", "\n", From 48ece2a0ef0bc95af4cd4976325847ae05343d44 Mon Sep 17 00:00:00 2001 From: SillyLossy Date: Tue, 25 Apr 2023 11:31:46 +0300 Subject: [PATCH 05/10] Change CF runner --- colab/GPU.ipynb | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/colab/GPU.ipynb b/colab/GPU.ipynb index d7e5f462f..80adbbc97 100644 --- a/colab/GPU.ipynb +++ b/colab/GPU.ipynb @@ -314,20 +314,13 @@ "\n", "%env colaburl=$url\n", "%env SILLY_TAVERN_PORT=5001\n", - "from flask_cloudflared import start_cloudflared\n", "!sed -i 's/listen = true/listen = false/g' config.conf\n", "!touch stdout.log stderr.log\n", "!forever start -o stdout.log -e stderr.log server.js\n", "print(\"KoboldAI LINK:\", url, '###Extensions API LINK###', globals.extras_url, \"###SillyTavern LINK###\", sep=\"\\n\")\n", - "import inspect\n", - "import random\n", - "sig = inspect.signature(start_cloudflared)\n", - "sum = sum(1 for param in sig.parameters.values() if param.kind == param.POSITIONAL_OR_KEYWORD)\n", - "if sum > 1:\n", - " metrics_port = random.randint(8100, 9000)\n", - " start_cloudflared(5001, metrics_port)\n", - "else:\n", - " start_cloudflared(5001)\n", + "from flask_cloudflared import _run_cloudflared\n", + "cloudflare = _run_cloudflared(5001)\n", + "print(cloudflare)\n", "!tail -f stdout.log stderr.log" ] } From 2b67b0042777d7463cdda9c41fbf72644499fb6b Mon Sep 17 00:00:00 2001 From: Cohee Date: Tue, 25 Apr 2023 12:21:58 +0300 Subject: [PATCH 06/10] Remove legacy colab link from Readme --- readme.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/readme.md b/readme.md index a73f55c09..b4cda4350 100644 --- a/readme.md +++ b/readme.md @@ -23,10 +23,6 @@ Try on Colab (runs KoboldAI backend and TavernAI Extras server alongside): **This fork can be run natively on Android phones using Termux. Please refer to this guide by ArroganceComplex#2659:** From 48359e2f0a83f34f2049c487a738efa6a02ec0a4 Mon Sep 17 00:00:00 2001 From: Grzegorz Gidel Date: Mon, 24 Apr 2023 23:59:30 +0200 Subject: [PATCH 07/10] Fix scenario positioning with pinned example messages --- public/script.js | 49 ++++++++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/public/script.js b/public/script.js index b7e0fcdd6..7d80dd7d6 100644 --- a/public/script.js +++ b/public/script.js @@ -1465,12 +1465,8 @@ async function Generate(type, automatic_trigger, force_name2) { if (count_view_mes < topAnchorDepth) { storyString += appendToStoryString(charPersonality, power_user.disable_personality_formatting ? '' : name2 + "'s personality: "); } - } - if (power_user.pin_examples && main_api !== 'openai') { - for (let example of mesExamplesArray) { - storyString += appendToStoryString(example, ''); - } + storyString += appendToStoryString(Scenario, power_user.disable_scenario_formatting ? '' : 'Circumstances and context of the dialogue: '); } // Pygmalion does that anyway @@ -1577,48 +1573,46 @@ async function Generate(type, automatic_trigger, force_name2) { chat2.push(''); } - // Collect enough messages to fill the context + let examplesString = ''; let chatString = ''; + function canFitMessages() { + const encodeString = JSON.stringify(worldInfoString + storyString + examplesString + chatString + anchorTop + anchorBottom + charPersonality + promptBias + allAnchors); + return getTokenCount(encodeString, padding_tokens) < this_max_context; + } + + // Force pinned examples into the context + let pinExmString; + if (power_user.pin_examples) { + pinExmString = examplesString = mesExamplesArray.map(example => appendToStoryString(example, '')).join(''); + } + + // Collect enough messages to fill the context let arrMes = []; for (let item of chat2) { chatString = item + chatString; - const encodeString = JSON.stringify( - worldInfoString + storyString + chatString + - anchorTop + anchorBottom + - charPersonality + promptBias + allAnchors - ); - const tokenCount = getTokenCount(encodeString, padding_tokens); - if (tokenCount < this_max_context) { //(The number of tokens in the entire promt) need fix, it must count correctly (added +120, so that the description of the character does not hide) + if (canFitMessages()) { //(The number of tokens in the entire promt) need fix, it must count correctly (added +120, so that the description of the character does not hide) //if (is_pygmalion && i == chat2.length-1) item='\n'+item; arrMes[arrMes.length] = item; } else { break; } - await delay(1); //For disable slow down (encode gpt-2 need fix) } // Estimate how many unpinned example messages fit in the context let count_exm_add = 0; if (!power_user.pin_examples) { - let mesExmString = ''; for (let example of mesExamplesArray) { - mesExmString += example; - const prompt = JSON.stringify(worldInfoString + storyString + mesExmString + chatString + anchorTop + anchorBottom + charPersonality + promptBias + allAnchors); - const tokenCount = getTokenCount(prompt, padding_tokens); - if (tokenCount < this_max_context) { + examplesString += example; + if (canFitMessages()) { count_exm_add++; - await delay(1); } else { break; } + await delay(1); } } - if (!is_pygmalion && Scenario && Scenario.length > 0) { - storyString += !power_user.disable_scenario_formatting ? `Circumstances and context of the dialogue: ${Scenario}\n` : `${Scenario}\n`; - } - let mesSend = []; console.log('calling runGenerate'); await runGenerate(); @@ -1692,15 +1686,12 @@ async function Generate(type, automatic_trigger, force_name2) { }); } - let mesSendString = ''; let mesExmString = ''; + let mesSendString = ''; function setPromtString() { + mesExmString = pinExmString ?? mesExamplesArray.slice(0, count_exm_add).join(''); mesSendString = ''; - mesExmString = ''; - for (let j = 0; j < count_exm_add; j++) { - mesExmString += mesExamplesArray[j]; - } for (let j = 0; j < mesSend.length; j++) { mesSendString += mesSend[j]; From ea709d246d71caeaf6349aeb6707fcfd1fa6e174 Mon Sep 17 00:00:00 2001 From: Grzegorz Gidel Date: Wed, 26 Apr 2023 00:38:03 +0200 Subject: [PATCH 08/10] Consistent spacing between examples regardless of pinning --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 7d80dd7d6..1635d5960 100644 --- a/public/script.js +++ b/public/script.js @@ -1583,7 +1583,7 @@ async function Generate(type, automatic_trigger, force_name2) { // Force pinned examples into the context let pinExmString; if (power_user.pin_examples) { - pinExmString = examplesString = mesExamplesArray.map(example => appendToStoryString(example, '')).join(''); + pinExmString = examplesString = mesExamplesArray.join(''); } // Collect enough messages to fill the context From a9e8484111efd36a075e94a3e783343d459b2dfb Mon Sep 17 00:00:00 2001 From: Chanka0 Date: Tue, 25 Apr 2023 20:48:42 -0600 Subject: [PATCH 09/10] Remote connection troubleshooting --- readme.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index a73f55c09..44388214b 100644 --- a/readme.md +++ b/readme.md @@ -124,7 +124,7 @@ const whitelistMode = false; Save the file. Restart your TAI server. -You will now be able to connect from other devices. +You will now be able to connect from other devices. ### Managing whitelisted IPs @@ -143,6 +143,10 @@ To connect over wifi you'll need your PC's local wifi IP address - (For Windows: windows button > type 'cmd.exe' in the search bar> type 'ipconfig' in the console, hit Enter > "IPv4" listing) if you want other people on the internet to connect, check [here](https://whatismyipaddress.com/) for 'IPv4' +### Still Unable To Connect? +- Create an inbound/outbound firewall rule for the port found in `config.conf`. Do NOT mistake this for portforwarding on your router, otherwise someone could find your chat logs and that's a big no-no. +- Enable the Private Network profile type in Settings > Network and Internet > Ethernet. This is VERY important for Windows 11, otherwise you would be unable to connect even with the aforementioned firewall rules. + ## Performance issues? Try enabling the No Blur Effect (Fast UI) mode on the User settings panel. From 38929366fb26fdc23adf622667a4109e5dd34eea Mon Sep 17 00:00:00 2001 From: SillyLossy Date: Wed, 26 Apr 2023 11:55:33 +0300 Subject: [PATCH 10/10] Fix [BUG] Poe.com "Invalid or expired token" when trying to use poe.com API. Cohee1207/SillyTavern#177 --- package-lock.json | 4 ++-- package.json | 2 +- poe-client.js | 31 ++++++++++++++++++------------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 135eddfaa..5e9186e84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sillytavern", - "version": "1.4.8", + "version": "1.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sillytavern", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@dqbd/tiktoken": "^1.0.2", "axios": "^1.3.4", diff --git a/package.json b/package.json index a5138b80e..d33ae18a3 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ } }, "name": "sillytavern", - "version": "1.4.8", + "version": "1.4.9", "scripts": { "start": "node server.js" }, diff --git a/poe-client.js b/poe-client.js index ada79c402..2f8798924 100644 --- a/poe-client.js +++ b/poe-client.js @@ -319,23 +319,28 @@ class Client { throw new Error('Invalid token.'); } const botList = viewer.availableBots; - + const retries = 2; const bots = {}; for (const bot of botList.filter(x => x.deletionState == 'not_deleted')) { - const url = `https://poe.com/_next/data/${this.next_data.buildId}/${bot.displayName}.json`; - let r; - - if (this.use_cached_bots && cached_bots[url]) { - r = cached_bots[url]; + try { + const url = `https://poe.com/_next/data/${this.next_data.buildId}/${bot.displayName}.json`; + let r; + + if (this.use_cached_bots && cached_bots[url]) { + r = cached_bots[url]; + } + else { + logger.info(`Downloading ${url}`); + r = await request_with_retries(() => this.session.get(url), retries); + cached_bots[url] = r; + } + + const chatData = r.data.pageProps.payload.chatOfBotDisplayName; + bots[chatData.defaultBotObject.nickname] = chatData; } - else { - logger.info(`Downloading ${url}`); - r = await request_with_retries(() => this.session.get(url)); - cached_bots[url] = r; + catch { + console.log(`Could not load bot: ${bot.displayName}`); } - - const chatData = r.data.pageProps.payload.chatOfBotDisplayName; - bots[chatData.defaultBotObject.nickname] = chatData; } return bots;