Telegram Conversation Summarizer Bot with ChatGPT and Flask (Quart)
Tags: python, flask, webdev, tutorial
Published: 2023-04-21 12:00:00

Everybody talks about ChatGPT right now. The exceptionally smart AI keeps dazzling the internet even a couple of months after its release. Having the ChatGPT available on the website is awesome, however, the real fun begins when you gain API access. This gives you a great opportunity to integrate smart AI into your projects and applications to make them more powerful and introduce amazing features.

This article brings you a guide on how to create your own Telegram bot and integrate ChatGPT with it, using the OpenAI's Python library. This may sound like the simplest thing to do, but let's spice things up a bit by introducing the summarize command to get you a summary of several posts in the chat.

---
The post assumes you have basic knowledge of Python. However, I recommend checking out Hyperskill's [Python](https://hyperskill.org/tracks/2?utm_source=medium_hs&utm_medium=social&utm_campaign=chatgpt&utm_term=20.04.2023) and [Flask](https://hyperskill.org/tracks/29?utm_source=medium_hs&utm_medium=social&utm_campaign=chatgpt&utm_term=20.04.2023) tracks to learn more about Python and developing web applications with Flask.

## Setting everything up

Before jumping right into the code, you'll need to do some preparations to get all the required accesses.
1. Register your bot in Telegram and retrieve the Telegram access token (using @BotFather in Telegram)
2. Get access to the [Telegram Core API](https://my.telegram.org/auth) and retrieve `api_hash` and `app_id`.
3. Sign up to [OpenAI](ttps://platform.openai.com/) and retrieve the API access token.

Save those secret strings and guard them with your life. No stranger should get access to them: this may lead to security violations.

## Writing the skeleton

_Note_: the full final project code (split into stages with commits) is available on my GitHub, please refer here for details: https://github.com/yellalena/telegram-gpt-summarizer

_Python libraries you need to install for this step: flask, pydantic, requests, and pyngrok._

Let's begin with writing the code for the very basic Telegram bot. It should receive messages from chat and be able to respond to them. 
First thing first - create a directory for your project and initialize a Python virtual environment. By the way, if you use PyCharm, it will create a virtual environment for you.

![PyCharm screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fkl10ma3mai74jcl3v2c.png)

At this stage, the goal is split into four parts:
1. Create a simple Flask app with one root route to handle webhook with telegram messages.
2. Create a class for the Telegram bot and make it able to send messages to a chat.
3. Make the application visible to the big internet.
4. Register the application address in Telegram.

This is what _main.py_ looks like at this point:

```python
app = Flask(__name__)

@app.route('/', methods=["GET", "POST"])
def handle_webhook():
    update = Update(**request.json)
    chat_id = update.message.chat.id

    response = f"This is a response for message: {update.message.text}"
    app.bot.send_message(chat_id, response)

    return "OK", 200

def run_ngrok(port=8000):
    http_tunnel = ngrok.connect(port)
    return http_tunnel.public_url

def main():
    app.bot = TelegramBot(Config.TELEGRAM_TOKEN)
    host = run_ngrok(Config.PORT)
    app.bot.set_webhook(host)
    app.run(port=Config.PORT, debug=True, use_reloader=False)

if __name__ == "__main__":
    main()
```
Few things need clarification:
* I like to put all configuration things in one place, so I created the _config.py_ file, which will be gathering and storing our tokens and other useful information from exported environment variables.
* Telegram sends updates as a nested JSON, so let's create a set of pydantic models to parse the input to be more convenient later.
* To expose the application to the web, I use ngrok. It makes your localhost's specific port visible to everyone else, giving it a temporary public address. This is why it's important to make sure you expose the same port you're running your Flask app on.
* Finally, I initialize the bot and set a webhook to the ngrok's public URL, so that the bot knows it has to communicate to this URL whenever it receives any message.

To set up a webhook, you will need to send a request to your bot's telegram API address, built using your acquired secret token. The telegram bot code looks as follows:
```python
import requests
from config import Config

class TelegramBot:
    def __init__(self, token):
        self.token = token
        self.bot_api_url = f"{Config.TELEGRAM_API}/bot{self.token}"

    def set_webhook(self, host):
        host = host.replace("http", "https")
        set_webhook_url = f"{self.bot_api_url}/setWebhook?url={host}"
        response = requests.get(set_webhook_url)
        response.raise_for_status()

    def send_message(self, chat_id, message):
        send_message_url = f"{self.bot_api_url}/sendMessage"
        response = requests.post(send_message_url, json={"chat_id": chat_id,
                                                          "text": message})
        response.raise_for_status()
```
Now that everything is ready (don't forget I omitted some of the basic code, you can find it in the repo), export your bot token in an environment variable and hit that "Run"!

![Telegram chat screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hpftf0yg66jum85bxdl9.png)
Yay! It's alive!

## Adding a brain

Amazing, the next step now should be adding a pinch of intelligence to our smart bot. Install an official OpenAI's lib for Python using pip: `pip install openai`.
After that, we will be able to create a helper class to communicate with the AI.

```python
import openai

class OpenAiHelper:
    def __init__(self, token, model="gpt-3.5-turbo"):
        openai.api_key = token
        self.model = model

    def get_response(self, message_text):
        response = openai.ChatCompletion.create(model=self.model,
                                                messages=[{"role": "user", "content": message_text}])
        return response.choices[0].message.content
```

The API [suggests](https://platform.openai.com/docs/models/overview) a variety of models to use for your project. The most popular are, of course, the GPTs. GPT-4 is the one that made the most noise lately, but (and because of that) it has limited access now, so for easier testing purposes, I choose GPT-3 instead. No big deal, you can always choose whichever you like the most, just change the string name you pass to the helper.

Don't forget to add `OPENAI_TOKEN` property to the config and let's use the helper in the code.

First, of course, instantiate it in the `main()` method:

![Initializing the OpenAI helper in main](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p1f3de8zn1abu4lc36g6.png)

And then call this guy from the view function, just like that:

```python
response = app.openai_helper.get_response(update.message.text)
```
Whoosh! The magic is happening!

![Telegram chat screenshot with OpenAI response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f32ivkvhncjpb0tkfs1n.png)

## Summarize it!
_Python libraries to install for this step: quart, telethon._

I bet you've been there - you have been added to a chat with a group of friends who like to discuss interesting things or share some news or ideas. You've had a lot of stuff to do and you've missed all the fun in the chat. Next thing you see - a hundred unread messages there. Wouldn't it be nice if someone could give you a brief overview of what happened there instead of reading all that? Well, GPT can do that, of course. We only need to ask it.

This is where the fun begins. For some reason, Telegram's bot API doesn't allow bots to read conversation history. We have webhooks, and we have the explicit GetUpdates() method, but they only work if someone has mentioned the bot. Another option is to make the bot get all the updates if it's added as an admin, but this approach has a few cons as well. First, you would need to set up whole storage for the messages. Second, what if you want to summarize the conversation that was going on before the bot was added to the chat? Not our case.

Obviously, that's not the reason to give up. Telegram provides the Core API, and this one can help with retrieving a chat history. The only thing is that it's asynchronous. And the most popular Python library for it, Telethon, is asynchronous, too. And Flask is synchronous. Uh-oh.

And that's where the mysterious [Quart](https://pgjones.gitlab.io/quart/tutorials/quickstart.html) mentioned in the title comes onto the stage. Quart is the Flask API re-implemented using async, await, and ASGI web server (rather than synchronous and WSGI). Its main advantage in our case is that the syntax is basically the same. Let's do a quick code reorganization.

The changes are simple. First, adjust imports and change every _Flask_ to _Quart_:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w7r5vc1ckx1fj4qqpqcj.png)

Then, make all web application's methods async. And await all the properties and methods that have become asynchronous now:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q6lb038yazuqd32llzuc.png)

If you're not sure about what's async Python, I encourage you to check this part of Telethon [documentation on the asyncio basics](https://docs.telethon.dev/en/stable/concepts/asyncio.html).

I have also moved ngrok and TelegramBot to launch them in a separate method decorated with `@app.before_serving`. This is a Quart built-in decorator that will ensure everything inside this method will run before the web app is up and serving. It's required so that the bot and the helper are both initialized in the same event loop as the main application.

```python
@app.before_serving
async def startup():
    host = run_ngrok(Config.PORT)
    app.bot = TelegramBot(Config.TELEGRAM_TOKEN)
    app.bot.set_webhook(host)
    app.openai_helper = OpenAiHelper(Config.OPENAI_TOKEN)
```

Running the app has slightly changed as well, but not much. Hypercorn is the ASGI server used to run Quart asynchronously, and if we want to specify an application's port, we need to do it in a config. Note that the `main()` is now also async and is run using asyncio:
```python
async def main():
    quart_cfg = hypercorn.Config()
    quart_cfg.bind = [f"127.0.0.1:{Config.PORT}"]
    await hypercorn.asyncio.serve(app, quart_cfg)

if __name__ == "__main__":
    asyncio.run(main())
```
That's it. Let's check if the changes went smoothly for our bot. Run, text, enter:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/inid8ehg07sfi7adjf7r.png)

It's speaking. Great. Now, get the chat history for AI to summarize. Let's use Telegram's Core API with the help of Telethon lib. There, we will need the last two secret strings you have - export them as environment variables too.

TelegramBot has slight changes in the `__init__` method: it will need to have a new `core_api_client` property which initializes a Telethon's client and of course, you need to pass the Core API secrets as arguments.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hp9z3k1nooimp6i5smfm.png)

And this tiny method will be responsible for retrieving the history:
```python
async def get_chat_history(self, chat_id, limit=30):
        if not self.core_api_client:
            return []
        history = await self.core_api_client.get_messages(chat_id, limit)
        result = [f"{message.sender.first_name} {message.sender.last_name}: {message.message} \n"
                  for message in history if not message.action]
        result.reverse()
        return '\n'.join(result)
```
Telethon's get_messages has many more different parameters you can pass beside the `limit` ones. For example, it can reverse the history, or limit it by date instead of the number of messages. It's fun to play with and you can adjust your own bot in any way you'd love to.

Well, we're almost done. The final touch is to add a summarization option to the webhook handler. This is what getting an answer looks like:

```python
# process "summarize" command
    if update.message.text.startswith("/summarize"):
        history = await app.bot.get_chat_history(chat_id)
        response = app.openai_helper.get_response("Please, briefly summarize the following conversation history:\n" +\
                                                  history)
    else:
        response = app.openai_helper.get_response(update.message.text)

    app.bot.send_message(chat_id, response)
```
Let's see it blooming!

After you run the application for the first time, it will ask you to log in to Telegram. That's ok: it's required to get access to message history and other private data the Core API has to offer us. Enter the same phone number you used to get access to Telegram Core API. You will receive a verification code inside your app, and after that you're good to go.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jctn0wyfbrwmlnlddy9m.png)

Add the bot to a conversation with friends and ask for a summary:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/208g8h5fqyd5k2bmajnd.png)

That's it! There's an endless list of things you can continue with: add processing other types of messages besides text, configure a number of messages to summarize from the chat, etc. Go for it and don't forget to push your code onto GitHub. Happy coding! :)

Don't forget to jump onto [Hyperskill](https://hyperskill.org/tracks?utm_source=medium_hs&utm_medium=social&utm_campaign=chatgpt&utm_term=20.04.2023) website to keep learning about developing web applications with Python and Flask. Here are links to some topics that you might find useful exactly for this project:
* [Error handlers](https://hyperskill.org/learn/step/17366?utm_source=medium_hs&utm_medium=social&utm_campaign=chatgpt&utm_term=20.04.2023): If you don't handle errors properly, there is a high chance of your application failing in the runtime or showing ugly tracebacks to the user. To avoid that, read about types of errors and how it's best to handle them in a Flask app.
* [Logging](https://hyperskill.org/learn/step/17609?utm_source=medium_hs&utm_medium=social&utm_campaign=chatgpt&utm_term=20.04.2023): That's one of the most important things when it comes to testing and debugging your application. Writing meaningful and readable logs is a must for a software developer. Check this topic to learn how to perform logging in Python.
* [Intro to SQLAlchemy](https://hyperskill.org/learn/step/12935?utm_source=medium_hs&utm_medium=social&utm_campaign=chatgpt&utm_term=20.04.2023): When you decide you want to store some application data, whether it be any user info or conversation history, you will need to communicate to the database. This topic introduces you to the basics of SQLAlchemy which makes working with databases easy and convenient.

Hyperskill is a project-based learning platform that offers a personalized curriculum and a variety of tracks to help people from different backgrounds gain market-relevant skills through online education. It's not only giving you solid pieces of theory but allows you to practice the skills right away - and practice makes learning perfect.

---
Did you find this post helpful? Hit clap and follow Hyperskill and me to read more of them later :)