├── 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 | |
|
|
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 |
--------------------------------------------------------------------------------