├── requirements.txt ├── scripts ├── kill.sh ├── run.sh └── setup.sh ├── utils ├── __init__.py ├── bard_utils.py └── claude_utils.py ├── docker-compose.yml ├── Dockerfile ├── config ├── config.example.yml └── __init__.py ├── LICENSE ├── .gitignore ├── README.md └── bot.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml 2 | python-telegram-bot 3 | anthropic 4 | GoogleBard 5 | -------------------------------------------------------------------------------- /scripts/kill.sh: -------------------------------------------------------------------------------- 1 | ps -ef | grep 'python3 bot.py' | awk '{print $2}' | xargs kill -9 2 | -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | source venv/bin/activate 2 | nohup python3 bot.py > bot.log 2>&1 & 3 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .bard_utils import Bard 2 | from .claude_utils import Claude 3 | 4 | 5 | def Session(mode): 6 | return Claude() if mode == "Claude" else Bard() 7 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | # create venv 2 | python3 -m venv venv 3 | 4 | # activate venv 5 | source venv/bin/activate 6 | 7 | # install dependencies 8 | pip3 install -r requirements.txt 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | claude-telegram-bot: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | volumes: 8 | - .:/app 9 | restart: unless-stopped 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | ENV PYTHONFAULTHANDLER=1 \ 4 | PYTHONUNBUFFERED=1 \ 5 | PYTHONDONTWRITEBYTECODE=1 \ 6 | PIP_DISABLE_PIP_VERSION_CHECK=on 7 | 8 | WORKDIR /app 9 | COPY . . 10 | RUN pip install -r requirements.txt --no-cache-dir 11 | 12 | CMD ["python", "bot.py"] 13 | -------------------------------------------------------------------------------- /config/config.example.yml: -------------------------------------------------------------------------------- 1 | telegram: 2 | bot_token: TELEGRAM_BOT_TOKEN 3 | user_ids: [TELEGRAM_USER_ID_1, TELEGRAM_USER_ID_2 ... TELEGRAM_USER_ID_N] 4 | claude: 5 | api: CLAUDE_API_KEY # leave it blank if you don't want to use claude 6 | bard: 7 | api: BARD_API_KEY (__Secure-1PSID, __Secure-1PSIDTS) # leave it blank if you don't want to use bard 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ciuzaak Wong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/bard_utils.py: -------------------------------------------------------------------------------- 1 | from Bard import AsyncChatbot 2 | 3 | from config import psid, psidts 4 | 5 | 6 | class Bard: 7 | def __init__(self): 8 | self.client = AsyncChatbot(psid, psidts) 9 | self.prev_conversation_id = "" 10 | self.prev_response_id = "" 11 | self.prev_choice_id = "" 12 | 13 | def reset(self): 14 | self.client.conversation_id = "" 15 | self.client.response_id = "" 16 | self.client.choice_id = "" 17 | self.prev_conversation_id = "" 18 | self.prev_response_id = "" 19 | self.prev_choice_id = "" 20 | 21 | def revert(self): 22 | self.client.conversation_id = self.prev_conversation_id 23 | self.client.response_id = self.prev_response_id 24 | self.client.choice_id = self.prev_choice_id 25 | 26 | async def send_message(self, message): 27 | if not hasattr(self.client, "SNlM0e"): 28 | self.client.SNlM0e = await self.client._AsyncChatbot__get_snlm0e() 29 | self.prev_conversation_id = self.client.conversation_id 30 | self.prev_response_id = self.client.response_id 31 | self.prev_choice_id = self.client.choice_id 32 | response = await self.client.ask(message) 33 | return response 34 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | from os import getenv, path 2 | 3 | from yaml import safe_load 4 | 5 | if not path.exists("config/config.yml"): 6 | # load env vars 7 | bot_token = getenv("BOT_TOKEN") 8 | user_ids = [int(user_id) for user_id in getenv("USER_IDS").split(",")] 9 | claude_api = getenv("CLAUDE_API") 10 | bard_api = getenv("BARD_API") 11 | else: 12 | # load yaml config 13 | with open("config/config.yml", "r") as f: 14 | config_yaml = safe_load(f) 15 | # config parameters 16 | bot_token = config_yaml["telegram"]["bot_token"] 17 | user_ids = config_yaml["telegram"]["user_ids"] 18 | claude_api = config_yaml["claude"]["api"] 19 | bard_api = config_yaml["bard"]["api"] 20 | 21 | assert bot_token is not None and user_ids is not None 22 | assert claude_api is not None or bard_api is not None 23 | 24 | if bard_api is not None: 25 | bard_api = bard_api.split(",") 26 | assert ( 27 | len(bard_api) == 2 28 | ), "Bard API must be a tuple of 2 keys (__Secure-1PSID, __Secure-1PSIDTS)" 29 | psid, psidts = bard_api[0].strip(), bard_api[1].strip() 30 | else: 31 | psid, psidts = None, None 32 | 33 | single_mode = claude_api is None or bard_api is None 34 | default_mode = "Claude" if claude_api is not None else "Bard" 35 | -------------------------------------------------------------------------------- /utils/claude_utils.py: -------------------------------------------------------------------------------- 1 | from anthropic import AI_PROMPT, HUMAN_PROMPT, AsyncAnthropic 2 | 3 | from config import claude_api 4 | 5 | 6 | class Claude: 7 | def __init__(self): 8 | self.model = "claude-2" 9 | self.temperature = 0.7 10 | self.cutoff = 50 11 | self.client = AsyncAnthropic(api_key=claude_api) 12 | self.prompt = "" 13 | 14 | def reset(self): 15 | self.prompt = "" 16 | 17 | def revert(self): 18 | self.prompt = self.prompt[: self.prompt.rfind(HUMAN_PROMPT)] 19 | 20 | def change_model(self, model): 21 | valid_models = {"claude-2", "claude-instant-1"} 22 | if model in valid_models: 23 | self.model = model 24 | return True 25 | return False 26 | 27 | def change_temperature(self, temperature): 28 | try: 29 | temperature = float(temperature) 30 | except ValueError: 31 | return False 32 | if 0 <= temperature <= 1: 33 | self.temperature = temperature 34 | return True 35 | return False 36 | 37 | def change_cutoff(self, cutoff): 38 | try: 39 | cutoff = int(cutoff) 40 | except ValueError: 41 | return False 42 | if cutoff > 0: 43 | self.cutoff = cutoff 44 | return True 45 | return False 46 | 47 | async def send_message_stream(self, message): 48 | self.prompt = f"{self.prompt}{HUMAN_PROMPT} {message}{AI_PROMPT}" 49 | response = await self.client.completions.create( 50 | prompt=self.prompt, 51 | model=self.model, 52 | temperature=self.temperature, 53 | stream=True, 54 | max_tokens_to_sample=100000, 55 | ) 56 | answer = "" 57 | async for data in response: 58 | answer = f"{answer}{data.completion}" 59 | yield answer 60 | self.prompt = f"{self.prompt}{answer}" 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | .vscode 162 | config/config.yml 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Claude & Bard Telegram Bot 2 | 3 | This is a Telegram bot that intereacts with **Anthropic Claude** and **Google Bard**. 4 | 5 | - Obtain APIs: [Claude (official)](https://console.anthropic.com/account/keys) | [Bard (reverse engineered)](https://github.com/acheong08/Bard#authentication) 6 | 7 | If you only have access to one of the models, you can still continue to use this bot. Some functions may be limited due to lack of authorization for the other model. 8 | 9 | ## Features 10 | 11 | - Support of official Claude API and reverse engineered Bard API 12 | - Support of partial Markdown formatting 13 | - Send extremely long inputs in segments 14 | - Resend the question and regenerate the answer 15 | - Private chat, group chat, independent chat session 16 | - **Claude only**: 17 | - Streaming output 18 | - Modify model's version and temperature 19 | - **Bard only**: 20 | - Toogle between different draft responses 21 | - View reference links and Google Search keywords 22 | - View images from Google Search 23 | 24 | | Claude | Bard | 25 | | :---------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------: | 26 | | ✅ Streaming output
❌ Access to the Internet | ❌ Streaming output
✅ Access to the Internet | 27 | | demo_claude | | 28 | 29 | ## Getting Started 30 | 31 | ### [Deployment (Zeabur)](https://github.com/ciuzaak/Claude-Telegram-Bot/issues/10#issue-1717101083) 32 | 33 | ### Deployment (Local) 34 | 35 | 1. Clone this repository. 36 | 37 | 2. Configure the bot in the following two ways: 38 | 1. **Create `config/config.yml`** and fill in the information with reference to `config/config.example.yml`. 39 | 2. or **Set environment variables:** 40 | 41 | ```bash 42 | export BOT_TOKEN="your bot token" 43 | export USER_IDS="user_id1, user_id2,..." 44 | export CLAUDE_API="your claude api" # ignore it if you don't want to use claude 45 | export BARD_API="__Secure-1PSID, __Secure-1PSIDTS" # ignore it if you don't want to use bard 46 | ``` 47 | 48 | - [How to obtain telegram bot token](https://core.telegram.org/bots/tutorial#obtain-your-bot-token) 49 | - [How to obtain telegram user id](https://bigone.zendesk.com/hc/en-us/articles/360008014894-How-to-get-the-Telegram-user-ID-) 50 | 51 | 3. Start the bot in the following two ways: 52 | 1. **Docker** (with docker engine pre-installed): 53 | 54 | ```bash 55 | docker compose up 56 | ``` 57 | 58 | 2. or **Scripts** (with python >= 3.8 and python3-venv pre-installed): 59 | 60 | ```bash 61 | # create the virtual environment 62 | bash scripts/setup.sh 63 | 64 | # start the bot 65 | bash scripts/run.sh 66 | ``` 67 | 68 | ### Usage 69 | 70 | #### Commands 71 | 72 | - `/id`: get your chat identifier 73 | - `/start`: start the bot and get help message 74 | - `/help`: get help message 75 | - `/reset`: reset the chat history 76 | - `/settings`: show Claude & Bard settings 77 | - `/mode`: switch between Claude and Bard 78 | - `/model NAME`: change model (**Claude only**) 79 | - **Options:** 80 | claude-2, 81 | claude-instant-1 82 | - `/temp VALUE`: set temperature (**Claude only**) 83 | - **Range:** float in [0, 1] 84 | - **Impact:** amount of randomness injected into the response 85 | - **Suggestion:** temp closer to 0 for analytical / multiple choice, and temp closer to 1 for creative and generative tasks 86 | - `/cutoff VALUE`: adjust cutoff (**Claude only**) 87 | - **Range:** int > 0 88 | - **Impact:**: smaller cutoff indicates higher frequency of streaming output 89 | - **Suggestion:** 50 for private chat, 150 for group chat 90 | - `/seg`: send messages in segments, example below: 91 | 1. Send `/seg` first 92 | 2. Paste a long text and send (or send a series of text in segments) 93 | 3. Input your other questions and send 94 | 4. Send `/seg` again 95 | 5. Bot will respond and you can continue the conversation 96 | - `/retry`: regenerate the answer. Use `/retry TEXT` to modify your last input. 97 | 98 | #### Others 99 | 100 | - `📝 View other drafts`: click to see other drafts (**Bard Only**) 101 | - `🔍 Google it`: click to view the search results (**Bard Only**) 102 | 103 | ## Acknowledgements 104 | 105 | This code is based on [Lakr233's ChatBot-TGLM6B](https://github.com/Lakr233/ChatBot-TGLM6B). 106 | 107 | The client library for Claude API is [anthropics's anthropic-sdk-python](https://github.com/anthropics/anthropic-sdk-python). 108 | 109 | The client library for Bard API is [acheong08's Bard](https://github.com/acheong08/Bard). 110 | 111 | Huge thanks to them!!! 🥰🥰🥰 112 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | from re import sub 2 | from urllib.parse import quote 3 | 4 | from telegram import ( 5 | BotCommand, 6 | InlineKeyboardButton, 7 | InlineKeyboardMarkup, 8 | InputMediaPhoto, 9 | Update, 10 | ) 11 | from telegram.constants import ParseMode 12 | from telegram.ext import ( 13 | Application, 14 | ApplicationBuilder, 15 | CallbackQueryHandler, 16 | CommandHandler, 17 | ContextTypes, 18 | MessageHandler, 19 | filters, 20 | ) 21 | 22 | from config import bot_token, default_mode, single_mode, user_ids 23 | from utils import Session 24 | 25 | 26 | def get_session(update: Update, context: ContextTypes.DEFAULT_TYPE): 27 | mode = context.chat_data.get("mode") 28 | if mode is None: 29 | mode = default_mode 30 | context.chat_data["mode"] = mode 31 | context.chat_data[mode] = {"session": Session(mode)} 32 | return mode, context.chat_data[mode]["session"] 33 | 34 | 35 | async def reset_chat(update: Update, context: ContextTypes.DEFAULT_TYPE): 36 | mode, session = get_session(update, context) 37 | session.reset() 38 | context.chat_data[mode].pop("last_msg_id", None) 39 | context.chat_data[mode].pop("last_input", None) 40 | context.chat_data[mode].pop("seg_message", None) 41 | context.chat_data[mode].pop("drafts", None) 42 | await update.message.reply_text("🧹 Chat history has been reset.") 43 | 44 | 45 | # Google bard: view other drafts 46 | async def view_other_drafts(update: Update, context: ContextTypes.DEFAULT_TYPE): 47 | last_msg_id = context.chat_data["Bard"].get("last_msg_id") 48 | if last_msg_id is not None and update.callback_query.data == f"{last_msg_id}": 49 | # increase choice index 50 | context.chat_data["Bard"]["drafts"]["index"] = ( 51 | context.chat_data["Bard"]["drafts"]["index"] + 1 52 | ) % len(context.chat_data["Bard"]["drafts"]["choices"]) 53 | await bard_response(update, context) 54 | 55 | 56 | # Google bard: response 57 | async def bard_response(update: Update, context: ContextTypes.DEFAULT_TYPE): 58 | session = context.chat_data["Bard"]["session"] 59 | message, markup, sources, choices, index = context.chat_data["Bard"][ 60 | "drafts" 61 | ].values() 62 | session.client.choice_id = choices[index]["id"] 63 | content = choices[index]["content"][0] 64 | _content = sub( 65 | r"[\_\*\[\]\(\)\~\>\#\+\-\=\|\{\}\.\!]", lambda x: f"\\{x.group(0)}", content 66 | ).replace("\\*\\*", "*") 67 | _sources = sub( 68 | r"[\_\*\[\]\(\)\~\`\>\#\+\-\=\|\{\}\.\!]", lambda x: f"\\{x.group(0)}", sources 69 | ) 70 | try: 71 | await message.edit_text( 72 | f"{_content[: 4096 - len(_sources)]}{_sources}", 73 | reply_markup=markup, 74 | parse_mode=ParseMode.MARKDOWN_V2, 75 | ) 76 | except Exception as e: 77 | if str(e).startswith("Message is not modified"): 78 | pass 79 | elif str(e).startswith("Can't parse entities"): 80 | await message.edit_text( 81 | f"{content[: 4095 - len(sources)]}.{sources}", reply_markup=markup 82 | ) 83 | else: 84 | print(f"[e] {e}") 85 | await message.edit_text(f"❌ Error orrurred: {e}. /reset") 86 | 87 | 88 | async def recv_msg(update: Update, context: ContextTypes.DEFAULT_TYPE): 89 | input_text = update.message.text 90 | if update.message.chat.type != "private": 91 | if ( 92 | update.message.reply_to_message 93 | and update.message.reply_to_message.from_user.username 94 | == context.bot.username 95 | ): 96 | pass 97 | elif update.message.entities is not None and input_text.startswith( 98 | f"@{context.bot.username}" 99 | ): 100 | input_text = input_text.lstrip(f"@{context.bot.username}").lstrip() 101 | else: 102 | return 103 | mode, session = get_session(update, context) 104 | 105 | # handle long message (for claude 100k model) 106 | seg_message = context.chat_data[mode].get("seg_message") 107 | if seg_message is None: 108 | if input_text.startswith("/seg"): 109 | input_text = input_text.lstrip("/seg").lstrip() 110 | if input_text.endswith("/seg"): 111 | input_text = input_text.rstrip("/seg").rstrip() 112 | else: 113 | context.chat_data[mode]["seg_message"] = input_text 114 | return 115 | else: 116 | if input_text.endswith("/seg"): 117 | input_text = f"{seg_message}\n\n{input_text.rstrip('/seg')}".strip() 118 | context.chat_data[mode].pop("seg_message", None) 119 | else: 120 | context.chat_data[mode]["seg_message"] = f"{seg_message}\n\n{input_text}" 121 | return 122 | 123 | # regenerate the answer 124 | if input_text.startswith("/retry"): 125 | last_input = context.chat_data[mode].get("last_input") 126 | if last_input is None: 127 | return await update.message.reply_text("❌ Empty conversation.") 128 | session.revert() 129 | input_text = input_text.lstrip("/retry").lstrip() 130 | input_text = input_text or last_input 131 | 132 | if input_text == "": 133 | return await update.message.reply_text("❌ Empty message.") 134 | message = await update.message.reply_text("Thinking...") 135 | context.chat_data[mode]["last_input"] = input_text 136 | context.chat_data[mode]["last_msg_id"] = message.message_id 137 | 138 | if mode == "Claude": 139 | prev_response = "" 140 | async for response in session.send_message_stream(input_text): 141 | response = response[:4096] 142 | if abs(len(response) - len(prev_response)) < session.cutoff: 143 | continue 144 | prev_response = response 145 | await message.edit_text(response) 146 | 147 | _response = sub( 148 | r"[\_\*\[\]\(\)\~\>\#\+\-\=\|\{\}\.\!]", 149 | lambda x: f"\\{x.group(0)}", 150 | response, 151 | ) 152 | try: 153 | await message.edit_text(_response[:4096], parse_mode=ParseMode.MARKDOWN_V2) 154 | except Exception as e: 155 | if str(e).startswith("Message is not modified"): 156 | pass 157 | elif str(e).startswith("Can't parse entities"): 158 | await message.edit_text(f"{response[:4095]}.") 159 | else: 160 | print(f"[e] {e}") 161 | await message.edit_text(f"❌ Error orrurred: {e}. /reset") 162 | 163 | else: # Bard 164 | response = await session.send_message(input_text) 165 | # get source links 166 | sources = "" 167 | if response["factualityQueries"]: 168 | links = set( 169 | item[2][0].split("//")[-1] 170 | for item in response["factualityQueries"][0] 171 | if item[2][0] != "" 172 | ) 173 | sources = "\n\nSources\n" + "\n".join( 174 | [f"{i+1}. {val}" for i, val in enumerate(links)] 175 | ) 176 | 177 | # Buttons 178 | search_url = ( 179 | quote(response["textQuery"][0]) 180 | if response["textQuery"] != "" 181 | else quote(input_text) 182 | ) 183 | search_url = f"https://www.google.com/search?q={search_url}" 184 | markup = InlineKeyboardMarkup( 185 | [ 186 | [ 187 | InlineKeyboardButton( 188 | text="📝 View other drafts", 189 | callback_data=f"{message.message_id}", 190 | ), 191 | InlineKeyboardButton(text="🔍 Google it", url=search_url), 192 | ] 193 | ] 194 | ) 195 | context.chat_data["Bard"]["drafts"] = { 196 | "message": message, 197 | "markup": markup, 198 | "sources": sources, 199 | "choices": response["choices"], 200 | "index": 0, 201 | } 202 | # get response 203 | await bard_response(update, context) 204 | # get images 205 | if len(response["images"]) != 0: 206 | captions = [ 207 | caption[1:-1] 208 | for caption in response["content"].splitlines() 209 | if caption.startswith("[Image") 210 | ] 211 | try: 212 | images = [ 213 | InputMediaPhoto(media=image, caption=captions[i]) 214 | for i, image in enumerate(response["images"]) 215 | ] 216 | await update.message.reply_media_group(images) 217 | except: 218 | images = "\n".join( 219 | [ 220 | f"{captions[i]}" 221 | for i, image in enumerate(response["images"]) 222 | ] 223 | ) 224 | await update.message.reply_text(images, parse_mode=ParseMode.HTML) 225 | 226 | 227 | async def show_settings(update: Update, context: ContextTypes.DEFAULT_TYPE): 228 | mode, session = get_session(update, context) 229 | 230 | infos = [ 231 | f"Mode: {mode}", 232 | ] 233 | extras = [] 234 | if mode == "Claude": 235 | extras = [ 236 | f"Model: {session.model}", 237 | f"Temperature: {session.temperature}", 238 | f"Cutoff: {session.cutoff}", 239 | "", 240 | "Commands:", 241 | "• /mode to use Google Bard", 242 | "• [/model NAME] to change model", 243 | "• [/temp VALUE] to set temperature", 244 | "• [/cutoff VALUE] to adjust cutoff", 245 | "Reference", 246 | ] 247 | else: # Bard 248 | extras = [ 249 | "", 250 | "Commands:", 251 | "• /mode to use Anthropic Claude", 252 | ] 253 | infos.extend(extras) 254 | await update.message.reply_text("\n".join(infos), parse_mode=ParseMode.HTML) 255 | 256 | 257 | async def change_mode(update: Update, context: ContextTypes.DEFAULT_TYPE): 258 | if single_mode: 259 | return await update.message.reply_text(f"❌ You cannot access the other mode.") 260 | mode, _ = get_session(update, context) 261 | 262 | final_mode, emoji = ("Bard", "🟠") if mode == "Claude" else ("Claude", "🟣") 263 | context.chat_data["mode"] = final_mode 264 | if final_mode not in context.chat_data: 265 | context.chat_data[final_mode] = {"session": Session(final_mode)} 266 | await update.message.reply_text( 267 | f"{emoji} Mode has been switched to {final_mode}.", 268 | parse_mode=ParseMode.HTML, 269 | ) 270 | 271 | last_msg_id = context.chat_data[final_mode].get("last_msg_id") 272 | if last_msg_id is not None: 273 | await update.message.reply_text( 274 | f"☝️ {final_mode}'s last answer. /reset", 275 | reply_to_message_id=last_msg_id, 276 | parse_mode=ParseMode.HTML, 277 | ) 278 | 279 | 280 | async def change_model(update: Update, context: ContextTypes.DEFAULT_TYPE): 281 | mode, session = get_session(update, context) 282 | 283 | if mode == "Bard": 284 | return await update.message.reply_text("❌ Invalid option for Google Bard.") 285 | if len(context.args) != 1: 286 | return await update.message.reply_text("❌ Please provide a model name.") 287 | 288 | model = context.args[0].strip() 289 | if not session.change_model(model): 290 | return await update.message.reply_text("❌ Invalid model name.") 291 | await update.message.reply_text( 292 | f"🤖 Model has been switched to {model}.", parse_mode=ParseMode.HTML 293 | ) 294 | 295 | 296 | async def change_temperature(update: Update, context: ContextTypes.DEFAULT_TYPE): 297 | mode, session = get_session(update, context) 298 | 299 | if mode == "Bard": 300 | return await update.message.reply_text("❌ Invalid option for Google Bard.") 301 | if len(context.args) != 1: 302 | return await update.message.reply_text("❌ Please provide a temperature value.") 303 | 304 | temperature = context.args[0].strip() 305 | if not session.change_temperature(temperature): 306 | return await update.message.reply_text("❌ Invalid temperature value.") 307 | await update.message.reply_text( 308 | f"🌡️ Temperature has been set to {temperature}.", 309 | parse_mode=ParseMode.HTML, 310 | ) 311 | 312 | 313 | async def change_cutoff(update: Update, context: ContextTypes.DEFAULT_TYPE): 314 | mode, session = get_session(update, context) 315 | 316 | if mode == "Bard": 317 | return await update.message.reply_text("❌ Invalid option for Google Bard.") 318 | if len(context.args) != 1: 319 | return await update.message.reply_text("❌ Please provide a cutoff value.") 320 | 321 | cutoff = context.args[0].strip() 322 | if not session.change_cutoff(cutoff): 323 | return await update.message.reply_text("❌ Invalid cutoff value.") 324 | await update.message.reply_text( 325 | f"✂️ Cutoff has been set to {cutoff}.", parse_mode=ParseMode.HTML 326 | ) 327 | 328 | 329 | async def start_bot(update: Update, context: ContextTypes.DEFAULT_TYPE): 330 | welcome_strs = [ 331 | "Welcome to Claude & Bard Telegram Bot", 332 | "", 333 | "Commands:", 334 | "• /id to get your chat identifier", 335 | "• /reset to reset the chat history", 336 | "• /retry to regenerate the answer", 337 | "• /seg to send message in segments", 338 | "• /mode to switch between Claude & Bard", 339 | "• /settings to show Claude & Bard settings", 340 | ] 341 | print(f"[i] {update.effective_user.username} started the bot") 342 | await update.message.reply_text("\n".join(welcome_strs), parse_mode=ParseMode.HTML) 343 | 344 | 345 | async def send_id(update: Update, context: ContextTypes.DEFAULT_TYPE): 346 | await update.message.reply_text( 347 | f"Your chat identifier is `{update.effective_chat.id}`, send it to the bot admin to get access\\.", 348 | parse_mode=ParseMode.MARKDOWN_V2, 349 | ) 350 | 351 | 352 | async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): 353 | print(f"[e] {context.error}") 354 | await update.message.reply_text(f"❌ Error orrurred: {context.error}. /reset") 355 | 356 | 357 | async def post_init(application: Application): 358 | await application.bot.set_my_commands( 359 | [ 360 | BotCommand("/reset", "Reset the chat history"), 361 | BotCommand("/retry", "Regenerate the answer"), 362 | BotCommand("/seg", "Send message in segments"), 363 | BotCommand("/mode", "Switch between Claude & Bard"), 364 | BotCommand("/settings", "Show Claude & Bard settings"), 365 | BotCommand("/help", "Get help message"), 366 | ] 367 | ) 368 | 369 | 370 | def run_bot(): 371 | print(f"[+] bot started, calling loop!") 372 | application = ( 373 | ApplicationBuilder() 374 | .token(bot_token) 375 | .post_init(post_init) 376 | .concurrent_updates(True) 377 | .build() 378 | ) 379 | 380 | user_filter = filters.Chat(chat_id=user_ids) 381 | msg_filter = filters.TEXT 382 | 383 | handler_list = [ 384 | CommandHandler("id", send_id), 385 | CommandHandler("start", start_bot), 386 | CommandHandler("help", start_bot), 387 | CommandHandler("reset", reset_chat, user_filter), 388 | CommandHandler("settings", show_settings, user_filter), 389 | CommandHandler("mode", change_mode, user_filter), 390 | CommandHandler("model", change_model, user_filter), 391 | CommandHandler("temp", change_temperature, user_filter), 392 | CommandHandler("cutoff", change_cutoff, user_filter), 393 | MessageHandler(user_filter & msg_filter, recv_msg), 394 | CallbackQueryHandler(view_other_drafts), 395 | ] 396 | for handler in handler_list: 397 | application.add_handler(handler) 398 | application.add_error_handler(error_handler) 399 | 400 | application.run_polling(drop_pending_updates=True) 401 | 402 | 403 | if __name__ == "__main__": 404 | run_bot() 405 | --------------------------------------------------------------------------------