From 97f58b30ff1191a755f91369602cbcce61483297 Mon Sep 17 00:00:00 2001 From: ned Date: Wed, 14 Jun 2023 19:46:02 +0200 Subject: [PATCH 01/52] initial functions support and added weather func --- README.md | 76 +++++++++++++++++++--------------- bot/functions.py | 23 +++++++++++ bot/main.py | 12 ++++-- bot/openai_helper.py | 97 +++++++++++++++++++++++++++++++++++++++----- plugins/weather.py | 47 +++++++++++++++++++++ requirements.txt | 3 +- 6 files changed, 209 insertions(+), 49 deletions(-) create mode 100644 bot/functions.py create mode 100644 plugins/weather.py diff --git a/README.md b/README.md index b525326b..e03cf98a 100644 --- a/README.md +++ b/README.md @@ -15,19 +15,23 @@ A [Telegram bot](https://core.telegram.org/bots/api) that integrates with OpenAI - [x] Typing indicator while generating a response - [x] Access can be restricted by specifying a list of allowed users - [x] Docker and Proxy support -- [x] (NEW!) Image generation using DALL·E via the `/image` command -- [x] (NEW!) Transcribe audio and video messages using Whisper (may require [ffmpeg](https://ffmpeg.org)) -- [x] (NEW!) Automatic conversation summary to avoid excessive token usage -- [x] (NEW!) Track token usage per user - by [@AlexHTW](https://github.com/AlexHTW) -- [x] (NEW!) Get personal token usage statistics and cost per day/month via the `/stats` command - by [@AlexHTW](https://github.com/AlexHTW) -- [x] (NEW!) User budgets and guest budgets - by [@AlexHTW](https://github.com/AlexHTW) -- [x] (NEW!) Stream support -- [x] (NEW!) GPT-4 support +- [x] Image generation using DALL·E via the `/image` command +- [x] Transcribe audio and video messages using Whisper (may require [ffmpeg](https://ffmpeg.org)) +- [x] Automatic conversation summary to avoid excessive token usage +- [x] Track token usage per user - by [@AlexHTW](https://github.com/AlexHTW) +- [x] Get personal token usage statistics and cost per day/month via the `/stats` command - by [@AlexHTW](https://github.com/AlexHTW) +- [x] User budgets and guest budgets - by [@AlexHTW](https://github.com/AlexHTW) +- [x] Stream support +- [x] GPT-4 support - If you have access to the GPT-4 API, simply change the `OPENAI_MODEL` parameter to `gpt-4` -- [x] (NEW!) Localized bot language +- [x] Localized bot language - Available languages :gb: :de: :ru: :tr: :it: :finland: :es: :indonesia: :netherlands: :cn: :taiwan: :vietnam: :iran: :brazil: :ukraine: -- [x] (NEW!) Improved inline queries support for group and private chats - by [@bugfloyd](https://github.com/bugfloyd) +- [x] Improved inline queries support for group and private chats - by [@bugfloyd](https://github.com/bugfloyd) - To use this feature, enable inline queries for your bot in BotFather via the `/setinline` [command](https://core.telegram.org/bots/inline) +- [x] (NEW!) Support *new models* [announced on June 13, 2023](https://openai.com/blog/function-calling-and-other-api-updates) +- [x] (NEW!) Support *functions* (plugins) to extend the bot's functionality with 3rd party services + - Currently available functions: + - Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) ## Additional features - help needed! If you'd like to help, check out the [issues](https://github.com/n3d1117/chatgpt-telegram-bot/issues) section and contribute! @@ -68,29 +72,35 @@ The following parameters are optional and can be set in the `.env` file: Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/discussions/184) for possible budget configurations. #### Additional optional configuration options -| Parameter | Description | Default value | -|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------| -| `ENABLE_QUOTING` | Whether to enable message quoting in private chats | `true` | -| `ENABLE_IMAGE_GENERATION` | Whether to enable image generation via the `/image` command | `true` | -| `ENABLE_TRANSCRIPTION` | Whether to enable transcriptions of audio and video messages | `true` | -| `PROXY` | Proxy to be used for OpenAI and Telegram bot (e.g. `http://localhost:8080`) | - | -| `OPENAI_MODEL` | The OpenAI model to use for generating responses. You can find all available models [here](https://platform.openai.com/docs/models/) | `gpt-3.5-turbo` | -| `ASSISTANT_PROMPT` | A system message that sets the tone and controls the behavior of the assistant | `You are a helpful assistant.` | -| `SHOW_USAGE` | Whether to show OpenAI token usage information after each response | `false` | -| `STREAM` | Whether to stream responses. **Note**: incompatible, if enabled, with `N_CHOICES` higher than 1 | `true` | -| `MAX_TOKENS` | Upper bound on how many tokens the ChatGPT API will return | `1200` for GPT-3, `2400` for GPT-4 | -| `MAX_HISTORY_SIZE` | Max number of messages to keep in memory, after which the conversation will be summarised to avoid excessive token usage | `15` | -| `MAX_CONVERSATION_AGE_MINUTES` | Maximum number of minutes a conversation should live since the last message, after which the conversation will be reset | `180` | -| `VOICE_REPLY_WITH_TRANSCRIPT_ONLY` | Whether to answer to voice messages with the transcript only or with a ChatGPT response of the transcript | `false` | -| `VOICE_REPLY_PROMPTS` | A semicolon separated list of phrases (i.e. `Hi bot;Hello chat`). If the transcript starts with any of them, it will be treated as a prompt even if `VOICE_REPLY_WITH_TRANSCRIPT_ONLY` is set to `true` | - | -| `N_CHOICES` | Number of answers to generate for each input message. **Note**: setting this to a number higher than 1 will not work properly if `STREAM` is enabled | `1` | -| `TEMPERATURE` | Number between 0 and 2. Higher values will make the output more random | `1.0` | -| `PRESENCE_PENALTY` | Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far | `0.0` | -| `FREQUENCY_PENALTY` | Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far | `0.0` | -| `IMAGE_SIZE` | The DALL·E generated image size. Allowed values: `256x256`, `512x512` or `1024x1024` | `512x512` | -| `GROUP_TRIGGER_KEYWORD` | If set, the bot in group chats will only respond to messages that start with this keyword | - | -| `IGNORE_GROUP_TRANSCRIPTIONS` | If set to true, the bot will not process transcriptions in group chats | `true` | -| `BOT_LANGUAGE` | Language of general bot messages. Currently available: `en`, `de`, `ru`, `tr`, `it`, `fi`, `es`, `id`, `nl`, `zh-cn`, `zh-tw`, `vi`, `fa`, `pt-br`, `uk`. [Contribute with additional translations](https://github.com/n3d1117/chatgpt-telegram-bot/discussions/219) | `en` | +| Parameter | Description | Default value | +|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| +| `ENABLE_QUOTING` | Whether to enable message quoting in private chats | `true` | +| `ENABLE_IMAGE_GENERATION` | Whether to enable image generation via the `/image` command | `true` | +| `ENABLE_TRANSCRIPTION` | Whether to enable transcriptions of audio and video messages | `true` | +| `PROXY` | Proxy to be used for OpenAI and Telegram bot (e.g. `http://localhost:8080`) | - | +| `OPENAI_MODEL` | The OpenAI model to use for generating responses. You can find all available models [here](https://platform.openai.com/docs/models/) | `gpt-3.5-turbo` | +| `ASSISTANT_PROMPT` | A system message that sets the tone and controls the behavior of the assistant | `You are a helpful assistant.` | +| `SHOW_USAGE` | Whether to show OpenAI token usage information after each response | `false` | +| `STREAM` | Whether to stream responses. **Note**: incompatible, if enabled, with `N_CHOICES` higher than 1 | `true` | +| `MAX_TOKENS` | Upper bound on how many tokens the ChatGPT API will return | `1200` for GPT-3, `2400` for GPT-4 | +| `MAX_HISTORY_SIZE` | Max number of messages to keep in memory, after which the conversation will be summarised to avoid excessive token usage | `15` | +| `MAX_CONVERSATION_AGE_MINUTES` | Maximum number of minutes a conversation should live since the last message, after which the conversation will be reset | `180` | +| `VOICE_REPLY_WITH_TRANSCRIPT_ONLY` | Whether to answer to voice messages with the transcript only or with a ChatGPT response of the transcript | `false` | +| `VOICE_REPLY_PROMPTS` | A semicolon separated list of phrases (i.e. `Hi bot;Hello chat`). If the transcript starts with any of them, it will be treated as a prompt even if `VOICE_REPLY_WITH_TRANSCRIPT_ONLY` is set to `true` | - | +| `N_CHOICES` | Number of answers to generate for each input message. **Note**: setting this to a number higher than 1 will not work properly if `STREAM` is enabled | `1` | +| `TEMPERATURE` | Number between 0 and 2. Higher values will make the output more random | `1.0` | +| `PRESENCE_PENALTY` | Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far | `0.0` | +| `FREQUENCY_PENALTY` | Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far | `0.0` | +| `IMAGE_SIZE` | The DALL·E generated image size. Allowed values: `256x256`, `512x512` or `1024x1024` | `512x512` | +| `GROUP_TRIGGER_KEYWORD` | If set, the bot in group chats will only respond to messages that start with this keyword | - | +| `IGNORE_GROUP_TRANSCRIPTIONS` | If set to true, the bot will not process transcriptions in group chats | `true` | +| `BOT_LANGUAGE` | Language of general bot messages. Currently available: `en`, `de`, `ru`, `tr`, `it`, `fi`, `es`, `id`, `nl`, `zh-cn`, `zh-tw`, `vi`, `fa`, `pt-br`, `uk`. [Contribute with additional translations](https://github.com/n3d1117/chatgpt-telegram-bot/discussions/219) | `en` | + +#### Functions +| Parameter | Description | Default value | +|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| +| `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | +| `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model before displaying a user-facing message | `10` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. diff --git a/bot/functions.py b/bot/functions.py new file mode 100644 index 00000000..50d90bf2 --- /dev/null +++ b/bot/functions.py @@ -0,0 +1,23 @@ +import json + +from plugins.weather import weather_function_spec, get_current_weather + + +def get_functions_specs(): + """ + Return the list of function specs that can be called by the model + """ + return [ + weather_function_spec(), + ] + + +async def call_function(function_name, arguments): + """ + Call a function based on the name and parameters provided + """ + if function_name == "get_current_weather": + arguments = json.loads(arguments) + return await get_current_weather(arguments["location"], arguments["unit"]) + + raise Exception(f"Function {function_name} not found") diff --git a/bot/main.py b/bot/main.py index dcc4400b..71bd43d5 100644 --- a/bot/main.py +++ b/bot/main.py @@ -3,7 +3,7 @@ from dotenv import load_dotenv -from openai_helper import OpenAIHelper, default_max_tokens +from openai_helper import OpenAIHelper, default_max_tokens, are_functions_available from telegram_bot import ChatGPTTelegramBot @@ -27,6 +27,7 @@ def main(): # Setup configurations model = os.environ.get('OPENAI_MODEL', 'gpt-3.5-turbo') + functions_available = are_functions_available(model=model) max_tokens_default = default_max_tokens(model=model) openai_config = { 'api_key': os.environ['OPENAI_API_KEY'], @@ -41,14 +42,17 @@ def main(): 'temperature': float(os.environ.get('TEMPERATURE', 1.0)), 'image_size': os.environ.get('IMAGE_SIZE', '512x512'), 'model': model, + 'enable_functions': os.environ.get('ENABLE_FUNCTIONS', str(functions_available)).lower() == 'true', + 'functions_max_consecutive_calls': int(os.environ.get('FUNCTIONS_MAX_CONSECUTIVE_CALLS', 10)), 'presence_penalty': float(os.environ.get('PRESENCE_PENALTY', 0.0)), 'frequency_penalty': float(os.environ.get('FREQUENCY_PENALTY', 0.0)), 'bot_language': os.environ.get('BOT_LANGUAGE', 'en'), } - # log deprecation warning for old budget variable names - # old variables are caught in the telegram_config definition for now - # remove support for old budget names at some point in the future + if openai_config['enable_functions'] and not functions_available: + logging.error(f'ENABLE_FUNCTIONS is set to true, but the model {model} does not support it. ' + f'Please set ENABLE_FUNCTIONS to false or use a model that supports it.') + exit(1) if os.environ.get('MONTHLY_USER_BUDGETS') is not None: logging.warning('The environment variable MONTHLY_USER_BUDGETS is deprecated. ' 'Please use USER_BUDGETS with BUDGET_PERIOD instead.') diff --git a/bot/openai_helper.py b/bot/openai_helper.py index 80e41183..b94ab0a5 100644 --- a/bot/openai_helper.py +++ b/bot/openai_helper.py @@ -14,6 +14,8 @@ from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type +from bot.functions import get_functions_specs, call_function + # Models can be found here: https://platform.openai.com/docs/models/overview GPT_3_MODELS = ("gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613") GPT_3_16K_MODELS = ("gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613") @@ -39,6 +41,19 @@ def default_max_tokens(model: str) -> int: return base * 8 +def are_functions_available(model: str) -> bool: + """ + Whether the given model supports functions + """ + # Deprecated models + if model in ("gpt-3.5-turbo-0301", "gpt-4-0314", "gpt-4-32k-0314"): + return False + # Stable models will be updated to support functions on June 27, 2023 + if model in ("gpt-3.5-turbo", "gpt-4", "gpt-4-32k"): + return datetime.date.today() > datetime.date(2023, 6, 27) + return True + + # Load translations parent_dir_path = os.path.join(os.path.dirname(__file__), os.pardir) translations_file_path = os.path.join(parent_dir_path, 'translations.json') @@ -98,6 +113,8 @@ async def get_chat_response(self, chat_id: int, query: str) -> tuple[str, str]: :return: The answer from the model and the number of tokens used """ response = await self.__common_get_chat_response(chat_id, query) + if self.config['enable_functions']: + response = await self.__handle_function_call(chat_id, response) answer = '' if len(response.choices) > 1 and self.config['n_choices'] > 1: @@ -129,13 +146,15 @@ async def get_chat_response_stream(self, chat_id: int, query: str): :return: The answer from the model and the number of tokens used, or 'not_finished' """ response = await self.__common_get_chat_response(chat_id, query, stream=True) + if self.config['enable_functions']: + response = await self.__handle_function_call(chat_id, response, stream=True) answer = '' async for item in response: if 'choices' not in item or len(item.choices) == 0: continue delta = item.choices[0].delta - if 'content' in delta: + if 'content' in delta and delta.content is not None: answer += delta.content yield answer, 'not_finished' answer = answer.strip() @@ -186,16 +205,22 @@ async def __common_get_chat_response(self, chat_id: int, query: str, stream=Fals logging.warning(f'Error while summarising chat history: {str(e)}. Popping elements instead...') self.conversations[chat_id] = self.conversations[chat_id][-self.config['max_history_size']:] - return await openai.ChatCompletion.acreate( - model=self.config['model'], - messages=self.conversations[chat_id], - temperature=self.config['temperature'], - n=self.config['n_choices'], - max_tokens=self.config['max_tokens'], - presence_penalty=self.config['presence_penalty'], - frequency_penalty=self.config['frequency_penalty'], - stream=stream - ) + common_args = { + 'model': self.config['model'], + 'messages': self.conversations[chat_id], + 'temperature': self.config['temperature'], + 'n': self.config['n_choices'], + 'max_tokens': self.config['max_tokens'], + 'presence_penalty': self.config['presence_penalty'], + 'frequency_penalty': self.config['frequency_penalty'], + 'stream': stream + } + + if self.config['enable_functions']: + common_args['functions'] = get_functions_specs() + common_args['function_call'] = 'auto' + + return await openai.ChatCompletion.acreate(**common_args) except openai.error.RateLimitError as e: raise e @@ -206,6 +231,50 @@ async def __common_get_chat_response(self, chat_id: int, query: str, stream=Fals except Exception as e: raise Exception(f"⚠️ _{localized_text('error', bot_language)}._ ⚠️\n{str(e)}") from e + async def __handle_function_call(self, chat_id, response, stream=False, times=0): + function_name = '' + arguments = '' + if stream: + async for item in response: + if 'choices' in item and len(item.choices) > 0: + first_choice = item.choices[0] + if 'delta' in first_choice \ + and 'function_call' in first_choice.delta: + if 'name' in first_choice.delta.function_call: + function_name += first_choice.delta.function_call.name + if 'arguments' in first_choice.delta.function_call: + arguments += str(first_choice.delta.function_call.arguments) + elif 'finish_reason' in first_choice and first_choice.finish_reason == 'function_call': + break + else: + return response + else: + return response + else: + if 'choices' in response and len(response.choices) > 0: + first_choice = response.choices[0] + if 'function_call' in first_choice.message: + if 'name' in first_choice.message.function_call: + function_name += first_choice.message.function_call.name + if 'arguments' in first_choice.message.function_call: + arguments += str(first_choice.message.function_call.arguments) + else: + return response + else: + return response + + logging.info(f'Calling function {function_name}...') + function_response = await call_function(function_name, arguments) + self.__add_function_call_to_history(chat_id=chat_id, function_name=function_name, content=function_response) + response = await openai.ChatCompletion.acreate( + model=self.config['model'], + messages=self.conversations[chat_id], + functions=get_functions_specs(), + function_call='auto' if times < self.config['functions_max_consecutive_calls'] else 'none', + stream=stream + ) + return await self.__handle_function_call(chat_id, response, stream, times+1) + async def generate_image(self, prompt: str) -> tuple[str, str]: """ Generates an image from the given prompt using DALL·E model. @@ -264,6 +333,12 @@ def __max_age_reached(self, chat_id) -> bool: max_age_minutes = self.config['max_conversation_age_minutes'] return last_updated < now - datetime.timedelta(minutes=max_age_minutes) + def __add_function_call_to_history(self, chat_id, function_name, content): + """ + Adds a function call to the conversation history + """ + self.conversations[chat_id].append({"role": "function", "name": function_name, "content": content}) + def __add_to_history(self, chat_id, role, content): """ Adds a message to the conversation history. diff --git a/plugins/weather.py b/plugins/weather.py new file mode 100644 index 00000000..33382440 --- /dev/null +++ b/plugins/weather.py @@ -0,0 +1,47 @@ +import json + +import requests +from geopy import Nominatim + + +def weather_function_spec(): + return { + "name": "get_current_weather", + "description": "Get the current and 7-day daily weather forecast for a location using Open Meteo APIs.", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The exact city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the provided location.", + }, + }, + "required": ["location", "unit"], + } + } + + +async def get_current_weather(location, unit): + """ + Get the current weather in a given location using the Open Meteo API + Source: https://open-meteo.com/en/docs + :param location: The location to get the weather for, in natural language + :param unit: The unit to use for the temperature (`celsius` or `fahrenheit`) + :return: The JSON response to be fed back to the model + """ + geolocator = Nominatim(user_agent="chatgpt-telegram-bot") + geoloc = geolocator.geocode(location) + request = requests.get(f'https://api.open-meteo.com/v1/forecast' + f'?latitude={geoloc.latitude}' + f'&longitude={geoloc.longitude}' + f'&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_mean,' + f'&forecast_days=7' + f'&timezone=auto' + f'&temperature_unit={unit}' + f'¤t_weather=true') + return json.dumps(request.json()) diff --git a/requirements.txt b/requirements.txt index 1c523d39..7018a13f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ tiktoken==0.4.0 openai==0.27.8 python-telegram-bot==20.3 requests~=2.31.0 -tenacity==8.2.2 \ No newline at end of file +tenacity==8.2.2 +geopy~=2.3.0 \ No newline at end of file From 0d0158eaf25ffd28fbbd475cbedf96d3ae53d18b Mon Sep 17 00:00:00 2001 From: ned Date: Wed, 14 Jun 2023 20:10:15 +0200 Subject: [PATCH 02/52] extract latitude and longitude directly --- bot/functions.py | 2 +- plugins/weather.py | 22 ++++++++++++---------- requirements.txt | 3 +-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/bot/functions.py b/bot/functions.py index 50d90bf2..eb1e8b26 100644 --- a/bot/functions.py +++ b/bot/functions.py @@ -18,6 +18,6 @@ async def call_function(function_name, arguments): """ if function_name == "get_current_weather": arguments = json.loads(arguments) - return await get_current_weather(arguments["location"], arguments["unit"]) + return await get_current_weather(arguments["latitude"], arguments["longitude"], arguments["unit"]) raise Exception(f"Function {function_name} not found") diff --git a/plugins/weather.py b/plugins/weather.py index 33382440..8d2892d4 100644 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -1,7 +1,6 @@ import json import requests -from geopy import Nominatim def weather_function_spec(): @@ -11,9 +10,13 @@ def weather_function_spec(): "parameters": { "type": "object", "properties": { - "location": { + "latitude": { "type": "string", - "description": "The exact city and state, e.g. San Francisco, CA" + "description": "Latitude of the location" + }, + "longitude": { + "type": "string", + "description": "Longitude of the location" }, "unit": { "type": "string", @@ -21,24 +24,23 @@ def weather_function_spec(): "description": "The temperature unit to use. Infer this from the provided location.", }, }, - "required": ["location", "unit"], + "required": ["latitude", "longitude", "unit"], } } -async def get_current_weather(location, unit): +async def get_current_weather(latitude, longitude, unit): """ Get the current weather in a given location using the Open Meteo API Source: https://open-meteo.com/en/docs - :param location: The location to get the weather for, in natural language + :param latitude: The latitude of the location to get the weather for + :param longitude: The longitude of the location to get the weather for :param unit: The unit to use for the temperature (`celsius` or `fahrenheit`) :return: The JSON response to be fed back to the model """ - geolocator = Nominatim(user_agent="chatgpt-telegram-bot") - geoloc = geolocator.geocode(location) request = requests.get(f'https://api.open-meteo.com/v1/forecast' - f'?latitude={geoloc.latitude}' - f'&longitude={geoloc.longitude}' + f'?latitude={latitude}' + f'&longitude={longitude}' f'&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_mean,' f'&forecast_days=7' f'&timezone=auto' diff --git a/requirements.txt b/requirements.txt index 7018a13f..1c523d39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,4 @@ tiktoken==0.4.0 openai==0.27.8 python-telegram-bot==20.3 requests~=2.31.0 -tenacity==8.2.2 -geopy~=2.3.0 \ No newline at end of file +tenacity==8.2.2 \ No newline at end of file From 23f0d6662da8c04da0794411aebd0c4e9a22a808 Mon Sep 17 00:00:00 2001 From: ned Date: Wed, 14 Jun 2023 20:44:00 +0200 Subject: [PATCH 03/52] reorganize folders --- bot/openai_helper.py | 2 +- {plugins => bot/plugins}/weather.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {plugins => bot/plugins}/weather.py (100%) diff --git a/bot/openai_helper.py b/bot/openai_helper.py index b94ab0a5..febf1ec6 100644 --- a/bot/openai_helper.py +++ b/bot/openai_helper.py @@ -14,7 +14,7 @@ from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type -from bot.functions import get_functions_specs, call_function +from functions import get_functions_specs, call_function # Models can be found here: https://platform.openai.com/docs/models/overview GPT_3_MODELS = ("gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613") diff --git a/plugins/weather.py b/bot/plugins/weather.py similarity index 100% rename from plugins/weather.py rename to bot/plugins/weather.py From c9d9c75d6caf62c0809694dc67b7f7f701e576e8 Mon Sep 17 00:00:00 2001 From: ned Date: Wed, 14 Jun 2023 22:04:19 +0200 Subject: [PATCH 04/52] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ff0034b7..e1f204b8 100644 --- a/README.md +++ b/README.md @@ -100,10 +100,10 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `BOT_LANGUAGE` | Language of general bot messages. Currently available: `en`, `de`, `ru`, `tr`, `it`, `fi`, `es`, `id`, `nl`, `zh-cn`, `zh-tw`, `vi`, `fa`, `pt-br`, `uk`. [Contribute with additional translations](https://github.com/n3d1117/chatgpt-telegram-bot/discussions/219) | `en` | #### Functions -| Parameter | Description | Default value | -|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| -| `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | -| `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model before displaying a user-facing message | `10` | +| Parameter | Description | Default value | +|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| +| `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | +| `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response before displaying a user-facing message | `10` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. From 302cdc035d5011136e24e7a0846ad77407f99ee3 Mon Sep 17 00:00:00 2001 From: ned Date: Sat, 17 Jun 2023 23:39:12 +0200 Subject: [PATCH 05/52] various improvements and new plugins --- README.md | 14 ++++-- bot/functions.py | 53 +++++++++++++++++------ bot/main.py | 9 +++- bot/openai_helper.py | 51 +++++++++++++++------- bot/plugins/crypto.py | 30 +++++++++++++ bot/plugins/plugin.py | 30 +++++++++++++ bot/plugins/weather.py | 84 ++++++++++++++++++------------------ bot/plugins/web_search.py | 51 ++++++++++++++++++++++ bot/plugins/wolfram_alpha.py | 48 +++++++++++++++++++++ requirements.txt | 4 +- translations.json | 2 +- 11 files changed, 298 insertions(+), 78 deletions(-) create mode 100644 bot/plugins/crypto.py create mode 100644 bot/plugins/plugin.py create mode 100644 bot/plugins/web_search.py create mode 100644 bot/plugins/wolfram_alpha.py diff --git a/README.md b/README.md index e1f204b8..e992802c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ A [Telegram bot](https://core.telegram.org/bots/api) that integrates with OpenAI - [x] (NEW!) Support *functions* (plugins) to extend the bot's functionality with 3rd party services - Currently available functions: - Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) + - Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) + - WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) - requires a WolframAlpha API key + - Web search (powered by [DuckDuckGo](https://duckduckgo.com)) ## Additional features - help needed! If you'd like to help, check out the [issues](https://github.com/n3d1117/chatgpt-telegram-bot/issues) section and contribute! @@ -100,10 +103,13 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `BOT_LANGUAGE` | Language of general bot messages. Currently available: `en`, `de`, `ru`, `tr`, `it`, `fi`, `es`, `id`, `nl`, `zh-cn`, `zh-tw`, `vi`, `fa`, `pt-br`, `uk`. [Contribute with additional translations](https://github.com/n3d1117/chatgpt-telegram-bot/discussions/219) | `en` | #### Functions -| Parameter | Description | Default value | -|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| -| `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | -| `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response before displaying a user-facing message | `10` | +| Parameter | Description | Default value | +|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| +| `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | +| `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response before displaying a user-facing message | `10` | +| `PLUGINS` | List of plugins to enable (`wolfram`, `weather`, `crypto`, `web_search`), e.g: PLUGINS=wolfram,weather | `-` | +| `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | +| `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation) | `false` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. diff --git a/bot/functions.py b/bot/functions.py index eb1e8b26..32274085 100644 --- a/bot/functions.py +++ b/bot/functions.py @@ -1,23 +1,48 @@ import json -from plugins.weather import weather_function_spec, get_current_weather +from bot.plugins.crypto import CryptoPlugin +from bot.plugins.weather import WeatherPlugin +from bot.plugins.web_search import WebSearchPlugin +from bot.plugins.wolfram_alpha import WolframAlphaPlugin -def get_functions_specs(): +class PluginManager: """ - Return the list of function specs that can be called by the model + A class to manage the plugins and call the correct functions """ - return [ - weather_function_spec(), - ] + def __init__(self, config): + enabled_plugins = config.get('plugins', []) + plugins = [ + WolframAlphaPlugin() if 'wolfram' in enabled_plugins else None, + WeatherPlugin() if 'weather' in enabled_plugins else None, + CryptoPlugin() if 'crypto' in enabled_plugins else None, + WebSearchPlugin() if 'web_search' in enabled_plugins else None, + ] + self.plugins = [plugin for plugin in plugins if plugin is not None] + def get_functions_specs(self): + """ + Return the list of function specs that can be called by the model + """ + return [plugin.get_spec() for plugin in self.plugins] -async def call_function(function_name, arguments): - """ - Call a function based on the name and parameters provided - """ - if function_name == "get_current_weather": - arguments = json.loads(arguments) - return await get_current_weather(arguments["latitude"], arguments["longitude"], arguments["unit"]) + async def call_function(self, function_name, arguments): + """ + Call a function based on the name and parameters provided + """ + plugin = self.__get_plugin_by_function_name(function_name) + if not plugin: + return json.dumps({'error': f'Function {function_name} not found'}) + return json.dumps(await plugin.execute(**json.loads(arguments))) + + def get_plugin_source_name(self, function_name) -> str: + """ + Return the source name of the plugin + """ + plugin = self.__get_plugin_by_function_name(function_name) + if not plugin: + return '' + return plugin.get_source_name() - raise Exception(f"Function {function_name} not found") + def __get_plugin_by_function_name(self, function_name): + return next((plugin for plugin in self.plugins if plugin.get_spec().get('name') == function_name), None) diff --git a/bot/main.py b/bot/main.py index 71bd43d5..081e42bf 100644 --- a/bot/main.py +++ b/bot/main.py @@ -3,6 +3,7 @@ from dotenv import load_dotenv +from bot.functions import PluginManager from openai_helper import OpenAIHelper, default_max_tokens, are_functions_available from telegram_bot import ChatGPTTelegramBot @@ -47,6 +48,7 @@ def main(): 'presence_penalty': float(os.environ.get('PRESENCE_PENALTY', 0.0)), 'frequency_penalty': float(os.environ.get('FREQUENCY_PENALTY', 0.0)), 'bot_language': os.environ.get('BOT_LANGUAGE', 'en'), + 'show_plugins_used': os.environ.get('SHOW_PLUGINS_USED', 'false').lower() == 'true', } if openai_config['enable_functions'] and not functions_available: @@ -82,8 +84,13 @@ def main(): 'bot_language': os.environ.get('BOT_LANGUAGE', 'en'), } + plugin_config = { + 'plugins': os.environ.get('PLUGINS', '').split(',') + } + # Setup and run ChatGPT and Telegram bot - openai_helper = OpenAIHelper(config=openai_config) + plugin_manager = PluginManager(config=plugin_config) + openai_helper = OpenAIHelper(config=openai_config, plugin_manager=plugin_manager) telegram_bot = ChatGPTTelegramBot(config=telegram_config, openai=openai_helper) telegram_bot.run() diff --git a/bot/openai_helper.py b/bot/openai_helper.py index febf1ec6..697583be 100644 --- a/bot/openai_helper.py +++ b/bot/openai_helper.py @@ -14,7 +14,7 @@ from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type -from functions import get_functions_specs, call_function +from bot.functions import PluginManager # Models can be found here: https://platform.openai.com/docs/models/overview GPT_3_MODELS = ("gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613") @@ -84,14 +84,16 @@ class OpenAIHelper: ChatGPT helper class. """ - def __init__(self, config: dict): + def __init__(self, config: dict, plugin_manager: PluginManager): """ Initializes the OpenAI helper class with the given configuration. :param config: A dictionary containing the GPT configuration + :param plugin_manager: The plugin manager """ openai.api_key = config['api_key'] openai.proxy = config['proxy'] self.config = config + self.plugin_manager = plugin_manager self.conversations: dict[int: list] = {} # {chat_id: history} self.last_updated: dict[int: datetime] = {} # {chat_id: last_update_timestamp} @@ -112,9 +114,10 @@ async def get_chat_response(self, chat_id: int, query: str) -> tuple[str, str]: :param query: The query to send to the model :return: The answer from the model and the number of tokens used """ + plugins_used = () response = await self.__common_get_chat_response(chat_id, query) if self.config['enable_functions']: - response = await self.__handle_function_call(chat_id, response) + response, plugins_used = await self.__handle_function_call(chat_id, response) answer = '' if len(response.choices) > 1 and self.config['n_choices'] > 1: @@ -130,11 +133,17 @@ async def get_chat_response(self, chat_id: int, query: str) -> tuple[str, str]: self.__add_to_history(chat_id, role="assistant", content=answer) bot_language = self.config['bot_language'] + show_plugins_used = len(plugins_used) > 0 and self.config['show_plugins_used'] + plugin_names = tuple(self.plugin_manager.get_plugin_source_name(plugin) for plugin in plugins_used) if self.config['show_usage']: answer += "\n\n---\n" \ f"💰 {str(response.usage['total_tokens'])} {localized_text('stats_tokens', bot_language)}" \ f" ({str(response.usage['prompt_tokens'])} {localized_text('prompt', bot_language)}," \ f" {str(response.usage['completion_tokens'])} {localized_text('completion', bot_language)})" + if show_plugins_used: + answer += f"\n🔌 {', '.join(plugin_names)}" + elif show_plugins_used: + answer += f"\n\n---\n🔌 {', '.join(plugin_names)}" return answer, response.usage['total_tokens'] @@ -145,9 +154,10 @@ async def get_chat_response_stream(self, chat_id: int, query: str): :param query: The query to send to the model :return: The answer from the model and the number of tokens used, or 'not_finished' """ + plugins_used = () response = await self.__common_get_chat_response(chat_id, query, stream=True) if self.config['enable_functions']: - response = await self.__handle_function_call(chat_id, response, stream=True) + response, plugins_used = await self.__handle_function_call(chat_id, response, stream=True) answer = '' async for item in response: @@ -161,8 +171,14 @@ async def get_chat_response_stream(self, chat_id: int, query: str): self.__add_to_history(chat_id, role="assistant", content=answer) tokens_used = str(self.__count_tokens(self.conversations[chat_id])) + show_plugins_used = len(plugins_used) > 0 and self.config['show_plugins_used'] + plugin_names = tuple(self.plugin_manager.get_plugin_source_name(plugin) for plugin in plugins_used) if self.config['show_usage']: answer += f"\n\n---\n💰 {tokens_used} {localized_text('stats_tokens', self.config['bot_language'])}" + if show_plugins_used: + answer += f"\n🔌 {', '.join(plugin_names)}" + elif show_plugins_used: + answer += f"\n\n---\n🔌 {', '.join(plugin_names)}" yield answer, tokens_used @@ -217,8 +233,10 @@ async def __common_get_chat_response(self, chat_id: int, query: str, stream=Fals } if self.config['enable_functions']: - common_args['functions'] = get_functions_specs() - common_args['function_call'] = 'auto' + functions = self.plugin_manager.get_functions_specs() + if len(functions) > 0: + common_args['functions'] = self.plugin_manager.get_functions_specs() + common_args['function_call'] = 'auto' return await openai.ChatCompletion.acreate(**common_args) @@ -231,7 +249,7 @@ async def __common_get_chat_response(self, chat_id: int, query: str, stream=Fals except Exception as e: raise Exception(f"⚠️ _{localized_text('error', bot_language)}._ ⚠️\n{str(e)}") from e - async def __handle_function_call(self, chat_id, response, stream=False, times=0): + async def __handle_function_call(self, chat_id, response, stream=False, times=0, plugins_used=()): function_name = '' arguments = '' if stream: @@ -247,9 +265,9 @@ async def __handle_function_call(self, chat_id, response, stream=False, times=0) elif 'finish_reason' in first_choice and first_choice.finish_reason == 'function_call': break else: - return response + return response, plugins_used else: - return response + return response, plugins_used else: if 'choices' in response and len(response.choices) > 0: first_choice = response.choices[0] @@ -259,21 +277,24 @@ async def __handle_function_call(self, chat_id, response, stream=False, times=0) if 'arguments' in first_choice.message.function_call: arguments += str(first_choice.message.function_call.arguments) else: - return response + return response, plugins_used else: - return response + return response, plugins_used - logging.info(f'Calling function {function_name}...') - function_response = await call_function(function_name, arguments) + logging.info(f'Calling function {function_name} with arguments {arguments}') + function_response = await self.plugin_manager.call_function(function_name, arguments) + logging.info(f'Got response {function_response}') self.__add_function_call_to_history(chat_id=chat_id, function_name=function_name, content=function_response) response = await openai.ChatCompletion.acreate( model=self.config['model'], messages=self.conversations[chat_id], - functions=get_functions_specs(), + functions=self.plugin_manager.get_functions_specs(), function_call='auto' if times < self.config['functions_max_consecutive_calls'] else 'none', stream=stream ) - return await self.__handle_function_call(chat_id, response, stream, times+1) + if function_name not in plugins_used: + plugins_used += (function_name,) + return await self.__handle_function_call(chat_id, response, stream, times + 1, plugins_used) async def generate_image(self, prompt: str) -> tuple[str, str]: """ diff --git a/bot/plugins/crypto.py b/bot/plugins/crypto.py new file mode 100644 index 00000000..1ce18dfe --- /dev/null +++ b/bot/plugins/crypto.py @@ -0,0 +1,30 @@ +from typing import Dict + +import requests + +from bot.plugins.plugin import Plugin + + +# Author: https://github.com/stumpyfr +class CryptoPlugin(Plugin): + """ + A plugin to fetch the current rate of various cryptocurrencies + """ + def get_source_name(self) -> str: + return "CoinCap" + + def get_spec(self) -> Dict: + return { + "name": "get_crypto_rate", + "description": "Get the current rate of various crypto currencies", + "parameters": { + "type": "object", + "properties": { + "asset": {"type": "string", "description": "Asset of the crypto"} + }, + "required": ["asset"], + }, + } + + async def execute(self, **kwargs) -> Dict: + return requests.get(f"https://api.coincap.io/v2/rates/{kwargs['asset']}").json() diff --git a/bot/plugins/plugin.py b/bot/plugins/plugin.py new file mode 100644 index 00000000..d3aa3aa5 --- /dev/null +++ b/bot/plugins/plugin.py @@ -0,0 +1,30 @@ +from abc import abstractmethod, ABC +from typing import Dict + + +class Plugin(ABC): + """ + A plugin interface which can be used to create plugins for the ChatGPT API. + """ + + @abstractmethod + def get_source_name(self) -> str: + """ + Return the name of the source of the plugin. + """ + pass + + @abstractmethod + def get_spec(self) -> Dict: + """ + Function spec in the form of JSON schema as specified in the OpenAI documentation: + https://platform.openai.com/docs/api-reference/chat/create#chat/create-functions + """ + pass + + @abstractmethod + async def execute(self, **kwargs) -> Dict: + """ + Execute the plugin and return a JSON serializable response + """ + pass diff --git a/bot/plugins/weather.py b/bot/plugins/weather.py index 8d2892d4..9e935ffa 100644 --- a/bot/plugins/weather.py +++ b/bot/plugins/weather.py @@ -1,49 +1,49 @@ -import json +from typing import Dict import requests - -def weather_function_spec(): - return { - "name": "get_current_weather", - "description": "Get the current and 7-day daily weather forecast for a location using Open Meteo APIs.", - "parameters": { - "type": "object", - "properties": { - "latitude": { - "type": "string", - "description": "Latitude of the location" - }, - "longitude": { - "type": "string", - "description": "Longitude of the location" - }, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - "description": "The temperature unit to use. Infer this from the provided location.", - }, - }, - "required": ["latitude", "longitude", "unit"], - } - } +from bot.plugins.plugin import Plugin -async def get_current_weather(latitude, longitude, unit): +class WeatherPlugin(Plugin): """ - Get the current weather in a given location using the Open Meteo API - Source: https://open-meteo.com/en/docs - :param latitude: The latitude of the location to get the weather for - :param longitude: The longitude of the location to get the weather for - :param unit: The unit to use for the temperature (`celsius` or `fahrenheit`) - :return: The JSON response to be fed back to the model + A plugin to get the current weather and 7-day daily forecast for a location """ - request = requests.get(f'https://api.open-meteo.com/v1/forecast' - f'?latitude={latitude}' - f'&longitude={longitude}' - f'&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_mean,' - f'&forecast_days=7' - f'&timezone=auto' - f'&temperature_unit={unit}' - f'¤t_weather=true') - return json.dumps(request.json()) + def get_source_name(self) -> str: + return "OpenMeteo" + + def get_spec(self) -> Dict: + return { + "name": "get_current_weather", + "description": "Get the current and 7-day daily weather forecast for a location using Open Meteo APIs.", + "parameters": { + "type": "object", + "properties": { + "latitude": { + "type": "string", + "description": "Latitude of the location" + }, + "longitude": { + "type": "string", + "description": "Longitude of the location" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the provided location.", + }, + }, + "required": ["latitude", "longitude", "unit"], + } + } + + async def execute(self, **kwargs) -> Dict: + url = f'https://api.open-meteo.com/v1/forecast'\ + f'?latitude={kwargs["latitude"]}'\ + f'&longitude={kwargs["longitude"]}'\ + f'&temperature_unit={kwargs["unit"]}' \ + '¤t_weather=true' \ + '&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_mean,' \ + '&forecast_days=7' \ + '&timezone=auto' + return requests.get(url).json() diff --git a/bot/plugins/web_search.py b/bot/plugins/web_search.py new file mode 100644 index 00000000..13763b46 --- /dev/null +++ b/bot/plugins/web_search.py @@ -0,0 +1,51 @@ +from itertools import islice +from typing import Dict + +from duckduckgo_search import DDGS + +from bot.plugins.plugin import Plugin + + +class WebSearchPlugin(Plugin): + """ + A plugin to search the web for a given query, using DuckDuckGo + """ + + def get_source_name(self) -> str: + return "DuckDuckGo" + + def get_spec(self) -> Dict: + return { + "name": "web_search", + "description": "Execute a web search for the given query and return a list of results", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "the user query" + } + }, + "required": ["query"], + }, + } + + async def execute(self, **kwargs) -> Dict: + with DDGS() as ddgs: + ddgs_gen = ddgs.text( + kwargs['query'], + region='wt-wt', + safesearch='off' + ) + results = list(islice(ddgs_gen, 8)) + + if results is None or len(results) == 0: + return {"Result": "No good DuckDuckGo Search Result was found"} + + def to_metadata(result: Dict) -> Dict[str, str]: + return { + "snippet": result["body"], + "title": result["title"], + "link": result["href"], + } + return {"result": [to_metadata(result) for result in results]} diff --git a/bot/plugins/wolfram_alpha.py b/bot/plugins/wolfram_alpha.py new file mode 100644 index 00000000..7d902c44 --- /dev/null +++ b/bot/plugins/wolfram_alpha.py @@ -0,0 +1,48 @@ +import os +from typing import Dict + +import wolframalpha + +from bot.plugins.plugin import Plugin + + +class WolframAlphaPlugin(Plugin): + """ + A plugin to answer questions using WolframAlpha. + """ + def __init__(self): + wolfram_app_id = os.getenv('WOLFRAM_APP_ID') + if not wolfram_app_id: + raise ValueError('WOLFRAM_APP_ID environment variable must be set to use WolframAlphaPlugin') + self.app_id = wolfram_app_id + + def get_source_name(self) -> str: + return "WolframAlpha" + + def get_spec(self) -> Dict: + return { + "name": "answer_with_wolfram_alpha", + "description": "Get an answer to a question using Wolfram Alpha. Input should the the query in English.", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "The search query, in english (translate if necessary)"} + }, + "required": ["query"] + } + } + + async def execute(self, **kwargs) -> Dict: + client = wolframalpha.Client(self.app_id) + res = client.query(kwargs['query']) + try: + assumption = next(res.pods).text + answer = next(res.results).text + except StopIteration: + return {'answer': 'Wolfram Alpha wasn\'t able to answer it'} + + if answer is None or answer == "": + return {'answer': 'No good Wolfram Alpha Result was found'} + else: + return {'assumption': assumption, 'answer': answer} + diff --git a/requirements.txt b/requirements.txt index 1c523d39..3cd7babd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ tiktoken==0.4.0 openai==0.27.8 python-telegram-bot==20.3 requests~=2.31.0 -tenacity==8.2.2 \ No newline at end of file +tenacity==8.2.2 +wolframalpha==5.0.0 +duckduckgo_search==3.8.3 \ No newline at end of file diff --git a/translations.json b/translations.json index 405c7c26..35ad1acc 100644 --- a/translations.json +++ b/translations.json @@ -12,7 +12,7 @@ "stats_conversation":["Current conversation", "chat messages in history", "chat tokens in history"], "usage_today":"Usage today", "usage_month":"Usage this month", - "stats_tokens":"chat tokens used", + "stats_tokens":"tokens", "stats_images":"images generated", "stats_transcribe":["minutes and", "seconds transcribed"], "stats_total":"💰 For a total amount of $", From de3c6c5b5c2010edff4e6b469287cbaece67300e Mon Sep 17 00:00:00 2001 From: ned Date: Sat, 17 Jun 2023 23:56:40 +0200 Subject: [PATCH 06/52] adjust imports --- bot/functions.py | 8 ++++---- bot/main.py | 2 +- bot/openai_helper.py | 2 +- bot/plugins/crypto.py | 2 +- bot/plugins/weather.py | 2 +- bot/plugins/web_search.py | 2 +- bot/plugins/wolfram_alpha.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bot/functions.py b/bot/functions.py index 32274085..62bed04a 100644 --- a/bot/functions.py +++ b/bot/functions.py @@ -1,9 +1,9 @@ import json -from bot.plugins.crypto import CryptoPlugin -from bot.plugins.weather import WeatherPlugin -from bot.plugins.web_search import WebSearchPlugin -from bot.plugins.wolfram_alpha import WolframAlphaPlugin +from plugins.crypto import CryptoPlugin +from plugins.weather import WeatherPlugin +from plugins.web_search import WebSearchPlugin +from plugins.wolfram_alpha import WolframAlphaPlugin class PluginManager: diff --git a/bot/main.py b/bot/main.py index 081e42bf..c58c6c90 100644 --- a/bot/main.py +++ b/bot/main.py @@ -3,7 +3,7 @@ from dotenv import load_dotenv -from bot.functions import PluginManager +from functions import PluginManager from openai_helper import OpenAIHelper, default_max_tokens, are_functions_available from telegram_bot import ChatGPTTelegramBot diff --git a/bot/openai_helper.py b/bot/openai_helper.py index 697583be..f93f7995 100644 --- a/bot/openai_helper.py +++ b/bot/openai_helper.py @@ -14,7 +14,7 @@ from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type -from bot.functions import PluginManager +from functions import PluginManager # Models can be found here: https://platform.openai.com/docs/models/overview GPT_3_MODELS = ("gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613") diff --git a/bot/plugins/crypto.py b/bot/plugins/crypto.py index 1ce18dfe..f36b8184 100644 --- a/bot/plugins/crypto.py +++ b/bot/plugins/crypto.py @@ -2,7 +2,7 @@ import requests -from bot.plugins.plugin import Plugin +from .plugin import Plugin # Author: https://github.com/stumpyfr diff --git a/bot/plugins/weather.py b/bot/plugins/weather.py index 9e935ffa..bdcbbc11 100644 --- a/bot/plugins/weather.py +++ b/bot/plugins/weather.py @@ -2,7 +2,7 @@ import requests -from bot.plugins.plugin import Plugin +from .plugin import Plugin class WeatherPlugin(Plugin): diff --git a/bot/plugins/web_search.py b/bot/plugins/web_search.py index 13763b46..17b89e2c 100644 --- a/bot/plugins/web_search.py +++ b/bot/plugins/web_search.py @@ -3,7 +3,7 @@ from duckduckgo_search import DDGS -from bot.plugins.plugin import Plugin +from .plugin import Plugin class WebSearchPlugin(Plugin): diff --git a/bot/plugins/wolfram_alpha.py b/bot/plugins/wolfram_alpha.py index 7d902c44..f20bb18f 100644 --- a/bot/plugins/wolfram_alpha.py +++ b/bot/plugins/wolfram_alpha.py @@ -3,7 +3,7 @@ import wolframalpha -from bot.plugins.plugin import Plugin +from .plugin import Plugin class WolframAlphaPlugin(Plugin): From c34bd9faa9721800a427dded44ab4dccd908ee32 Mon Sep 17 00:00:00 2001 From: ned Date: Sun, 18 Jun 2023 00:05:09 +0200 Subject: [PATCH 07/52] update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e992802c..181dadbb 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,9 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di |-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| | `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | | `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response before displaying a user-facing message | `10` | -| `PLUGINS` | List of plugins to enable (`wolfram`, `weather`, `crypto`, `web_search`), e.g: PLUGINS=wolfram,weather | `-` | +| `PLUGINS` | List of plugins to enable (`wolfram`, `weather`, `crypto`, `web_search`), e.g: `PLUGINS=wolfram,weather | `-` | | `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | -| `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation) | `false` | +| `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation)) | `false` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. From 29ae491d0631cd5f7a6ef7126fe7d2b9bf321b4d Mon Sep 17 00:00:00 2001 From: ned Date: Sun, 18 Jun 2023 00:31:20 +0200 Subject: [PATCH 08/52] update README.md --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 181dadbb..a4ec3d74 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,7 @@ A [Telegram bot](https://core.telegram.org/bots/api) that integrates with OpenAI - To use this feature, enable inline queries for your bot in BotFather via the `/setinline` [command](https://core.telegram.org/bots/inline) - [x] (NEW!) Support *new models* [announced on June 13, 2023](https://openai.com/blog/function-calling-and-other-api-updates) - [x] (NEW!) Support *functions* (plugins) to extend the bot's functionality with 3rd party services - - Currently available functions: - - Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) - - Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) - - WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) - requires a WolframAlpha API key - - Web search (powered by [DuckDuckGo](https://duckduckgo.com)) + - See [here](#available-plugins) for a list of available plugins ## Additional features - help needed! If you'd like to help, check out the [issues](https://github.com/n3d1117/chatgpt-telegram-bot/issues) section and contribute! @@ -107,10 +103,18 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di |-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| | `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | | `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response before displaying a user-facing message | `10` | -| `PLUGINS` | List of plugins to enable (`wolfram`, `weather`, `crypto`, `web_search`), e.g: `PLUGINS=wolfram,weather | `-` | +| `PLUGINS` | List of plugins to enable (see below for a full list), e.g: `PLUGINS=wolfram,weather` | `-` | | `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | | `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation)) | `false` | +#### Available plugins +| Name | Description | Requires API key | +|--------------|---------------------------------------------------------------------------------------------------------------------|------------------| +| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | `-` | +| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | +| `web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | +| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | `-` | + Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. ### Installing From cf418a2fc534b0565e4a90378445411e3cb3d955 Mon Sep 17 00:00:00 2001 From: ned Date: Sun, 18 Jun 2023 00:41:00 +0200 Subject: [PATCH 09/52] update README.md --- README.md | 4 ++-- bot/telegram_bot.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a4ec3d74..2a82d51e 100644 --- a/README.md +++ b/README.md @@ -105,10 +105,10 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response before displaying a user-facing message | `10` | | `PLUGINS` | List of plugins to enable (see below for a full list), e.g: `PLUGINS=wolfram,weather` | `-` | | `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | -| `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation)) | `false` | +| `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation)) | `-` | #### Available plugins -| Name | Description | Requires API key | +| Name | Description | Required API key | |--------------|---------------------------------------------------------------------------------------------------------------------|------------------| | `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | `-` | | `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | diff --git a/bot/telegram_bot.py b/bot/telegram_bot.py index 3d94bbce..11ee1908 100644 --- a/bot/telegram_bot.py +++ b/bot/telegram_bot.py @@ -436,7 +436,8 @@ async def _reply(): sent_message = await update.effective_message.reply_text( message_thread_id=get_thread_id(update), reply_to_message_id=get_reply_to_message_id(self.config, update), - text=content + text=content, + disable_web_page_preview=True ) except: continue From d86cd385271e3e3f11bcf463cef8545f9e5f1b55 Mon Sep 17 00:00:00 2001 From: ned Date: Sun, 18 Jun 2023 00:45:19 +0200 Subject: [PATCH 10/52] update utils.py --- bot/telegram_bot.py | 4 ---- bot/utils.py | 6 ++++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/bot/telegram_bot.py b/bot/telegram_bot.py index 11ee1908..b63411ed 100644 --- a/bot/telegram_bot.py +++ b/bot/telegram_bot.py @@ -390,10 +390,6 @@ async def prompt(self, update: Update, context: ContextTypes.DEFAULT_TYPE): if self.config['stream']: async def _reply(): nonlocal total_tokens - await update.effective_message.reply_chat_action( - action=constants.ChatAction.TYPING, - message_thread_id=get_thread_id(update) - ) stream_response = self.openai.get_chat_response_stream(chat_id=chat_id, query=prompt) i = 0 diff --git a/bot/utils.py b/bot/utils.py index 04c46a3e..4144c6db 100644 --- a/bot/utils.py +++ b/bot/utils.py @@ -117,7 +117,8 @@ async def edit_message_with_retry(context: ContextTypes.DEFAULT_TYPE, chat_id: i message_id=int(message_id) if not is_inline else None, inline_message_id=message_id if is_inline else None, text=text, - parse_mode=constants.ParseMode.MARKDOWN if markdown else None + parse_mode=constants.ParseMode.MARKDOWN if markdown else None, + disable_web_page_preview=True ) except telegram.error.BadRequest as e: if str(e).startswith("Message is not modified"): @@ -127,7 +128,8 @@ async def edit_message_with_retry(context: ContextTypes.DEFAULT_TYPE, chat_id: i chat_id=chat_id, message_id=int(message_id) if not is_inline else None, inline_message_id=message_id if is_inline else None, - text=text + text=text, + disable_web_page_preview=True ) except Exception as e: logging.warning(f'Failed to edit message: {str(e)}') From c1bfda47b546001dc95b19d3e916d532d1b9642e Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 19 Jun 2023 19:53:34 +0200 Subject: [PATCH 11/52] improvements and added initial spotify plugin --- .gitignore | 1 + README.md | 16 +- bot/main.py | 2 +- bot/openai_helper.py | 2 +- bot/{functions.py => plugin_manager.py} | 22 +- bot/plugins/crypto.py | 8 +- bot/plugins/plugin.py | 6 +- bot/plugins/spotify.py | 302 ++++++++++++++++++++++++ bot/plugins/weather.py | 96 +++++--- bot/plugins/web_search.py | 8 +- bot/plugins/wolfram_alpha.py | 8 +- bot/telegram_bot.py | 130 +++++----- requirements.txt | 3 +- 13 files changed, 475 insertions(+), 129 deletions(-) rename bot/{functions.py => plugin_manager.py} (65%) create mode 100644 bot/plugins/spotify.py diff --git a/.gitignore b/.gitignore index 5ccada3f..a156be6a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ .DS_Store /usage_logs venv +/.cache diff --git a/README.md b/README.md index 2a82d51e..c06e2b14 100644 --- a/README.md +++ b/README.md @@ -106,14 +106,18 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `PLUGINS` | List of plugins to enable (see below for a full list), e.g: `PLUGINS=wolfram,weather` | `-` | | `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | | `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation)) | `-` | +| `SPOTIFY_CLIENT_ID` | Spotify app Client ID (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | +| `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | +| `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | #### Available plugins -| Name | Description | Required API key | -|--------------|---------------------------------------------------------------------------------------------------------------------|------------------| -| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | `-` | -| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | -| `web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | -| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | `-` | +| Name | Description | Required API key(s) | +|--------------|-------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | `-` | +| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | +| `web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | +| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | `-` | +| `spotify` | Spotify top tracks/artists and currently playing song (powered by [Spotify](https://spotify.com)). Requires one-time auth approval. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. diff --git a/bot/main.py b/bot/main.py index c58c6c90..c8b1a60c 100644 --- a/bot/main.py +++ b/bot/main.py @@ -3,7 +3,7 @@ from dotenv import load_dotenv -from functions import PluginManager +from plugin_manager import PluginManager from openai_helper import OpenAIHelper, default_max_tokens, are_functions_available from telegram_bot import ChatGPTTelegramBot diff --git a/bot/openai_helper.py b/bot/openai_helper.py index f93f7995..67ab7686 100644 --- a/bot/openai_helper.py +++ b/bot/openai_helper.py @@ -14,7 +14,7 @@ from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type -from functions import PluginManager +from plugin_manager import PluginManager # Models can be found here: https://platform.openai.com/docs/models/overview GPT_3_MODELS = ("gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613") diff --git a/bot/functions.py b/bot/plugin_manager.py similarity index 65% rename from bot/functions.py rename to bot/plugin_manager.py index 62bed04a..9abbadf8 100644 --- a/bot/functions.py +++ b/bot/plugin_manager.py @@ -1,5 +1,7 @@ import json +from plugins.python import PythonPlugin +from plugins.spotify import SpotifyPlugin from plugins.crypto import CryptoPlugin from plugins.weather import WeatherPlugin from plugins.web_search import WebSearchPlugin @@ -12,19 +14,20 @@ class PluginManager: """ def __init__(self, config): enabled_plugins = config.get('plugins', []) - plugins = [ - WolframAlphaPlugin() if 'wolfram' in enabled_plugins else None, - WeatherPlugin() if 'weather' in enabled_plugins else None, - CryptoPlugin() if 'crypto' in enabled_plugins else None, - WebSearchPlugin() if 'web_search' in enabled_plugins else None, - ] - self.plugins = [plugin for plugin in plugins if plugin is not None] + plugin_mapping = { + 'wolfram': WolframAlphaPlugin(), + 'weather': WeatherPlugin(), + 'crypto': CryptoPlugin(), + 'web_search': WebSearchPlugin(), + 'spotify': SpotifyPlugin(), + } + self.plugins = [plugin_mapping[plugin] for plugin in enabled_plugins] def get_functions_specs(self): """ Return the list of function specs that can be called by the model """ - return [plugin.get_spec() for plugin in self.plugins] + return [spec for specs in map(lambda plugin: plugin.get_spec(), self.plugins) for spec in specs] async def call_function(self, function_name, arguments): """ @@ -45,4 +48,5 @@ def get_plugin_source_name(self, function_name) -> str: return plugin.get_source_name() def __get_plugin_by_function_name(self, function_name): - return next((plugin for plugin in self.plugins if plugin.get_spec().get('name') == function_name), None) + return next((plugin for plugin in self.plugins + if function_name in map(lambda spec: spec.get('name'), plugin.get_spec())), None) diff --git a/bot/plugins/crypto.py b/bot/plugins/crypto.py index f36b8184..42e35e32 100644 --- a/bot/plugins/crypto.py +++ b/bot/plugins/crypto.py @@ -13,8 +13,8 @@ class CryptoPlugin(Plugin): def get_source_name(self) -> str: return "CoinCap" - def get_spec(self) -> Dict: - return { + def get_spec(self) -> [Dict]: + return [{ "name": "get_crypto_rate", "description": "Get the current rate of various crypto currencies", "parameters": { @@ -24,7 +24,7 @@ def get_spec(self) -> Dict: }, "required": ["asset"], }, - } + }] - async def execute(self, **kwargs) -> Dict: + async def execute(self, function_name, **kwargs) -> Dict: return requests.get(f"https://api.coincap.io/v2/rates/{kwargs['asset']}").json() diff --git a/bot/plugins/plugin.py b/bot/plugins/plugin.py index d3aa3aa5..b028b270 100644 --- a/bot/plugins/plugin.py +++ b/bot/plugins/plugin.py @@ -15,15 +15,15 @@ def get_source_name(self) -> str: pass @abstractmethod - def get_spec(self) -> Dict: + def get_spec(self) -> [Dict]: """ - Function spec in the form of JSON schema as specified in the OpenAI documentation: + Function specs in the form of JSON schema as specified in the OpenAI documentation: https://platform.openai.com/docs/api-reference/chat/create#chat/create-functions """ pass @abstractmethod - async def execute(self, **kwargs) -> Dict: + async def execute(self, function_name, **kwargs) -> Dict: """ Execute the plugin and return a JSON serializable response """ diff --git a/bot/plugins/spotify.py b/bot/plugins/spotify.py new file mode 100644 index 00000000..19603be1 --- /dev/null +++ b/bot/plugins/spotify.py @@ -0,0 +1,302 @@ +import os +from typing import Dict + +import spotipy +from spotipy import SpotifyOAuth + +from .plugin import Plugin + + +class SpotifyPlugin(Plugin): + """ + A plugin to fetch information from Spotify + """ + def __init__(self): + spotify_client_id = os.getenv('SPOTIFY_CLIENT_ID') + spotify_client_secret = os.getenv('SPOTIFY_CLIENT_SECRET') + spotify_redirect_uri = os.getenv('SPOTIFY_REDIRECT_URI') + if not spotify_client_id or not spotify_client_secret or not spotify_redirect_uri: + raise ValueError('SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET and SPOTIFY_REDIRECT_URI environment variables' + ' are required to use SpotifyPlugin') + self.spotify = spotipy.Spotify( + auth_manager=SpotifyOAuth( + client_id=spotify_client_id, + client_secret=spotify_client_secret, + redirect_uri=spotify_redirect_uri, + scope="user-top-read,user-read-currently-playing", + open_browser=False + ) + ) + + def get_source_name(self) -> str: + return "Spotify" + + def get_spec(self) -> [Dict]: + time_range_param = { + "type": "string", + "enum": ["short_term", "medium_term", "long_term"], + "description": "The time range of the data to be returned. Short term is the last 4 weeks, " + "medium term is last 6 months, long term is last several years. " + "Ignore if action is currently_playing", + } + limit_param = { + "type": "integer", + "description": "The number of results to return. Max is 50. Default to 10 if not specified." + "Ignore if action is currently_playing", + } + type_param = { + "type": "string", + "enum": ["album", "artist", "track"], + "description": "Type of content to search", + } + return [ + { + "name": "spotify_get_currently_playing_song", + "description": "Get the user's currently playing song", + "parameters": { + "type": "object", + "properties": {} + } + }, + { + "name": "spotify_get_users_top_artists", + "description": "Get the user's top artists", + "parameters": { + "type": "object", + "properties": { + "time_range": time_range_param, + "limit": limit_param + } + } + }, + { + "name": "spotify_get_users_top_tracks", + "description": "Get the user's top tracks", + "parameters": { + "type": "object", + "properties": { + "time_range": time_range_param, + "limit": limit_param + } + } + }, + { + "name": "spotify_search_by_query", + "description": "Search spotify content by query", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The search query", + }, + "type": type_param + }, + "required": ["query", "type"] + } + }, + { + "name": "spotify_lookup_by_id", + "description": "Lookup spotify content by id", + "parameters": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The exact id to lookup. Can be a track id, an artist id or an album id", + }, + "type": type_param + }, + "required": ["id", "type"] + } + } + ] + + async def execute(self, function_name, **kwargs) -> Dict: + time_range = kwargs.get('time_range', 'short_term') + limit = kwargs.get('limit', 10) + + if function_name == 'spotify_get_currently_playing_song': + return self.fetch_currently_playing() + elif function_name == 'spotify_get_users_top_artists': + return self.fetch_top_artists(time_range, limit) + elif function_name == 'spotify_get_users_top_tracks': + return self.fetch_top_tracks(time_range, limit) + elif function_name == 'spotify_search_by_query': + query = kwargs.get('query', '') + search_type = kwargs.get('type', 'track') + return self.search_by_query(query, search_type, limit) + elif function_name == 'spotify_lookup_by_id': + content_id = kwargs.get('id') + search_type = kwargs.get('type', 'track') + return self.search_by_id(content_id, search_type) + + def fetch_currently_playing(self) -> Dict: + """ + Fetch user's currently playing song from Spotify + """ + currently_playing = self.spotify.current_user_playing_track() + result = { + 'name': currently_playing['item']['name'], + 'artist': currently_playing['item']['artists'][0]['name'], + 'album': currently_playing['item']['album']['name'], + 'url': currently_playing['item']['external_urls']['spotify'], + '__album_id': currently_playing['item']['album']['id'], + '__artist_id': currently_playing['item']['artists'][0]['id'], + '__track_id': currently_playing['item']['id'], + } + return {"result": result} + + def fetch_top_tracks(self, time_range='short_term', limit=5) -> Dict: + """ + Fetch user's top tracks from Spotify + """ + results = [] + top_tracks = self.spotify.current_user_top_tracks(limit=limit, time_range=time_range) + for item in top_tracks['items']: + results.append({ + 'name': item['name'], + 'artist': item['artists'][0]['name'], + 'album': item['album']['name'], + 'album_release_date': item['album']['release_date'], + 'url': item['external_urls']['spotify'], + 'album_url': item['album']['external_urls']['spotify'], + 'artist_url': item['artists'][0]['external_urls']['spotify'], + '__track_id': item['id'], + '__album_id': item['album']['id'], + '__artist_id': item['artists'][0]['id'], + }) + return {'results': results} + + def fetch_top_artists(self, time_range='short_term', limit=5) -> Dict: + """ + Fetch user's top artists from Spotify + """ + results = [] + top_artists = self.spotify.current_user_top_artists(limit=limit, time_range=time_range) + for item in top_artists['items']: + results.append({ + 'name': item['name'], + 'url': item['external_urls']['spotify'], + '__artist_id': item['id'] + }) + return {'results': results} + + def search_by_query(self, query, search_type, limit=5) -> Dict: + """ + Search content by query on Spotify + """ + results = {} + search_response = self.spotify.search(q=query, limit=limit, type=search_type) + if 'tracks' in search_response: + results['tracks'] = [] + for item in search_response['tracks']['items']: + results['tracks'].append({ + 'name': item['name'], + 'artist': item['artists'][0]['name'], + 'album': item['album']['name'], + 'album_release_date': item['album']['release_date'], + 'url': item['external_urls']['spotify'], + 'album_url': item['album']['external_urls']['spotify'], + 'artist_url': item['artists'][0]['external_urls']['spotify'], + '__artist_id': item['artists'][0]['id'], + '__album_id': item['album']['id'], + '__track_id': item['id'], + }) + if 'artists' in search_response: + results['artists'] = [] + for item in search_response['artists']['items']: + results['artists'].append({ + 'name': item['name'], + 'url': item['external_urls']['spotify'], + '__artist_id': item['id'], + }) + if 'albums' in search_response: + results['albums'] = [] + for item in search_response['albums']['items']: + results['albums'].append({ + 'name': item['name'], + 'artist': item['artists'][0]['name'], + 'url': item['external_urls']['spotify'], + 'artist_url': item['artists'][0]['external_urls']['spotify'], + 'release_date': item['release_date'], + '__artist_id': item['artists'][0]['id'], + '__album_id': item['id'], + }) + return {'results': results} + + def search_by_id(self, content_id, search_type) -> Dict: + """ + Search content by exact id on Spotify + """ + if search_type == 'track': + search_response = self.spotify.track(content_id) + return {'result': self._get_track(search_response)} + + elif search_type == 'artist': + search_response = self.spotify.artist(content_id) + albums_response = self.spotify.artist_albums(artist_id=content_id, limit=3) + return {'result': self._get_artist(search_response, albums_response)} + + elif search_type == 'album': + search_response = self.spotify.album(content_id) + return {'result': self._get_album(search_response)} + + else: + return {'error': 'Invalid search type. Must be track, artist or album'} + + def _get_artist(self, response, albums): + return { + 'name': response['name'], + 'url': response['external_urls']['spotify'], + '__artist_id': response['id'], + 'followers': response['followers']['total'], + 'genres': response['genres'], + 'albums': [ + { + 'name': album['name'], + '__album_id': album['id'], + 'url': album['external_urls']['spotify'], + 'release_date': album['release_date'], + 'total_tracks': album['total_tracks'], + } + for album in albums['items'] + ], + } + + def _get_track(self, response): + return { + 'name': response['name'], + 'artist': response['artists'][0]['name'], + '__artist_id': response['artists'][0]['id'], + 'album': response['album']['name'], + '__album_id': response['album']['id'], + 'url': response['external_urls']['spotify'], + '__track_id': response['id'], + 'duration_ms': response['duration_ms'], + 'track_number': response['track_number'], + 'explicit': response['explicit'], + } + + def _get_album(self, response): + return { + 'name': response['name'], + 'artist': response['artists'][0]['name'], + '__artist_id': response['artists'][0]['id'], + 'url': response['external_urls']['spotify'], + 'release_date': response['release_date'], + 'total_tracks': response['total_tracks'], + '__album_id': response['id'], + 'label': response['label'], + 'tracks': [ + { + 'name': track['name'], + 'url': track['external_urls']['spotify'], + '__track_id': track['id'], + 'duration_ms': track['duration_ms'], + 'track_number': track['track_number'], + 'explicit': track['explicit'], + } + for track in response['tracks']['items'] + ] + } diff --git a/bot/plugins/weather.py b/bot/plugins/weather.py index bdcbbc11..ad783a32 100644 --- a/bot/plugins/weather.py +++ b/bot/plugins/weather.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Dict import requests @@ -9,41 +10,74 @@ class WeatherPlugin(Plugin): """ A plugin to get the current weather and 7-day daily forecast for a location """ + def get_source_name(self) -> str: return "OpenMeteo" - def get_spec(self) -> Dict: - return { - "name": "get_current_weather", - "description": "Get the current and 7-day daily weather forecast for a location using Open Meteo APIs.", - "parameters": { - "type": "object", - "properties": { - "latitude": { - "type": "string", - "description": "Latitude of the location" - }, - "longitude": { - "type": "string", - "description": "Longitude of the location" + def get_spec(self) -> [Dict]: + return [ + { + "name": "get_current_weather", + "description": "Get the current weather for a location using Open Meteo APIs.", + "parameters": { + "type": "object", + "properties": { + "latitude": {"type": "string", "description": "Latitude of the location"}, + "longitude": {"type": "string", "description": "Longitude of the location"}, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the provided location.", + }, }, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - "description": "The temperature unit to use. Infer this from the provided location.", + "required": ["latitude", "longitude", "unit"], + }, + }, + { + "name": "get_forecast_weather", + "description": "Get daily weather forecast for a location using Open Meteo APIs." + f"Today is {datetime.today().strftime('%A, %B %d, %Y')}", + "parameters": { + "type": "object", + "properties": { + "latitude": {"type": "string", "description": "Latitude of the location"}, + "longitude": {"type": "string", "description": "Longitude of the location"}, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the provided location.", + }, + "forecast_days": { + "type": "integer", + "description": "The number of days to forecast, including today. Default is 7. Max 14. " + "Use 1 for today, 2 for today and tomorrow, and so on.", + }, }, + "required": ["latitude", "longitude", "unit", "forecast_days"], }, - "required": ["latitude", "longitude", "unit"], } - } - - async def execute(self, **kwargs) -> Dict: - url = f'https://api.open-meteo.com/v1/forecast'\ - f'?latitude={kwargs["latitude"]}'\ - f'&longitude={kwargs["longitude"]}'\ - f'&temperature_unit={kwargs["unit"]}' \ - '¤t_weather=true' \ - '&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_mean,' \ - '&forecast_days=7' \ - '&timezone=auto' - return requests.get(url).json() + ] + + async def execute(self, function_name, **kwargs) -> Dict: + url = f'https://api.open-meteo.com/v1/forecast' \ + f'?latitude={kwargs["latitude"]}' \ + f'&longitude={kwargs["longitude"]}' \ + f'&temperature_unit={kwargs["unit"]}' + if function_name == 'get_current_weather': + url += '¤t_weather=true' + return requests.get(url).json() + + elif function_name == 'get_forecast_weather': + url += '&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_mean,' + url += f'&forecast_days={kwargs["forecast_days"]}' + url += '&timezone=auto' + response = requests.get(url).json() + results = {} + for i, time in enumerate(response["daily"]["time"]): + results[datetime.strptime(time, "%Y-%m-%d").strftime("%A, %B %d, %Y")] = { + "weathercode": response["daily"]["weathercode"][i], + "temperature_2m_max": response["daily"]["temperature_2m_max"][i], + "temperature_2m_min": response["daily"]["temperature_2m_min"][i], + "precipitation_probability_mean": response["daily"]["precipitation_probability_mean"][i] + } + return {"today": datetime.today().strftime("%A, %B %d, %Y"), "forecast": results} diff --git a/bot/plugins/web_search.py b/bot/plugins/web_search.py index 17b89e2c..7594d617 100644 --- a/bot/plugins/web_search.py +++ b/bot/plugins/web_search.py @@ -14,8 +14,8 @@ class WebSearchPlugin(Plugin): def get_source_name(self) -> str: return "DuckDuckGo" - def get_spec(self) -> Dict: - return { + def get_spec(self) -> [Dict]: + return [{ "name": "web_search", "description": "Execute a web search for the given query and return a list of results", "parameters": { @@ -28,9 +28,9 @@ def get_spec(self) -> Dict: }, "required": ["query"], }, - } + }] - async def execute(self, **kwargs) -> Dict: + async def execute(self, function_name, **kwargs) -> Dict: with DDGS() as ddgs: ddgs_gen = ddgs.text( kwargs['query'], diff --git a/bot/plugins/wolfram_alpha.py b/bot/plugins/wolfram_alpha.py index f20bb18f..fc97e95c 100644 --- a/bot/plugins/wolfram_alpha.py +++ b/bot/plugins/wolfram_alpha.py @@ -19,8 +19,8 @@ def __init__(self): def get_source_name(self) -> str: return "WolframAlpha" - def get_spec(self) -> Dict: - return { + def get_spec(self) -> [Dict]: + return [{ "name": "answer_with_wolfram_alpha", "description": "Get an answer to a question using Wolfram Alpha. Input should the the query in English.", "parameters": { @@ -30,9 +30,9 @@ def get_spec(self) -> Dict: }, "required": ["query"] } - } + }] - async def execute(self, **kwargs) -> Dict: + async def execute(self, function_name, **kwargs) -> Dict: client = wolframalpha.Client(self.app_id) res = client.query(kwargs['query']) try: diff --git a/bot/telegram_bot.py b/bot/telegram_bot.py index b63411ed..842528b4 100644 --- a/bot/telegram_bot.py +++ b/bot/telegram_bot.py @@ -388,85 +388,85 @@ async def prompt(self, update: Update, context: ContextTypes.DEFAULT_TYPE): total_tokens = 0 if self.config['stream']: - async def _reply(): - nonlocal total_tokens - - stream_response = self.openai.get_chat_response_stream(chat_id=chat_id, query=prompt) - i = 0 - prev = '' - sent_message = None - backoff = 0 - stream_chunk = 0 - - async for content, tokens in stream_response: - if len(content.strip()) == 0: - continue - - stream_chunks = split_into_chunks(content) - if len(stream_chunks) > 1: - content = stream_chunks[-1] - if stream_chunk != len(stream_chunks) - 1: - stream_chunk += 1 - try: - await edit_message_with_retry(context, chat_id, str(sent_message.message_id), - stream_chunks[-2]) - except: - pass - try: - sent_message = await update.effective_message.reply_text( - message_thread_id=get_thread_id(update), - text=content if len(content) > 0 else "..." - ) - except: - pass - continue - - cutoff = get_stream_cutoff_values(update, content) - cutoff += backoff + await update.effective_message.reply_chat_action( + action=constants.ChatAction.TYPING, + message_thread_id=get_thread_id(update) + ) - if i == 0: + stream_response = self.openai.get_chat_response_stream(chat_id=chat_id, query=prompt) + i = 0 + prev = '' + sent_message = None + backoff = 0 + stream_chunk = 0 + + async for content, tokens in stream_response: + if len(content.strip()) == 0: + continue + + stream_chunks = split_into_chunks(content) + if len(stream_chunks) > 1: + content = stream_chunks[-1] + if stream_chunk != len(stream_chunks) - 1: + stream_chunk += 1 + try: + await edit_message_with_retry(context, chat_id, str(sent_message.message_id), + stream_chunks[-2]) + except: + pass try: - if sent_message is not None: - await context.bot.delete_message(chat_id=sent_message.chat_id, - message_id=sent_message.message_id) sent_message = await update.effective_message.reply_text( message_thread_id=get_thread_id(update), - reply_to_message_id=get_reply_to_message_id(self.config, update), - text=content, - disable_web_page_preview=True + text=content if len(content) > 0 else "..." ) except: - continue + pass + continue - elif abs(len(content) - len(prev)) > cutoff or tokens != 'not_finished': - prev = content + cutoff = get_stream_cutoff_values(update, content) + cutoff += backoff - try: - use_markdown = tokens != 'not_finished' - await edit_message_with_retry(context, chat_id, str(sent_message.message_id), - text=content, markdown=use_markdown) + if i == 0: + try: + if sent_message is not None: + await context.bot.delete_message(chat_id=sent_message.chat_id, + message_id=sent_message.message_id) + sent_message = await update.effective_message.reply_text( + message_thread_id=get_thread_id(update), + reply_to_message_id=get_reply_to_message_id(self.config, update), + text=content, + disable_web_page_preview=True + ) + except: + continue - except RetryAfter as e: - backoff += 5 - await asyncio.sleep(e.retry_after) - continue + elif abs(len(content) - len(prev)) > cutoff or tokens != 'not_finished': + prev = content - except TimedOut: - backoff += 5 - await asyncio.sleep(0.5) - continue + try: + use_markdown = tokens != 'not_finished' + await edit_message_with_retry(context, chat_id, str(sent_message.message_id), + text=content, markdown=use_markdown) - except Exception: - backoff += 5 - continue + except RetryAfter as e: + backoff += 5 + await asyncio.sleep(e.retry_after) + continue - await asyncio.sleep(0.01) + except TimedOut: + backoff += 5 + await asyncio.sleep(0.5) + continue - i += 1 - if tokens != 'not_finished': - total_tokens = int(tokens) + except Exception: + backoff += 5 + continue - await wrap_with_indicator(update, context, _reply, constants.ChatAction.TYPING) + await asyncio.sleep(0.01) + + i += 1 + if tokens != 'not_finished': + total_tokens = int(tokens) else: async def _reply(): diff --git a/requirements.txt b/requirements.txt index 3cd7babd..7070ed34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ python-telegram-bot==20.3 requests~=2.31.0 tenacity==8.2.2 wolframalpha==5.0.0 -duckduckgo_search==3.8.3 \ No newline at end of file +duckduckgo_search==3.8.3 +spotipy==2.23.0 \ No newline at end of file From 5ea0e56ea3d5e82c73458605d79cf32ac8baa926 Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 19 Jun 2023 20:06:39 +0200 Subject: [PATCH 12/52] fix broken import --- bot/plugin_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 9abbadf8..609d3e92 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -1,6 +1,5 @@ import json -from plugins.python import PythonPlugin from plugins.spotify import SpotifyPlugin from plugins.crypto import CryptoPlugin from plugins.weather import WeatherPlugin From 39542a93ba35e7a0dedd62b56b32d3f05cca2611 Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 20 Jun 2023 20:32:05 +0200 Subject: [PATCH 13/52] added missing argument --- bot/plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 609d3e92..0e4998e5 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -35,7 +35,7 @@ async def call_function(self, function_name, arguments): plugin = self.__get_plugin_by_function_name(function_name) if not plugin: return json.dumps({'error': f'Function {function_name} not found'}) - return json.dumps(await plugin.execute(**json.loads(arguments))) + return json.dumps(await plugin.execute(function_name, **json.loads(arguments))) def get_plugin_source_name(self, function_name) -> str: """ From fff035c7d837361992157875aeef797ac9be9e07 Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 20 Jun 2023 20:34:26 +0200 Subject: [PATCH 14/52] check if a song is currently playing --- bot/plugins/spotify.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/plugins/spotify.py b/bot/plugins/spotify.py index 19603be1..25234ae0 100644 --- a/bot/plugins/spotify.py +++ b/bot/plugins/spotify.py @@ -136,6 +136,8 @@ def fetch_currently_playing(self) -> Dict: Fetch user's currently playing song from Spotify """ currently_playing = self.spotify.current_user_playing_track() + if not currently_playing: + return {"result": "No song is currently playing"} result = { 'name': currently_playing['item']['name'], 'artist': currently_playing['item']['artists'][0]['name'], From cc9f33b1d3ff7faf97f7ca843233d16d118960ff Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 20 Jun 2023 22:43:41 +0200 Subject: [PATCH 15/52] decrease limit and handle null values --- bot/plugins/spotify.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/bot/plugins/spotify.py b/bot/plugins/spotify.py index 25234ae0..b42b0043 100644 --- a/bot/plugins/spotify.py +++ b/bot/plugins/spotify.py @@ -41,7 +41,7 @@ def get_spec(self) -> [Dict]: } limit_param = { "type": "integer", - "description": "The number of results to return. Max is 50. Default to 10 if not specified." + "description": "The number of results to return. Max is 50. Default to 5 if not specified." "Ignore if action is currently_playing", } type_param = { @@ -114,7 +114,7 @@ def get_spec(self) -> [Dict]: async def execute(self, function_name, **kwargs) -> Dict: time_range = kwargs.get('time_range', 'short_term') - limit = kwargs.get('limit', 10) + limit = kwargs.get('limit', 5) if function_name == 'spotify_get_currently_playing_song': return self.fetch_currently_playing() @@ -155,6 +155,8 @@ def fetch_top_tracks(self, time_range='short_term', limit=5) -> Dict: """ results = [] top_tracks = self.spotify.current_user_top_tracks(limit=limit, time_range=time_range) + if not top_tracks or 'items' not in top_tracks or len(top_tracks['items']) == 0: + return {"results": "No top tracks found"} for item in top_tracks['items']: results.append({ 'name': item['name'], @@ -176,6 +178,8 @@ def fetch_top_artists(self, time_range='short_term', limit=5) -> Dict: """ results = [] top_artists = self.spotify.current_user_top_artists(limit=limit, time_range=time_range) + if not top_artists or 'items' not in top_artists or len(top_artists['items']) == 0: + return {"results": "No top artists found"} for item in top_artists['items']: results.append({ 'name': item['name'], @@ -190,6 +194,9 @@ def search_by_query(self, query, search_type, limit=5) -> Dict: """ results = {} search_response = self.spotify.search(q=query, limit=limit, type=search_type) + if not search_response: + return {"results": "No content found"} + if 'tracks' in search_response: results['tracks'] = [] for item in search_response['tracks']['items']: @@ -233,15 +240,23 @@ def search_by_id(self, content_id, search_type) -> Dict: """ if search_type == 'track': search_response = self.spotify.track(content_id) + if not search_response: + return {"result": "No track found"} return {'result': self._get_track(search_response)} elif search_type == 'artist': search_response = self.spotify.artist(content_id) + if not search_response: + return {"result": "No artisti found"} albums_response = self.spotify.artist_albums(artist_id=content_id, limit=3) + if not albums_response: + albums_response = {"items": []} return {'result': self._get_artist(search_response, albums_response)} elif search_type == 'album': search_response = self.spotify.album(content_id) + if not search_response: + return {"result": "No album found"} return {'result': self._get_album(search_response)} else: From 0b0771aaa4ce66acb666de1bc85bd4b3adfe8f33 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 22 Jun 2023 20:13:02 +0200 Subject: [PATCH 16/52] small improvements --- bot/plugins/spotify.py | 20 +++++++++++--------- bot/plugins/weather.py | 27 +++++++++++++-------------- bot/plugins/web_search.py | 2 +- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/bot/plugins/spotify.py b/bot/plugins/spotify.py index b42b0043..3c0db65c 100644 --- a/bot/plugins/spotify.py +++ b/bot/plugins/spotify.py @@ -36,13 +36,12 @@ def get_spec(self) -> [Dict]: "type": "string", "enum": ["short_term", "medium_term", "long_term"], "description": "The time range of the data to be returned. Short term is the last 4 weeks, " - "medium term is last 6 months, long term is last several years. " - "Ignore if action is currently_playing", + "medium term is last 6 months, long term is last several years. Default to " + "short_term if not specified." } limit_param = { "type": "integer", - "description": "The number of results to return. Max is 50. Default to 5 if not specified." - "Ignore if action is currently_playing", + "description": "The number of results to return. Max is 50. Default to 5 if not specified.", } type_param = { "type": "string", @@ -60,7 +59,7 @@ def get_spec(self) -> [Dict]: }, { "name": "spotify_get_users_top_artists", - "description": "Get the user's top artists", + "description": "Get the user's top listened artists", "parameters": { "type": "object", "properties": { @@ -71,7 +70,7 @@ def get_spec(self) -> [Dict]: }, { "name": "spotify_get_users_top_tracks", - "description": "Get the user's top tracks", + "description": "Get the user's top listened tracks", "parameters": { "type": "object", "properties": { @@ -262,7 +261,8 @@ def search_by_id(self, content_id, search_type) -> Dict: else: return {'error': 'Invalid search type. Must be track, artist or album'} - def _get_artist(self, response, albums): + @staticmethod + def _get_artist(response, albums): return { 'name': response['name'], 'url': response['external_urls']['spotify'], @@ -281,7 +281,8 @@ def _get_artist(self, response, albums): ], } - def _get_track(self, response): + @staticmethod + def _get_track(response): return { 'name': response['name'], 'artist': response['artists'][0]['name'], @@ -295,7 +296,8 @@ def _get_track(self, response): 'explicit': response['explicit'], } - def _get_album(self, response): + @staticmethod + def _get_album(response): return { 'name': response['name'], 'artist': response['artists'][0]['name'], diff --git a/bot/plugins/weather.py b/bot/plugins/weather.py index ad783a32..abfce458 100644 --- a/bot/plugins/weather.py +++ b/bot/plugins/weather.py @@ -15,6 +15,13 @@ def get_source_name(self) -> str: return "OpenMeteo" def get_spec(self) -> [Dict]: + latitude_param = {"type": "string", "description": "Latitude of the location"} + longitude_param = {"type": "string", "description": "Longitude of the location"} + unit_param = { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the provided location.", + } return [ { "name": "get_current_weather", @@ -22,13 +29,9 @@ def get_spec(self) -> [Dict]: "parameters": { "type": "object", "properties": { - "latitude": {"type": "string", "description": "Latitude of the location"}, - "longitude": {"type": "string", "description": "Longitude of the location"}, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - "description": "The temperature unit to use. Infer this from the provided location.", - }, + "latitude": latitude_param, + "longitude": longitude_param, + "unit": unit_param, }, "required": ["latitude", "longitude", "unit"], }, @@ -40,13 +43,9 @@ def get_spec(self) -> [Dict]: "parameters": { "type": "object", "properties": { - "latitude": {"type": "string", "description": "Latitude of the location"}, - "longitude": {"type": "string", "description": "Longitude of the location"}, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - "description": "The temperature unit to use. Infer this from the provided location.", - }, + "latitude": latitude_param, + "longitude": longitude_param, + "unit": unit_param, "forecast_days": { "type": "integer", "description": "The number of days to forecast, including today. Default is 7. Max 14. " diff --git a/bot/plugins/web_search.py b/bot/plugins/web_search.py index 7594d617..0c8bff9f 100644 --- a/bot/plugins/web_search.py +++ b/bot/plugins/web_search.py @@ -37,7 +37,7 @@ async def execute(self, function_name, **kwargs) -> Dict: region='wt-wt', safesearch='off' ) - results = list(islice(ddgs_gen, 8)) + results = list(islice(ddgs_gen, 3)) if results is None or len(results) == 0: return {"Result": "No good DuckDuckGo Search Result was found"} From 37f279e73b47f974ada8fb97f8dc3dce00652218 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 22 Jun 2023 21:13:15 +0200 Subject: [PATCH 17/52] added translate and image-search plugins --- bot/plugin_manager.py | 4 ++++ bot/plugins/images.py | 43 ++++++++++++++++++++++++++++++++++++++++ bot/plugins/translate.py | 31 +++++++++++++++++++++++++++++ bot/telegram_bot.py | 1 - bot/utils.py | 2 -- 5 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 bot/plugins/images.py create mode 100644 bot/plugins/translate.py diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 0e4998e5..21805657 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -1,5 +1,7 @@ import json +from plugins.images import ImageSearchPlugin +from plugins.translate import TranslatePlugin from plugins.spotify import SpotifyPlugin from plugins.crypto import CryptoPlugin from plugins.weather import WeatherPlugin @@ -19,6 +21,8 @@ def __init__(self, config): 'crypto': CryptoPlugin(), 'web_search': WebSearchPlugin(), 'spotify': SpotifyPlugin(), + 'translate': TranslatePlugin(), + 'image_search': ImageSearchPlugin(), } self.plugins = [plugin_mapping[plugin] for plugin in enabled_plugins] diff --git a/bot/plugins/images.py b/bot/plugins/images.py new file mode 100644 index 00000000..d0d52ce3 --- /dev/null +++ b/bot/plugins/images.py @@ -0,0 +1,43 @@ +from itertools import islice +from typing import Dict + +from duckduckgo_search import DDGS + +from .plugin import Plugin + + +class ImageSearchPlugin(Plugin): + """ + A plugin to search images and GIFs for a given query, using DuckDuckGo + """ + def get_source_name(self) -> str: + return "DuckDuckGo Images" + + def get_spec(self) -> [Dict]: + return [{ + "name": "search_images", + "description": "Search image or GIFs for a given query", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "The query to search for"}, + "type": { + "type": "string", + "enum": ["photo", "gif"], + "description": "The type of image to search for. Default to photo if not specified", + } + }, + "required": ["query", "type"], + }, + }] + + async def execute(self, function_name, **kwargs) -> Dict: + with DDGS() as ddgs: + ddgs_images_gen = ddgs.images( + kwargs['query'], + region="wt-wt", + safesearch='off', + type_image=kwargs['type'], + ) + results = list(islice(ddgs_images_gen, 1)) + return {"result": results[0]["image"]} diff --git a/bot/plugins/translate.py b/bot/plugins/translate.py new file mode 100644 index 00000000..c7ee3c33 --- /dev/null +++ b/bot/plugins/translate.py @@ -0,0 +1,31 @@ +from typing import Dict + +from duckduckgo_search import DDGS + +from .plugin import Plugin + + +class TranslatePlugin(Plugin): + """ + A plugin to translate a given text from a language to another, using DuckDuckGo + """ + def get_source_name(self) -> str: + return "DuckDuckGo Translate" + + def get_spec(self) -> [Dict]: + return [{ + "name": "translate", + "description": "Translate a given text from a language to another", + "parameters": { + "type": "object", + "properties": { + "text": {"type": "string", "description": "The text to translate"}, + "to_language": {"type": "string", "description": "The language to translate to (e.g. 'it')"} + }, + "required": ["text", "to_language"], + }, + }] + + async def execute(self, function_name, **kwargs) -> Dict: + with DDGS() as ddgs: + return ddgs.translate(kwargs['text'], to=kwargs['to_language']) diff --git a/bot/telegram_bot.py b/bot/telegram_bot.py index 842528b4..5385977d 100644 --- a/bot/telegram_bot.py +++ b/bot/telegram_bot.py @@ -435,7 +435,6 @@ async def prompt(self, update: Update, context: ContextTypes.DEFAULT_TYPE): message_thread_id=get_thread_id(update), reply_to_message_id=get_reply_to_message_id(self.config, update), text=content, - disable_web_page_preview=True ) except: continue diff --git a/bot/utils.py b/bot/utils.py index 4144c6db..7dafc12d 100644 --- a/bot/utils.py +++ b/bot/utils.py @@ -118,7 +118,6 @@ async def edit_message_with_retry(context: ContextTypes.DEFAULT_TYPE, chat_id: i inline_message_id=message_id if is_inline else None, text=text, parse_mode=constants.ParseMode.MARKDOWN if markdown else None, - disable_web_page_preview=True ) except telegram.error.BadRequest as e: if str(e).startswith("Message is not modified"): @@ -129,7 +128,6 @@ async def edit_message_with_retry(context: ContextTypes.DEFAULT_TYPE, chat_id: i message_id=int(message_id) if not is_inline else None, inline_message_id=message_id if is_inline else None, text=text, - disable_web_page_preview=True ) except Exception as e: logging.warning(f'Failed to edit message: {str(e)}') From 34dd8237341a84d0639f0ac07b7d069a80f558d4 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 22 Jun 2023 21:18:17 +0200 Subject: [PATCH 18/52] updated README.md --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c06e2b14..bd1a8ab2 100644 --- a/README.md +++ b/README.md @@ -111,13 +111,15 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | #### Available plugins -| Name | Description | Required API key(s) | -|--------------|-------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| -| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | `-` | -| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | -| `web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | -| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | `-` | -| `spotify` | Spotify top tracks/artists and currently playing song (powered by [Spotify](https://spotify.com)). Requires one-time auth approval. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | +| Name | Description | Required API key(s) | +|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | `-` | +| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | +| `web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | +| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | `-` | +| `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | +| `translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | +| `image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. From 00cd5096835e927ace4bacb6480e570e46ea46da Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 22 Jun 2023 22:57:36 +0200 Subject: [PATCH 19/52] image type default value --- bot/plugins/images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/plugins/images.py b/bot/plugins/images.py index d0d52ce3..152b2540 100644 --- a/bot/plugins/images.py +++ b/bot/plugins/images.py @@ -37,7 +37,7 @@ async def execute(self, function_name, **kwargs) -> Dict: kwargs['query'], region="wt-wt", safesearch='off', - type_image=kwargs['type'], + type_image=kwargs.get('type', 'photo'), ) results = list(islice(ddgs_images_gen, 1)) return {"result": results[0]["image"]} From 6d32ae916cd5d0a98c1d6f8b251b1eb8e19749d3 Mon Sep 17 00:00:00 2001 From: Bacer Date: Tue, 27 Jun 2023 22:56:31 +0300 Subject: [PATCH 20/52] added deepl plugin --- README.md | 2 ++ bot/plugin_manager.py | 2 ++ bot/plugins/deepl.py | 40 ++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++- 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 bot/plugins/deepl.py diff --git a/README.md b/README.md index bd1a8ab2..ef6bbb8c 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `SPOTIFY_CLIENT_ID` | Spotify app Client ID (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | | `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | | `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | +| `DEEPL_API_KEY` | DeepL API key (required for the `deepl` plugin, you can get one [here](https://www.deepl.com/pro-api?cta=header-pro-api)) | `-` | #### Available plugins | Name | Description | Required API key(s) | @@ -119,6 +120,7 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | `-` | | `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | | `translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | +| `deepl` | Translate text to any language (powered by [DeepL](https://deepl.com)) | `DEEPL_API_KEY` | | `image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 21805657..9eaf3279 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -7,6 +7,7 @@ from plugins.weather import WeatherPlugin from plugins.web_search import WebSearchPlugin from plugins.wolfram_alpha import WolframAlphaPlugin +from plugins.deepl import DeeplTranslatePlugin class PluginManager: @@ -23,6 +24,7 @@ def __init__(self, config): 'spotify': SpotifyPlugin(), 'translate': TranslatePlugin(), 'image_search': ImageSearchPlugin(), + 'deepl': DeeplTranslatePlugin() } self.plugins = [plugin_mapping[plugin] for plugin in enabled_plugins] diff --git a/bot/plugins/deepl.py b/bot/plugins/deepl.py new file mode 100644 index 00000000..0ed655a5 --- /dev/null +++ b/bot/plugins/deepl.py @@ -0,0 +1,40 @@ +import os +from typing import Dict + +import deepl + +from .plugin import Plugin + + +class DeeplTranslatePlugin(Plugin): + """ + A plugin to translate a given text from a language to another, using DeepL + """ + + def __init__(self): + deepl_api_key = os.getenv('DEEPL_API_KEY') + if not deepl_api_key: + raise ValueError('DEEPL_API_KEY environment variable must be set to use DeeplPlugin') + self.api_key = deepl_api_key + + def get_source_name(self) -> str: + return "DeepL Translate" + + def get_spec(self) -> [Dict]: + return [{ + "name": "translate", + "description": "Translate a given text from a language to another", + "parameters": { + "type": "object", + "properties": { + "text": {"type": "string", "description": "The text to translate"}, + "to_language": {"type": "string", "description": "The language to translate to (e.g. 'it')"} + }, + "required": ["text", "to_language"], + }, + }] + + async def execute(self, function_name, **kwargs) -> Dict: + translator = deepl.Translator(self.api_key) + answer = translator.translate_text(kwargs['text'], target_lang=kwargs['to_language']) + return answer.text diff --git a/requirements.txt b/requirements.txt index 7070ed34..d1e32b85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ requests~=2.31.0 tenacity==8.2.2 wolframalpha==5.0.0 duckduckgo_search==3.8.3 -spotipy==2.23.0 \ No newline at end of file +spotipy==2.23.0 +deepl>=1.15.0 \ No newline at end of file From bc721bff82d95173ce03da1da832e960b1db666f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=BE=D1=80=D0=B8=D1=8D=D0=BB=D1=8C=20=D0=9A=D1=80?= =?UTF-8?q?=D1=83=D1=81?= Date: Sun, 2 Jul 2023 15:29:37 +0800 Subject: [PATCH 21/52] feat: worldtimeapi plugin --- bot/plugins/worldtimeapi.py | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 bot/plugins/worldtimeapi.py diff --git a/bot/plugins/worldtimeapi.py b/bot/plugins/worldtimeapi.py new file mode 100644 index 00000000..7fc9dc92 --- /dev/null +++ b/bot/plugins/worldtimeapi.py @@ -0,0 +1,56 @@ +import os +from typing import Dict +from WorldTimeAPI import service as serv + +from .plugin import Plugin + +class WorldTimeApiPlugin(Plugin): + """ + A plugin to get the current time from a given timezone, using WorldTimeAPI + """ + + def __init__(self): + wta_timezone = os.getenv('WORLDTIME_DEFAULT_TIMEZONE') + if not wta_timezone: + raise ValueError('WORLDTIME_DEFAULT_TIMEZONE environment variable must be set to use WorldTimeApiPlugin') + self.plugin_tz = wta_timezone + + def get_source_name(self) -> str: + return "WorldTimeAPI" + + def get_spec(self) -> [Dict]: + return [{ + "name": "worldtimeapi", + "description": "Get the current time from a given timezone. use " + self.plugin_tz + " if not specified. include 12hr format in the response.", + "parameters": { + "type": "object", + "properties": { + "area": { + "type": "string", + "description": "the continent or region of the location" + }, + "location": { + "type": "string", + "description": "the city" + } + }, + "required": ["area", "location"], + }, + }] + + async def execute(self, function_name, **kwargs) -> Dict: + wtime = serv.Client('timezone') + + requests = { + "area": kwargs['area'], + "location": kwargs['location'] + } + + response = wtime.get(**requests) + + try: + res = response.datetime + except: + res = {"result": "No WorldTimeAPI result was found"} + + return res From 3d9793d3603306a526f8f1849c42db5624b07dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=BE=D1=80=D0=B8=D1=8D=D0=BB=D1=8C=20=D0=9A=D1=80?= =?UTF-8?q?=D1=83=D1=81?= Date: Sun, 2 Jul 2023 15:30:29 +0800 Subject: [PATCH 22/52] feat: worldtimeapi plugin --- bot/plugin_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 21805657..4f9b5a1b 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -7,7 +7,7 @@ from plugins.weather import WeatherPlugin from plugins.web_search import WebSearchPlugin from plugins.wolfram_alpha import WolframAlphaPlugin - +from plugins.worldtimeapi import WorldTimeApiPlugin class PluginManager: """ @@ -23,6 +23,7 @@ def __init__(self, config): 'spotify': SpotifyPlugin(), 'translate': TranslatePlugin(), 'image_search': ImageSearchPlugin(), + 'worldtimeapi': WorldTimeApiPlugin(), } self.plugins = [plugin_mapping[plugin] for plugin in enabled_plugins] From 9bd36c984534732260bc741b1ab058528747e231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=BE=D1=80=D0=B8=D1=8D=D0=BB=D1=8C=20=D0=9A=D1=80?= =?UTF-8?q?=D1=83=D1=81?= Date: Sun, 2 Jul 2023 15:44:13 +0800 Subject: [PATCH 23/52] fix: worldtimeapi.py desc --- bot/plugins/worldtimeapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/plugins/worldtimeapi.py b/bot/plugins/worldtimeapi.py index 7fc9dc92..5bebca0a 100644 --- a/bot/plugins/worldtimeapi.py +++ b/bot/plugins/worldtimeapi.py @@ -21,7 +21,7 @@ def get_source_name(self) -> str: def get_spec(self) -> [Dict]: return [{ "name": "worldtimeapi", - "description": "Get the current time from a given timezone. use " + self.plugin_tz + " if not specified. include 12hr format in the response.", + "description": f"Get the current time from a given timezone. include 12hr format in the response. use {self.plugin_tz} if timezone not specified.", "parameters": { "type": "object", "properties": { From d56ba3e91060c651a4fb3c31e656224475116a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=BE=D1=80=D0=B8=D1=8D=D0=BB=D1=8C=20=D0=9A=D1=80?= =?UTF-8?q?=D1=83=D1=81?= Date: Sun, 2 Jul 2023 16:28:57 +0800 Subject: [PATCH 24/52] fix: worldtimeapi.py desc --- bot/plugins/worldtimeapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/plugins/worldtimeapi.py b/bot/plugins/worldtimeapi.py index 5bebca0a..d76e91be 100644 --- a/bot/plugins/worldtimeapi.py +++ b/bot/plugins/worldtimeapi.py @@ -21,7 +21,7 @@ def get_source_name(self) -> str: def get_spec(self) -> [Dict]: return [{ "name": "worldtimeapi", - "description": f"Get the current time from a given timezone. include 12hr format in the response. use {self.plugin_tz} if timezone not specified.", + "description": f"Get the current time from a given timezone. use 12hr time format in the response. use {self.plugin_tz} if timezone or location is not specified.", "parameters": { "type": "object", "properties": { From c46fc67c6c84622b8b6fb9ccdf3024be99afcec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=BE=D1=80=D0=B8=D1=8D=D0=BB=D1=8C=20=D0=9A=D1=80?= =?UTF-8?q?=D1=83=D1=81?= Date: Sun, 2 Jul 2023 21:46:59 +0800 Subject: [PATCH 25/52] refactor: use plain http requests to get time from API --- bot/plugins/worldtimeapi.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/bot/plugins/worldtimeapi.py b/bot/plugins/worldtimeapi.py index d76e91be..52f8ced6 100644 --- a/bot/plugins/worldtimeapi.py +++ b/bot/plugins/worldtimeapi.py @@ -1,7 +1,6 @@ -import os +import os, requests from typing import Dict -from WorldTimeAPI import service as serv - +from datetime import datetime from .plugin import Plugin class WorldTimeApiPlugin(Plugin): @@ -13,7 +12,7 @@ def __init__(self): wta_timezone = os.getenv('WORLDTIME_DEFAULT_TIMEZONE') if not wta_timezone: raise ValueError('WORLDTIME_DEFAULT_TIMEZONE environment variable must be set to use WorldTimeApiPlugin') - self.plugin_tz = wta_timezone + self.defTz = wta_timezone.split('/'); def get_source_name(self) -> str: return "WorldTimeAPI" @@ -21,17 +20,17 @@ def get_source_name(self) -> str: def get_spec(self) -> [Dict]: return [{ "name": "worldtimeapi", - "description": f"Get the current time from a given timezone. use 12hr time format in the response. use {self.plugin_tz} if timezone or location is not specified.", + "description": f"Get the current time from a given timezone", "parameters": { "type": "object", "properties": { "area": { "type": "string", - "description": "the continent or region of the location" + "description": f"the continent of timezone identifier. use {self.defTz[0]} if not specified." }, "location": { "type": "string", - "description": "the city" + "description": f"the city/region of timezone identifier. use {self.defTz[1]} if not specified." } }, "required": ["area", "location"], @@ -39,17 +38,22 @@ def get_spec(self) -> [Dict]: }] async def execute(self, function_name, **kwargs) -> Dict: - wtime = serv.Client('timezone') - - requests = { - "area": kwargs['area'], - "location": kwargs['location'] - } + areaVal = kwargs.get('area', self.defTz[0]) + locVal = kwargs.get('location', self.defTz[1]) - response = wtime.get(**requests) + url = f'https://worldtimeapi.org/api/timezone/{areaVal}/{locVal}' try: - res = response.datetime + wtr = requests.get(url).json().get('datetime') + wtr_obj = datetime.strptime(wtr, "%Y-%m-%dT%H:%M:%S.%f%z") + + time_24hr = wtr_obj.strftime("%H:%M:%S") + time_12hr = wtr_obj.strftime("%I:%M:%S %p") + + res = { + "24hr": time_24hr, + "12hr": time_12hr + } except: res = {"result": "No WorldTimeAPI result was found"} From 9971a3f1ce117a6a65dfd9e8484887c94681cc63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=BE=D1=80=D0=B8=D1=8D=D0=BB=D1=8C=20=D0=9A=D1=80?= =?UTF-8?q?=D1=83=D1=81?= Date: Mon, 3 Jul 2023 02:15:24 +0800 Subject: [PATCH 26/52] docs: added worldtimeapi --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd1a8ab2..836791e3 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `SPOTIFY_CLIENT_ID` | Spotify app Client ID (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | | `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | | `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | +| `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use (required for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | `-` | #### Available plugins | Name | Description | Required API key(s) | @@ -119,7 +120,10 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | `-` | | `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | | `translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | -| `image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | +| `image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) + | `-` | +| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) + | `-` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. From fdb4dc8f044b04c8ae6a23ac624ff0e703371a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=BE=D1=80=D0=B8=D1=8D=D0=BB=D1=8C=20=D0=9A=D1=80?= =?UTF-8?q?=D1=83=D1=81?= Date: Mon, 3 Jul 2023 02:17:14 +0800 Subject: [PATCH 27/52] docs: update worldtimeapi --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 836791e3..b943b791 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `SPOTIFY_CLIENT_ID` | Spotify app Client ID (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | | `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | | `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | -| `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use (required for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | `-` | +| `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use (required for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | `-` | #### Available plugins | Name | Description | Required API key(s) | @@ -120,10 +120,8 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | `-` | | `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | | `translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | -| `image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) - | `-` | -| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) - | `-` | +| `image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | +| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `-` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. From 5028148858275d30f0f3bcaf88247c837ca77638 Mon Sep 17 00:00:00 2001 From: ned Date: Sun, 2 Jul 2023 20:27:54 +0200 Subject: [PATCH 28/52] only initialize enabled plugins --- bot/plugin_manager.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 21805657..d1e9e71f 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -16,15 +16,15 @@ class PluginManager: def __init__(self, config): enabled_plugins = config.get('plugins', []) plugin_mapping = { - 'wolfram': WolframAlphaPlugin(), - 'weather': WeatherPlugin(), - 'crypto': CryptoPlugin(), - 'web_search': WebSearchPlugin(), - 'spotify': SpotifyPlugin(), - 'translate': TranslatePlugin(), - 'image_search': ImageSearchPlugin(), + 'wolfram': WolframAlphaPlugin, + 'weather': WeatherPlugin, + 'crypto': CryptoPlugin, + 'web_search': WebSearchPlugin, + 'spotify': SpotifyPlugin, + 'translate': TranslatePlugin, + 'image_search': ImageSearchPlugin, } - self.plugins = [plugin_mapping[plugin] for plugin in enabled_plugins] + self.plugins = [plugin_mapping[plugin]() for plugin in enabled_plugins if plugin in plugin_mapping] def get_functions_specs(self): """ From ff8101b26cb36cea661c0ee4cb045536c90f03c5 Mon Sep 17 00:00:00 2001 From: ned Date: Sun, 2 Jul 2023 20:48:12 +0200 Subject: [PATCH 29/52] small improvements to the worldtimeapi plugin --- README.md | 36 ++++++++++++++++++------------------ bot/plugin_manager.py | 2 ++ bot/plugins/worldtimeapi.py | 36 +++++++++++++----------------------- 3 files changed, 33 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index b943b791..1677e29c 100644 --- a/README.md +++ b/README.md @@ -99,29 +99,29 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `BOT_LANGUAGE` | Language of general bot messages. Currently available: `en`, `de`, `ru`, `tr`, `it`, `fi`, `es`, `id`, `nl`, `zh-cn`, `zh-tw`, `vi`, `fa`, `pt-br`, `uk`. [Contribute with additional translations](https://github.com/n3d1117/chatgpt-telegram-bot/discussions/219) | `en` | #### Functions -| Parameter | Description | Default value | -|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| -| `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | -| `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response before displaying a user-facing message | `10` | -| `PLUGINS` | List of plugins to enable (see below for a full list), e.g: `PLUGINS=wolfram,weather` | `-` | -| `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | -| `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation)) | `-` | -| `SPOTIFY_CLIENT_ID` | Spotify app Client ID (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | -| `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | -| `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | -| `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use (required for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | `-` | +| Parameter | Description | Default value | +|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| +| `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | +| `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response before displaying a user-facing message | `10` | +| `PLUGINS` | List of plugins to enable (see below for a full list), e.g: `PLUGINS=wolfram,weather` | - | +| `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | +| `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required only for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation)) | - | +| `SPOTIFY_CLIENT_ID` | Spotify app Client ID (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | +| `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | +| `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | +| `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use, i.e. `Europe/Rome` (required only for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | - | #### Available plugins -| Name | Description | Required API key(s) | +| Name | Description | Required environment variable(s) | |----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| -| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | `-` | +| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | - | | `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | -| `web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | -| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | `-` | +| `web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | - | | `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | -| `translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | -| `image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | `-` | -| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `-` | +| `translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `WORLDTIME_DEFAULT_TIMEZONE` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index ccf6ec6e..eff71b06 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -9,10 +9,12 @@ from plugins.wolfram_alpha import WolframAlphaPlugin from plugins.worldtimeapi import WorldTimeApiPlugin + class PluginManager: """ A class to manage the plugins and call the correct functions """ + def __init__(self, config): enabled_plugins = config.get('plugins', []) plugin_mapping = { diff --git a/bot/plugins/worldtimeapi.py b/bot/plugins/worldtimeapi.py index 52f8ced6..9c866581 100644 --- a/bot/plugins/worldtimeapi.py +++ b/bot/plugins/worldtimeapi.py @@ -1,18 +1,20 @@ import os, requests from typing import Dict from datetime import datetime + from .plugin import Plugin + class WorldTimeApiPlugin(Plugin): """ A plugin to get the current time from a given timezone, using WorldTimeAPI """ def __init__(self): - wta_timezone = os.getenv('WORLDTIME_DEFAULT_TIMEZONE') - if not wta_timezone: + default_timezone = os.getenv('WORLDTIME_DEFAULT_TIMEZONE') + if not default_timezone: raise ValueError('WORLDTIME_DEFAULT_TIMEZONE environment variable must be set to use WorldTimeApiPlugin') - self.defTz = wta_timezone.split('/'); + self.default_timezone = default_timezone def get_source_name(self) -> str: return "WorldTimeAPI" @@ -24,37 +26,25 @@ def get_spec(self) -> [Dict]: "parameters": { "type": "object", "properties": { - "area": { - "type": "string", - "description": f"the continent of timezone identifier. use {self.defTz[0]} if not specified." - }, - "location": { + "timezone": { "type": "string", - "description": f"the city/region of timezone identifier. use {self.defTz[1]} if not specified." + "description": f"The timezone identifier (e.g: `Europe/Rome`). Infer this from the location." + f"Use {self.default_timezone} if not specified." } }, - "required": ["area", "location"], + "required": ["timezone"], }, }] async def execute(self, function_name, **kwargs) -> Dict: - areaVal = kwargs.get('area', self.defTz[0]) - locVal = kwargs.get('location', self.defTz[1]) + timezone = kwargs.get('timezone', self.default_timezone) + url = f'https://worldtimeapi.org/api/timezone/{timezone}' - url = f'https://worldtimeapi.org/api/timezone/{areaVal}/{locVal}' - try: wtr = requests.get(url).json().get('datetime') wtr_obj = datetime.strptime(wtr, "%Y-%m-%dT%H:%M:%S.%f%z") - time_24hr = wtr_obj.strftime("%H:%M:%S") time_12hr = wtr_obj.strftime("%I:%M:%S %p") - - res = { - "24hr": time_24hr, - "12hr": time_12hr - } + return {"24hr": time_24hr, "12hr": time_12hr} except: - res = {"result": "No WorldTimeAPI result was found"} - - return res + return {"result": "No result was found"} From 710f1eaaa72af63125daf62ee6470f9b5fc0f354 Mon Sep 17 00:00:00 2001 From: ned Date: Sun, 2 Jul 2023 22:57:05 +0200 Subject: [PATCH 30/52] update naming --- README.md | 20 +++++++++---------- bot/plugin_manager.py | 12 +++++------ .../{images.py => ddg_image_search.py} | 4 ++-- .../{translate.py => ddg_translate.py} | 2 +- .../{web_search.py => ddg_web_search.py} | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) rename bot/plugins/{images.py => ddg_image_search.py} (94%) rename bot/plugins/{translate.py => ddg_translate.py} (96%) rename bot/plugins/{web_search.py => ddg_web_search.py} (97%) diff --git a/README.md b/README.md index 1677e29c..be06735b 100644 --- a/README.md +++ b/README.md @@ -112,16 +112,16 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use, i.e. `Europe/Rome` (required only for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | - | #### Available plugins -| Name | Description | Required environment variable(s) | -|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| -| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | - | -| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | -| `web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | - | -| `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | -| `translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `WORLDTIME_DEFAULT_TIMEZONE` | +| Name | Description | Required environment variable(s) | +|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | - | +| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | +| `ddg_web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `ddg_translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `ddg_image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | - | +| `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | +| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `WORLDTIME_DEFAULT_TIMEZONE` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index eff71b06..2f37e780 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -1,11 +1,11 @@ import json -from plugins.images import ImageSearchPlugin -from plugins.translate import TranslatePlugin +from plugins.ddg_image_search import DDGImageSearchPlugin +from plugins.ddg_translate import DDGTranslatePlugin from plugins.spotify import SpotifyPlugin from plugins.crypto import CryptoPlugin from plugins.weather import WeatherPlugin -from plugins.web_search import WebSearchPlugin +from plugins.ddg_web_search import DDGWebSearchPlugin from plugins.wolfram_alpha import WolframAlphaPlugin from plugins.worldtimeapi import WorldTimeApiPlugin @@ -21,10 +21,10 @@ def __init__(self, config): 'wolfram': WolframAlphaPlugin, 'weather': WeatherPlugin, 'crypto': CryptoPlugin, - 'web_search': WebSearchPlugin, + 'ddg_web_search': DDGWebSearchPlugin, + 'ddg_translate': DDGTranslatePlugin, + 'ddg_image_search': DDGImageSearchPlugin, 'spotify': SpotifyPlugin, - 'translate': TranslatePlugin, - 'image_search': ImageSearchPlugin, 'worldtimeapi': WorldTimeApiPlugin, } self.plugins = [plugin_mapping[plugin]() for plugin in enabled_plugins if plugin in plugin_mapping] diff --git a/bot/plugins/images.py b/bot/plugins/ddg_image_search.py similarity index 94% rename from bot/plugins/images.py rename to bot/plugins/ddg_image_search.py index 152b2540..8ef96de9 100644 --- a/bot/plugins/images.py +++ b/bot/plugins/ddg_image_search.py @@ -6,7 +6,7 @@ from .plugin import Plugin -class ImageSearchPlugin(Plugin): +class DDGImageSearchPlugin(Plugin): """ A plugin to search images and GIFs for a given query, using DuckDuckGo """ @@ -24,7 +24,7 @@ def get_spec(self) -> [Dict]: "type": { "type": "string", "enum": ["photo", "gif"], - "description": "The type of image to search for. Default to photo if not specified", + "description": "The type of image to search for. Default to `photo` if not specified", } }, "required": ["query", "type"], diff --git a/bot/plugins/translate.py b/bot/plugins/ddg_translate.py similarity index 96% rename from bot/plugins/translate.py rename to bot/plugins/ddg_translate.py index c7ee3c33..c5b1211d 100644 --- a/bot/plugins/translate.py +++ b/bot/plugins/ddg_translate.py @@ -5,7 +5,7 @@ from .plugin import Plugin -class TranslatePlugin(Plugin): +class DDGTranslatePlugin(Plugin): """ A plugin to translate a given text from a language to another, using DuckDuckGo """ diff --git a/bot/plugins/web_search.py b/bot/plugins/ddg_web_search.py similarity index 97% rename from bot/plugins/web_search.py rename to bot/plugins/ddg_web_search.py index 0c8bff9f..6cc84ae3 100644 --- a/bot/plugins/web_search.py +++ b/bot/plugins/ddg_web_search.py @@ -6,7 +6,7 @@ from .plugin import Plugin -class WebSearchPlugin(Plugin): +class DDGWebSearchPlugin(Plugin): """ A plugin to search the web for a given query, using DuckDuckGo """ From 694d81563dc4e321fafe2637d26f6b5d0c0d87bf Mon Sep 17 00:00:00 2001 From: Bacer Date: Mon, 3 Jul 2023 00:05:34 +0300 Subject: [PATCH 31/52] drop deepl api --- bot/plugins/deepl.py | 15 +++++++++++---- requirements.txt | 3 +-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/bot/plugins/deepl.py b/bot/plugins/deepl.py index 0ed655a5..ff2c78f2 100644 --- a/bot/plugins/deepl.py +++ b/bot/plugins/deepl.py @@ -1,7 +1,7 @@ import os from typing import Dict -import deepl +import requests from .plugin import Plugin @@ -35,6 +35,13 @@ def get_spec(self) -> [Dict]: }] async def execute(self, function_name, **kwargs) -> Dict: - translator = deepl.Translator(self.api_key) - answer = translator.translate_text(kwargs['text'], target_lang=kwargs['to_language']) - return answer.text + translator = requests.get( + "https://api.deepl.com/v2/translate", + params={ + "auth_key": self.api_key, + "target_lang": kwargs['to_language'], + "text": kwargs['text'], + }, + ) + translated_text = translator.json()["translations"][0]["text"] + return translated_text diff --git a/requirements.txt b/requirements.txt index d1e32b85..7070ed34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,4 @@ requests~=2.31.0 tenacity==8.2.2 wolframalpha==5.0.0 duckduckgo_search==3.8.3 -spotipy==2.23.0 -deepl>=1.15.0 \ No newline at end of file +spotipy==2.23.0 \ No newline at end of file From cdd02af0f79ebc6d63aa17c900f4e1681c6488d2 Mon Sep 17 00:00:00 2001 From: Bacer Date: Mon, 3 Jul 2023 00:38:34 +0300 Subject: [PATCH 32/52] fix bugs --- bot/plugins/deepl.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/bot/plugins/deepl.py b/bot/plugins/deepl.py index ff2c78f2..50507441 100644 --- a/bot/plugins/deepl.py +++ b/bot/plugins/deepl.py @@ -13,9 +13,11 @@ class DeeplTranslatePlugin(Plugin): def __init__(self): deepl_api_key = os.getenv('DEEPL_API_KEY') - if not deepl_api_key: - raise ValueError('DEEPL_API_KEY environment variable must be set to use DeeplPlugin') + deepl_api_pro = os.getenv('DEEPL_API_PRO', 'false').lower() == 'true' + if not deepl_api_key or not deepl_api_pro: + raise ValueError('DEEPL_API_KEY and DEEPL_API_PLAN environment variable must be set to use DeepL Plugin') self.api_key = deepl_api_key + self.api_pro = deepl_api_pro def get_source_name(self) -> str: return "DeepL Translate" @@ -35,13 +37,20 @@ def get_spec(self) -> [Dict]: }] async def execute(self, function_name, **kwargs) -> Dict: - translator = requests.get( - "https://api.deepl.com/v2/translate", - params={ - "auth_key": self.api_key, - "target_lang": kwargs['to_language'], - "text": kwargs['text'], - }, - ) - translated_text = translator.json()["translations"][0]["text"] - return translated_text + if self.api_pro: + url = "https://api.deepl.com/v2/translate" + else: + url = "https://api-free.deepl.com/v2/translate" + + headers = { + "Authorization": f"DeepL-Auth-Key {self.api_key}", + "User-Agent": "chatgpt-telegram-bot/0.2.7", + "Content-Type": "application/x-www-form-urlencoded" + } + + data = { + "text": kwargs['text'], + "target_lang": kwargs['to_language'] + } + + return requests.post(url, headers=headers, data=data).json()["translations"][0]["text"] From 229b3424d13526e41b028989f0e1fcc5aa208487 Mon Sep 17 00:00:00 2001 From: Bacer Date: Mon, 3 Jul 2023 00:47:56 +0300 Subject: [PATCH 33/52] fix --- bot/plugins/deepl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/plugins/deepl.py b/bot/plugins/deepl.py index 50507441..40e6c789 100644 --- a/bot/plugins/deepl.py +++ b/bot/plugins/deepl.py @@ -13,9 +13,9 @@ class DeeplTranslatePlugin(Plugin): def __init__(self): deepl_api_key = os.getenv('DEEPL_API_KEY') - deepl_api_pro = os.getenv('DEEPL_API_PRO', 'false').lower() == 'true' + deepl_api_pro = os.getenv('DEEPL_API_PRO').lower() == 'true' if not deepl_api_key or not deepl_api_pro: - raise ValueError('DEEPL_API_KEY and DEEPL_API_PLAN environment variable must be set to use DeepL Plugin') + raise ValueError('DEEPL_API_KEY and DEEPL_API_PRO environment variable must be set to use DeepL Plugin') self.api_key = deepl_api_key self.api_pro = deepl_api_pro From 0855ee2499ca74ddac90a87f003eb350d80e6030 Mon Sep 17 00:00:00 2001 From: Bacer Date: Mon, 3 Jul 2023 00:50:02 +0300 Subject: [PATCH 34/52] fix --- bot/plugins/deepl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/plugins/deepl.py b/bot/plugins/deepl.py index 40e6c789..533db7a8 100644 --- a/bot/plugins/deepl.py +++ b/bot/plugins/deepl.py @@ -13,9 +13,9 @@ class DeeplTranslatePlugin(Plugin): def __init__(self): deepl_api_key = os.getenv('DEEPL_API_KEY') - deepl_api_pro = os.getenv('DEEPL_API_PRO').lower() == 'true' - if not deepl_api_key or not deepl_api_pro: - raise ValueError('DEEPL_API_KEY and DEEPL_API_PRO environment variable must be set to use DeepL Plugin') + deepl_api_pro = os.getenv('DEEPL_API_PRO', 'false').lower() == 'true' + if not deepl_api_key: + raise ValueError('DEEPL_API_KEY environment variable must be set to use DeepL Plugin') self.api_key = deepl_api_key self.api_pro = deepl_api_pro From 4751c9fd0429cbf423e2e7759e1b9a787fdfd916 Mon Sep 17 00:00:00 2001 From: Bacer Date: Mon, 3 Jul 2023 00:56:46 +0300 Subject: [PATCH 35/52] add readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ef6bbb8c..0ec93f6f 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | | `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | | `DEEPL_API_KEY` | DeepL API key (required for the `deepl` plugin, you can get one [here](https://www.deepl.com/pro-api?cta=header-pro-api)) | `-` | +| `DEEPL_API_PRO` | DeepL API key plan (required for the `deepl` plugin. set 'true' if you have paid api key) | `false` | #### Available plugins | Name | Description | Required API key(s) | From 215229401918a3ea4d4f9595223fad2e761a6943 Mon Sep 17 00:00:00 2001 From: Bacer Date: Mon, 3 Jul 2023 01:08:05 +0300 Subject: [PATCH 36/52] del deepl_api_pro var --- README.md | 1 - bot/plugins/deepl.py | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0ec93f6f..ef6bbb8c 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,6 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | | `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | `-` | | `DEEPL_API_KEY` | DeepL API key (required for the `deepl` plugin, you can get one [here](https://www.deepl.com/pro-api?cta=header-pro-api)) | `-` | -| `DEEPL_API_PRO` | DeepL API key plan (required for the `deepl` plugin. set 'true' if you have paid api key) | `false` | #### Available plugins | Name | Description | Required API key(s) | diff --git a/bot/plugins/deepl.py b/bot/plugins/deepl.py index 533db7a8..b58d271b 100644 --- a/bot/plugins/deepl.py +++ b/bot/plugins/deepl.py @@ -13,11 +13,9 @@ class DeeplTranslatePlugin(Plugin): def __init__(self): deepl_api_key = os.getenv('DEEPL_API_KEY') - deepl_api_pro = os.getenv('DEEPL_API_PRO', 'false').lower() == 'true' if not deepl_api_key: raise ValueError('DEEPL_API_KEY environment variable must be set to use DeepL Plugin') self.api_key = deepl_api_key - self.api_pro = deepl_api_pro def get_source_name(self) -> str: return "DeepL Translate" @@ -37,10 +35,10 @@ def get_spec(self) -> [Dict]: }] async def execute(self, function_name, **kwargs) -> Dict: - if self.api_pro: - url = "https://api.deepl.com/v2/translate" - else: + if self.api_key.endswith(':fx'): url = "https://api-free.deepl.com/v2/translate" + else: + url = "https://api.deepl.com/v2/translate" headers = { "Authorization": f"DeepL-Auth-Key {self.api_key}", From 0597b8ef35b31039c364a4d4c8e5a9e3ee583b5e Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 3 Jul 2023 22:18:43 +0200 Subject: [PATCH 37/52] added youtube audio extractor, dice, and direct response for images --- README.md | 23 +++++---- bot/openai_helper.py | 20 ++++++-- bot/plugin_manager.py | 4 ++ bot/plugins/ddg_image_search.py | 43 +++++++++++++--- bot/plugins/ddg_web_search.py | 22 ++++++-- bot/plugins/dice.py | 38 ++++++++++++++ bot/plugins/worldtimeapi.py | 1 - bot/plugins/youtube_audio_extractor.py | 44 ++++++++++++++++ bot/telegram_bot.py | 26 +++++++++- bot/utils.py | 69 ++++++++++++++++++++++++++ requirements.txt | 3 +- translations.json | 48 ++++++++++++------ 12 files changed, 300 insertions(+), 41 deletions(-) create mode 100644 bot/plugins/dice.py create mode 100644 bot/plugins/youtube_audio_extractor.py diff --git a/README.md b/README.md index be06735b..7fbeab6c 100644 --- a/README.md +++ b/README.md @@ -110,18 +110,21 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | | `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | | `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use, i.e. `Europe/Rome` (required only for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | - | +| `DUCKDUCKGO_SAFESEARCH` | DuckDuckGo safe search (`on`, `off` or `moderate`) (optional, applies to `ddg_web_search` and `ddg_image_search`) | `moderate` | #### Available plugins -| Name | Description | Required environment variable(s) | -|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| -| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | - | -| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | -| `ddg_web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `ddg_translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `ddg_image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | - | -| `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | -| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `WORLDTIME_DEFAULT_TIMEZONE` | +| Name | Description | Required environment variable(s) | +|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | - | +| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | +| `ddg_web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `ddg_translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `ddg_image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | - | +| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | - | +| `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | +| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `WORLDTIME_DEFAULT_TIMEZONE` | +| `dice` | Send a dice in the chat! | - | +| `youtube_audio_extractor` | Extract audio from YouTube videos | - | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. diff --git a/bot/openai_helper.py b/bot/openai_helper.py index 67ab7686..61fabf50 100644 --- a/bot/openai_helper.py +++ b/bot/openai_helper.py @@ -14,6 +14,7 @@ from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type +from bot.utils import is_direct_result from plugin_manager import PluginManager # Models can be found here: https://platform.openai.com/docs/models/overview @@ -118,6 +119,9 @@ async def get_chat_response(self, chat_id: int, query: str) -> tuple[str, str]: response = await self.__common_get_chat_response(chat_id, query) if self.config['enable_functions']: response, plugins_used = await self.__handle_function_call(chat_id, response) + if is_direct_result(response): + return response, '0' + answer = '' if len(response.choices) > 1 and self.config['n_choices'] > 1: @@ -158,6 +162,9 @@ async def get_chat_response_stream(self, chat_id: int, query: str): response = await self.__common_get_chat_response(chat_id, query, stream=True) if self.config['enable_functions']: response, plugins_used = await self.__handle_function_call(chat_id, response, stream=True) + if is_direct_result(response): + yield response, '0' + return answer = '' async for item in response: @@ -283,7 +290,16 @@ async def __handle_function_call(self, chat_id, response, stream=False, times=0, logging.info(f'Calling function {function_name} with arguments {arguments}') function_response = await self.plugin_manager.call_function(function_name, arguments) - logging.info(f'Got response {function_response}') + + if function_name not in plugins_used: + plugins_used += (function_name,) + + if is_direct_result(function_response): + self.__add_function_call_to_history(chat_id=chat_id, function_name=function_name, + content=json.dumps({'result': 'Done, the content has been sent' + 'to the user.'})) + return function_response, plugins_used + self.__add_function_call_to_history(chat_id=chat_id, function_name=function_name, content=function_response) response = await openai.ChatCompletion.acreate( model=self.config['model'], @@ -292,8 +308,6 @@ async def __handle_function_call(self, chat_id, response, stream=False, times=0, function_call='auto' if times < self.config['functions_max_consecutive_calls'] else 'none', stream=stream ) - if function_name not in plugins_used: - plugins_used += (function_name,) return await self.__handle_function_call(chat_id, response, stream, times + 1, plugins_used) async def generate_image(self, prompt: str) -> tuple[str, str]: diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 2f37e780..e54a7d4c 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -1,5 +1,7 @@ import json +from bot.plugins.dice import DicePlugin +from bot.plugins.youtube_audio_extractor import YouTubeAudioExtractorPlugin from plugins.ddg_image_search import DDGImageSearchPlugin from plugins.ddg_translate import DDGTranslatePlugin from plugins.spotify import SpotifyPlugin @@ -26,6 +28,8 @@ def __init__(self, config): 'ddg_image_search': DDGImageSearchPlugin, 'spotify': SpotifyPlugin, 'worldtimeapi': WorldTimeApiPlugin, + 'youtube_audio_extractor': YouTubeAudioExtractorPlugin, + 'dice': DicePlugin, } self.plugins = [plugin_mapping[plugin]() for plugin in enabled_plugins if plugin in plugin_mapping] diff --git a/bot/plugins/ddg_image_search.py b/bot/plugins/ddg_image_search.py index 8ef96de9..61918243 100644 --- a/bot/plugins/ddg_image_search.py +++ b/bot/plugins/ddg_image_search.py @@ -1,3 +1,5 @@ +import os +import random from itertools import islice from typing import Dict @@ -10,6 +12,9 @@ class DDGImageSearchPlugin(Plugin): """ A plugin to search images and GIFs for a given query, using DuckDuckGo """ + def __init__(self): + self.safesearch = os.getenv('DUCKDUCKGO_SAFESEARCH', 'moderate') + def get_source_name(self) -> str: return "DuckDuckGo Images" @@ -25,19 +30,45 @@ def get_spec(self) -> [Dict]: "type": "string", "enum": ["photo", "gif"], "description": "The type of image to search for. Default to `photo` if not specified", + }, + "region": { + "type": "string", + "enum": ['xa-ar', 'xa-en', 'ar-es', 'au-en', 'at-de', 'be-fr', 'be-nl', 'br-pt', 'bg-bg', + 'ca-en', 'ca-fr', 'ct-ca', 'cl-es', 'cn-zh', 'co-es', 'hr-hr', 'cz-cs', 'dk-da', + 'ee-et', 'fi-fi', 'fr-fr', 'de-de', 'gr-el', 'hk-tzh', 'hu-hu', 'in-en', 'id-id', + 'id-en', 'ie-en', 'il-he', 'it-it', 'jp-jp', 'kr-kr', 'lv-lv', 'lt-lt', 'xl-es', + 'my-ms', 'my-en', 'mx-es', 'nl-nl', 'nz-en', 'no-no', 'pe-es', 'ph-en', 'ph-tl', + 'pl-pl', 'pt-pt', 'ro-ro', 'ru-ru', 'sg-en', 'sk-sk', 'sl-sl', 'za-en', 'es-es', + 'se-sv', 'ch-de', 'ch-fr', 'ch-it', 'tw-tzh', 'th-th', 'tr-tr', 'ua-uk', 'uk-en', + 'us-en', 'ue-es', 've-es', 'vn-vi', 'wt-wt'], + "description": "The region to use for the search. Infer this from the language used for the" + "query. Default to `wt-wt` if not specified", } }, - "required": ["query", "type"], + "required": ["query", "type", "region"], }, }] async def execute(self, function_name, **kwargs) -> Dict: with DDGS() as ddgs: + image_type = kwargs.get('type', 'photo') ddgs_images_gen = ddgs.images( kwargs['query'], - region="wt-wt", - safesearch='off', - type_image=kwargs.get('type', 'photo'), + region=kwargs.get('region', 'wt-wt'), + safesearch=self.safesearch, + type_image=image_type, ) - results = list(islice(ddgs_images_gen, 1)) - return {"result": results[0]["image"]} + results = list(islice(ddgs_images_gen, 10)) + if not results or len(results) == 0: + return {"result": "No results found"} + + # Shuffle the results to avoid always returning the same image + random.shuffle(results) + + return { + 'direct_result': { + 'kind': image_type, + 'format': 'url', + 'value': results[0]['image'] + } + } diff --git a/bot/plugins/ddg_web_search.py b/bot/plugins/ddg_web_search.py index 6cc84ae3..fbd3d78b 100644 --- a/bot/plugins/ddg_web_search.py +++ b/bot/plugins/ddg_web_search.py @@ -1,3 +1,4 @@ +import os from itertools import islice from typing import Dict @@ -10,6 +11,8 @@ class DDGWebSearchPlugin(Plugin): """ A plugin to search the web for a given query, using DuckDuckGo """ + def __init__(self): + self.safesearch = os.getenv('DUCKDUCKGO_SAFESEARCH', 'moderate') def get_source_name(self) -> str: return "DuckDuckGo" @@ -24,9 +27,22 @@ def get_spec(self) -> [Dict]: "query": { "type": "string", "description": "the user query" + }, + "region": { + "type": "string", + "enum": ['xa-ar', 'xa-en', 'ar-es', 'au-en', 'at-de', 'be-fr', 'be-nl', 'br-pt', 'bg-bg', + 'ca-en', 'ca-fr', 'ct-ca', 'cl-es', 'cn-zh', 'co-es', 'hr-hr', 'cz-cs', 'dk-da', + 'ee-et', 'fi-fi', 'fr-fr', 'de-de', 'gr-el', 'hk-tzh', 'hu-hu', 'in-en', 'id-id', + 'id-en', 'ie-en', 'il-he', 'it-it', 'jp-jp', 'kr-kr', 'lv-lv', 'lt-lt', 'xl-es', + 'my-ms', 'my-en', 'mx-es', 'nl-nl', 'nz-en', 'no-no', 'pe-es', 'ph-en', 'ph-tl', + 'pl-pl', 'pt-pt', 'ro-ro', 'ru-ru', 'sg-en', 'sk-sk', 'sl-sl', 'za-en', 'es-es', + 'se-sv', 'ch-de', 'ch-fr', 'ch-it', 'tw-tzh', 'th-th', 'tr-tr', 'ua-uk', 'uk-en', + 'us-en', 'ue-es', 've-es', 'vn-vi', 'wt-wt'], + "description": "The region to use for the search. Infer this from the language used for the" + "query. Default to `wt-wt` if not specified", } }, - "required": ["query"], + "required": ["query", "region"], }, }] @@ -34,8 +50,8 @@ async def execute(self, function_name, **kwargs) -> Dict: with DDGS() as ddgs: ddgs_gen = ddgs.text( kwargs['query'], - region='wt-wt', - safesearch='off' + region=kwargs.get('region', 'wt-wt'), + safesearch=self.safesearch ) results = list(islice(ddgs_gen, 3)) diff --git a/bot/plugins/dice.py b/bot/plugins/dice.py new file mode 100644 index 00000000..1f2a0908 --- /dev/null +++ b/bot/plugins/dice.py @@ -0,0 +1,38 @@ +from typing import Dict + +from .plugin import Plugin + + +class DicePlugin(Plugin): + """ + A plugin to send a die in the chat + """ + def get_source_name(self) -> str: + return "Dice" + + def get_spec(self) -> [Dict]: + return [{ + "name": "send_dice", + "description": "Send a dice in the chat, with a random number between 1 and 6", + "parameters": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "enum": ["🎲", "🎯", "🏀", "⚽", "🎳", "🎰"], + "description": "Emoji on which the dice throw animation is based." + "Dice can have values 1-6 for “🎲”, “🎯” and “🎳”, values 1-5 for “🏀” " + "and “⚽”, and values 1-64 for “🎰”. Defaults to “🎲”.", + } + }, + }, + }] + + async def execute(self, function_name, **kwargs) -> Dict: + return { + 'direct_result': { + 'kind': 'dice', + 'format': 'dice', + 'value': kwargs.get('emoji', '🎲') + } + } diff --git a/bot/plugins/worldtimeapi.py b/bot/plugins/worldtimeapi.py index 9c866581..9ec15d7e 100644 --- a/bot/plugins/worldtimeapi.py +++ b/bot/plugins/worldtimeapi.py @@ -9,7 +9,6 @@ class WorldTimeApiPlugin(Plugin): """ A plugin to get the current time from a given timezone, using WorldTimeAPI """ - def __init__(self): default_timezone = os.getenv('WORLDTIME_DEFAULT_TIMEZONE') if not default_timezone: diff --git a/bot/plugins/youtube_audio_extractor.py b/bot/plugins/youtube_audio_extractor.py new file mode 100644 index 00000000..5d61fd5c --- /dev/null +++ b/bot/plugins/youtube_audio_extractor.py @@ -0,0 +1,44 @@ +from typing import Dict + +from pytube import YouTube + +from .plugin import Plugin + + +class YouTubeAudioExtractorPlugin(Plugin): + """ + A plugin to extract audio from a YouTube video + """ + + def get_source_name(self) -> str: + return "YouTube Audio Extractor" + + def get_spec(self) -> [Dict]: + return [{ + "name": "extract_youtube_audio", + "description": "Extract audio from a YouTube video", + "parameters": { + "type": "object", + "properties": { + "youtube_link": {"type": "string", "description": "YouTube video link to extract audio from"} + }, + "required": ["youtube_link"], + }, + }] + + async def execute(self, function_name, **kwargs) -> Dict: + link = kwargs['youtube_link'] + try: + video = YouTube(link) + audio = video.streams.filter(only_audio=True, file_extension='mp4').first() + output = video.title + '.mp4' + audio.download(filename=output) + return { + 'direct_result': { + 'kind': 'file', + 'format': 'path', + 'value': output + } + } + except: + return {'result': 'Failed to extract audio'} diff --git a/bot/telegram_bot.py b/bot/telegram_bot.py index 5385977d..0d4266e7 100644 --- a/bot/telegram_bot.py +++ b/bot/telegram_bot.py @@ -16,7 +16,8 @@ from utils import is_group_chat, get_thread_id, message_text, wrap_with_indicator, split_into_chunks, \ edit_message_with_retry, get_stream_cutoff_values, is_allowed, get_remaining_budget, is_admin, is_within_budget, \ - get_reply_to_message_id, add_chat_request_to_usage_tracker, error_handler + get_reply_to_message_id, add_chat_request_to_usage_tracker, error_handler, is_direct_result, handle_direct_result, \ + cleanup_intermediate_files from openai_helper import OpenAIHelper, localized_text from usage_tracker import UsageTracker @@ -401,6 +402,9 @@ async def prompt(self, update: Update, context: ContextTypes.DEFAULT_TYPE): stream_chunk = 0 async for content, tokens in stream_response: + if is_direct_result(content): + return await handle_direct_result(self.config, update, content) + if len(content.strip()) == 0: continue @@ -472,6 +476,9 @@ async def _reply(): nonlocal total_tokens response, total_tokens = await self.openai.get_chat_response(chat_id=chat_id, query=prompt) + if is_direct_result(response): + return await handle_direct_result(self.config, update, response) + # Split into chunks of 4096 characters (Telegram's message limit) chunks = split_into_chunks(response) @@ -585,12 +592,21 @@ async def handle_callback_inline_query(self, update: Update, context: CallbackCo is_inline=True) return + unavailable_message = localized_text("function_unavailable_in_inline_mode", bot_language) if self.config['stream']: stream_response = self.openai.get_chat_response_stream(chat_id=user_id, query=query) i = 0 prev = '' backoff = 0 async for content, tokens in stream_response: + if is_direct_result(content): + cleanup_intermediate_files(content) + await edit_message_with_retry(context, chat_id=None, + message_id=inline_message_id, + text=f'{query}\n\n_{answer_tr}:_\n{unavailable_message}', + is_inline=True) + return + if len(content.strip()) == 0: continue @@ -648,6 +664,14 @@ async def _send_inline_query_response(): logging.info(f'Generating response for inline query by {name}') response, total_tokens = await self.openai.get_chat_response(chat_id=user_id, query=query) + if is_direct_result(response): + cleanup_intermediate_files(response) + await edit_message_with_retry(context, chat_id=None, + message_id=inline_message_id, + text=f'{query}\n\n_{answer_tr}:_\n{unavailable_message}', + is_inline=True) + return + text_content = f'{query}\n\n_{answer_tr}:_\n{response}' # We only want to send the first 4096 characters. No chunking allowed in inline mode. diff --git a/bot/utils.py b/bot/utils.py index 7dafc12d..8220325d 100644 --- a/bot/utils.py +++ b/bot/utils.py @@ -2,7 +2,9 @@ import asyncio import itertools +import json import logging +import os import telegram from telegram import Message, MessageEntity, Update, ChatMember, constants @@ -173,6 +175,7 @@ async def is_allowed(config, update: Update, context: CallbackContext, is_inline f'(id: {user_id}) are not allowed') return False + def is_admin(config, user_id: int, log_no_admin=False) -> bool: """ Checks if the user is the admin of the bot. @@ -284,6 +287,9 @@ def add_chat_request_to_usage_tracker(usage, config, user_id, used_tokens): :param used_tokens: The number of tokens used """ try: + if int(used_tokens) == 0: + logging.warning('No tokens used. Not adding chat request to usage tracker.') + return # add chat request to users usage tracker usage[user_id].add_chat_tokens(used_tokens, config['token_price']) # add guest chat request to guest usage tracker @@ -305,3 +311,66 @@ def get_reply_to_message_id(config, update: Update): if config['enable_quoting'] or is_group_chat(update): return update.message.message_id return None + + +def is_direct_result(response: any) -> bool: + """ + Checks if the dict contains a direct result that can be sent directly to the user + :param response: The response value + :return: Boolean indicating if the result is a direct result + """ + if type(response) is not dict: + try: + json_response = json.loads(response) + return json_response.get('direct_result', False) + except: + return False + else: + return response.get('direct_result', False) + + +async def handle_direct_result(config, update: Update, response: any): + """ + Handles a direct result from a plugin + """ + if type(response) is not dict: + response = json.loads(response) + + result = response['direct_result'] + kind = result['kind'] + format = result['format'] + value = result['value'] + + common_args = { + 'message_thread_id': get_thread_id(update), + 'reply_to_message_id': get_reply_to_message_id(config, update), + } + + if kind == 'photo': + if format == 'url': + await update.effective_message.reply_photo(**common_args, photo=value) + elif kind == 'gif': + if format == 'url': + await update.effective_message.reply_document(**common_args, document=value) + elif kind == 'file': + if format == 'path': + await update.effective_message.reply_document(**common_args, document=open(value, 'rb')) + cleanup_intermediate_files(response) + elif kind == 'dice': + await update.effective_message.reply_dice(**common_args, emoji=value) + + +def cleanup_intermediate_files(response: any): + """ + Deletes intermediate files created by plugins + """ + if type(response) is not dict: + response = json.loads(response) + + result = response['direct_result'] + kind = result['kind'] + format = result['format'] + value = result['value'] + + if kind == 'file' and format == 'path': + os.remove(value) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7070ed34..bb30eed3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ requests~=2.31.0 tenacity==8.2.2 wolframalpha==5.0.0 duckduckgo_search==3.8.3 -spotipy==2.23.0 \ No newline at end of file +spotipy==2.23.0 +pytube==15.0.0 \ No newline at end of file diff --git a/translations.json b/translations.json index 35ad1acc..ce7c2990 100644 --- a/translations.json +++ b/translations.json @@ -39,7 +39,8 @@ "try_again":"Please try again in a while", "answer_with_chatgpt":"Answer with ChatGPT", "ask_chatgpt":"Ask ChatGPT", - "loading":"Loading..." + "loading":"Loading...", + "function_unavailable_in_inline_mode": "This function is unavailable in inline mode" }, "es": { "help_description":"Muestra el mensaje de ayuda", @@ -81,9 +82,10 @@ "try_again":"Por favor, inténtalo de nuevo más tarde", "answer_with_chatgpt":"Responder con ChatGPT", "ask_chatgpt":"Preguntar a ChatGPT", - "loading":"Cargando..." + "loading":"Cargando...", + "function_unavailable_in_inline_mode": "Esta función no está disponible en el modo inline" }, - "pt-br":{ + "pt-br": { "help_description": "Mostra a mensagem de ajuda", "reset_description": "Redefine a conversa. Opcionalmente, passe instruções de alto nível (por exemplo, /reset Você é um assistente útil)", "image_description": "Gera uma imagem a partir do prompt (por exemplo, /image gato)", @@ -123,7 +125,9 @@ "try_again": "Por favor, tente novamente mais tarde", "answer_with_chatgpt": "Responder com ChatGPT", "ask_chatgpt": "Perguntar ao ChatGPT", - "loading": "Carregando..."}, + "loading": "Carregando...", + "function_unavailable_in_inline_mode": "Esta função não está disponível no modo inline" + }, "de": { "help_description":"Zeige die Hilfenachricht", "reset_description":"Setze die Konversation zurück. Optionale Eingabe einer grundlegenden Anweisung (z.B. /reset Du bist ein hilfreicher Assistent)", @@ -164,7 +168,8 @@ "try_again":"Bitte versuche es später erneut", "answer_with_chatgpt":"Antworte mit ChatGPT", "ask_chatgpt":"Frage ChatGPT", - "loading":"Lade..." + "loading":"Lade...", + "function_unavailable_in_inline_mode": "Diese Funktion ist im Inline-Modus nicht verfügbar" }, "fi": { "help_description":"Näytä ohjeet", @@ -206,7 +211,8 @@ "try_again":"Yritä myöhemmin uudelleen", "answer_with_chatgpt":"Vastaa ChatGPT:n avulla", "ask_chatgpt":"Kysy ChatGPT:ltä", - "loading":"Lataa..." + "loading":"Lataa...", + "function_unavailable_in_inline_mode": "Tämä toiminto ei ole käytettävissä sisäisessä tilassa" }, "ru": { "help_description":"Показать справочное сообщение", @@ -248,7 +254,8 @@ "try_again":"Пожалуйста, повторите попытку позже", "answer_with_chatgpt":"Ответить с помощью ChatGPT", "ask_chatgpt":"Спросить ChatGPT", - "loading":"Загрузка..." + "loading":"Загрузка...", + "function_unavailable_in_inline_mode": "Эта функция недоступна в режиме inline" }, "tr": { "help_description":"Yardım mesajını göster", @@ -290,7 +297,8 @@ "try_again":"Lütfen birazdan tekrar deneyiniz", "answer_with_chatgpt":"ChatGPT ile cevapla", "ask_chatgpt":"ChatGPT'ye sor", - "loading":"Yükleniyor..." + "loading":"Yükleniyor...", + "function_unavailable_in_inline_mode": "Bu işlev inline modda kullanılamaz" }, "it": { "help_description":"Mostra il messaggio di aiuto", @@ -332,7 +340,8 @@ "try_again":"Riprova più tardi", "answer_with_chatgpt":"Rispondi con ChatGPT", "ask_chatgpt":"Chiedi a ChatGPT", - "loading":"Carico..." + "loading":"Carico...", + "function_unavailable_in_inline_mode": "Questa funzione non è disponibile in modalità inline" }, "id": { "help_description": "Menampilkan pesan bantuan", @@ -374,7 +383,8 @@ "try_again": "Silakan coba lagi nanti", "answer_with_chatgpt": "Jawaban dengan ChatGPT", "ask_chatgpt": "Tanya ChatGPT", - "loading": "Sedang memuat..." + "loading": "Sedang memuat...", + "function_unavailable_in_inline_mode": "Fungsi ini tidak tersedia dalam mode inline" }, "nl": { "help_description":"Toon uitleg", @@ -416,7 +426,8 @@ "try_again":"Probeer het a.u.b. later opnieuw", "answer_with_chatgpt":"Antwoord met ChatGPT", "ask_chatgpt":"Vraag ChatGPT", - "loading":"Laden..." + "loading":"Laden...", + "function_unavailable_in_inline_mode": "Deze functie is niet beschikbaar in de inline modus" }, "zh-cn": { "help_description":"显示帮助信息", @@ -458,7 +469,8 @@ "try_again":"请稍后再试", "answer_with_chatgpt":"使用ChatGPT回答", "ask_chatgpt":"询问ChatGPT", - "loading":"载入中..." + "loading":"载入中...", + "function_unavailable_in_inline_mode": "此功能在内联模式下不可用" }, "zh-tw": { "help_description":"顯示幫助訊息", @@ -500,7 +512,8 @@ "try_again":"請稍後重試", "answer_with_chatgpt":"使用 ChatGPT 回答", "ask_chatgpt":"詢問 ChatGPT", - "loading":"載入中…" + "loading":"載入中…", + "function_unavailable_in_inline_mode": "此功能在內嵌模式下不可用" }, "vi": { "help_description":"Hiển thị trợ giúp", @@ -542,7 +555,8 @@ "try_again":"Vui lòng thử lại sau một lúc", "answer_with_chatgpt":"Trả lời với ChatGPT", "ask_chatgpt":"Hỏi ChatGPT", - "loading":"Đang tải..." + "loading":"Đang tải...", + "function_unavailable_in_inline_mode": "Chức năng này không khả dụng trong chế độ nội tuyến" }, "fa": { "help_description":"نمایش پیغام راهنما", @@ -584,7 +598,8 @@ "try_again":"لطفا بعد از مدتی دوباره امتحان کنید", "answer_with_chatgpt":"با ChatGPT پاسخ دهید", "ask_chatgpt":"از ChatGPT بپرسید", - "loading":"در حال بارگذاری..." + "loading":"در حال بارگذاری...", + "function_unavailable_in_inline_mode": "این عملکرد در حالت آنلاین در دسترس نیست" }, "uk": { "help_description":"Показати повідомлення допомоги", @@ -626,6 +641,7 @@ "try_again":"Будь ласка, спробуйте знову через деякий час", "answer_with_chatgpt":"Відповідь за допомогою ChatGPT", "ask_chatgpt":"Запитати ChatGPT", - "loading":"Завантаження..." + "loading":"Завантаження...", + "function_unavailable_in_inline_mode": "Ця функція недоступна в режимі Inline" } } From 2a0e2105018f2ead9822ce88c36a08cc60c319a5 Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 3 Jul 2023 22:28:06 +0200 Subject: [PATCH 38/52] minor improvements --- README.md | 4 ++-- bot/plugin_manager.py | 2 +- bot/plugins/deepl.py | 13 +++++-------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7e5d1900..012ff698 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | | `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use, i.e. `Europe/Rome` (required only for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | - | | `DUCKDUCKGO_SAFESEARCH` | DuckDuckGo safe search (`on`, `off` or `moderate`) (optional, applies to `ddg_web_search` and `ddg_image_search`) | `moderate` | -| `DEEPL_API_KEY` | DeepL API key (required for the `deepl` plugin, you can get one [here](https://www.deepl.com/pro-api?cta=header-pro-api)) | `-` | +| `DEEPL_API_KEY` | DeepL API key (required for the `deepl` plugin, you can get one [here](https://www.deepl.com/pro-api?cta=header-pro-api)) | - | #### Available plugins | Name | Description | Required environment variable(s) | @@ -126,7 +126,7 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `WORLDTIME_DEFAULT_TIMEZONE` | | `dice` | Send a dice in the chat! | - | | `youtube_audio_extractor` | Extract audio from YouTube videos | - | -| `deepl` | Translate text to any language (powered by [DeepL](https://deepl.com)) - by [@LedyBacer](https://github.com/LedyBacer) | `DEEPL_API_KEY` | +| `deepl_translate` | Translate text to any language (powered by [DeepL](https://deepl.com)) - by [@LedyBacer](https://github.com/LedyBacer) | `DEEPL_API_KEY` | Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 8b7d4a48..2d0ad3f8 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -31,7 +31,7 @@ def __init__(self, config): 'worldtimeapi': WorldTimeApiPlugin, 'youtube_audio_extractor': YouTubeAudioExtractorPlugin, 'dice': DicePlugin, - 'deepl': DeeplTranslatePlugin + 'deepl_translate': DeeplTranslatePlugin } self.plugins = [plugin_mapping[plugin]() for plugin in enabled_plugins if plugin in plugin_mapping] diff --git a/bot/plugins/deepl.py b/bot/plugins/deepl.py index b58d271b..a43a6f8c 100644 --- a/bot/plugins/deepl.py +++ b/bot/plugins/deepl.py @@ -10,12 +10,11 @@ class DeeplTranslatePlugin(Plugin): """ A plugin to translate a given text from a language to another, using DeepL """ - def __init__(self): - deepl_api_key = os.getenv('DEEPL_API_KEY') - if not deepl_api_key: - raise ValueError('DEEPL_API_KEY environment variable must be set to use DeepL Plugin') - self.api_key = deepl_api_key + deepl_api_key = os.getenv('DEEPL_API_KEY') + if not deepl_api_key: + raise ValueError('DEEPL_API_KEY environment variable must be set to use DeepL Plugin') + self.api_key = deepl_api_key def get_source_name(self) -> str: return "DeepL Translate" @@ -42,13 +41,11 @@ async def execute(self, function_name, **kwargs) -> Dict: headers = { "Authorization": f"DeepL-Auth-Key {self.api_key}", - "User-Agent": "chatgpt-telegram-bot/0.2.7", + "User-Agent": "chatgpt-telegram-bot", "Content-Type": "application/x-www-form-urlencoded" } - data = { "text": kwargs['text'], "target_lang": kwargs['to_language'] } - return requests.post(url, headers=headers, data=data).json()["translations"][0]["text"] From 7994a3003cfbe772ddc439a76b3cc9d138dd34dc Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 3 Jul 2023 22:33:15 +0200 Subject: [PATCH 39/52] split plugin environment variables into its own table --- README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 012ff698..060479ee 100644 --- a/README.md +++ b/README.md @@ -105,13 +105,6 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response before displaying a user-facing message | `10` | | `PLUGINS` | List of plugins to enable (see below for a full list), e.g: `PLUGINS=wolfram,weather` | - | | `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | -| `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required only for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation)) | - | -| `SPOTIFY_CLIENT_ID` | Spotify app Client ID (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | -| `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | -| `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | -| `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use, i.e. `Europe/Rome` (required only for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | - | -| `DUCKDUCKGO_SAFESEARCH` | DuckDuckGo safe search (`on`, `off` or `moderate`) (optional, applies to `ddg_web_search` and `ddg_image_search`) | `moderate` | -| `DEEPL_API_KEY` | DeepL API key (required for the `deepl` plugin, you can get one [here](https://www.deepl.com/pro-api?cta=header-pro-api)) | - | #### Available plugins | Name | Description | Required environment variable(s) | @@ -128,6 +121,18 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `youtube_audio_extractor` | Extract audio from YouTube videos | - | | `deepl_translate` | Translate text to any language (powered by [DeepL](https://deepl.com)) - by [@LedyBacer](https://github.com/LedyBacer) | `DEEPL_API_KEY` | +#### Environment variables +| Variable | Description | Default value | +|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| +| `WOLFRAM_APP_ID` | Wolfram Alpha APP ID (required only for the `wolfram` plugin, you can get one [here](https://products.wolframalpha.com/simple-api/documentation)) | - | +| `SPOTIFY_CLIENT_ID` | Spotify app Client ID (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | +| `SPOTIFY_CLIENT_SECRET` | Spotify app Client Secret (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | +| `SPOTIFY_REDIRECT_URI` | Spotify app Redirect URI (required only for the `spotify` plugin, you can find it on the [dashboard](https://developer.spotify.com/dashboard/)) | - | +| `WORLDTIME_DEFAULT_TIMEZONE` | Default timezone to use, i.e. `Europe/Rome` (required only for the `worldtimeapi` plugin, you can get TZ Identifiers from [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)) | - | +| `DUCKDUCKGO_SAFESEARCH` | DuckDuckGo safe search (`on`, `off` or `moderate`) (optional, applies to `ddg_web_search` and `ddg_image_search`) | `moderate` | +| `DEEPL_API_KEY` | DeepL API key (required for the `deepl` plugin, you can get one [here](https://www.deepl.com/pro-api?cta=header-pro-api)) | - | + + Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. ### Installing From 73a192c4a8322d6393536b3a3903877ca2848e06 Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 3 Jul 2023 22:35:18 +0200 Subject: [PATCH 40/52] update README.md --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 060479ee..0ddd65c6 100644 --- a/README.md +++ b/README.md @@ -98,13 +98,15 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `IGNORE_GROUP_TRANSCRIPTIONS` | If set to true, the bot will not process transcriptions in group chats | `true` | | `BOT_LANGUAGE` | Language of general bot messages. Currently available: `en`, `de`, `ru`, `tr`, `it`, `fi`, `es`, `id`, `nl`, `zh-cn`, `zh-tw`, `vi`, `fa`, `pt-br`, `uk`. [Contribute with additional translations](https://github.com/n3d1117/chatgpt-telegram-bot/discussions/219) | `en` | +Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. + #### Functions -| Parameter | Description | Default value | -|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| -| `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | -| `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response before displaying a user-facing message | `10` | -| `PLUGINS` | List of plugins to enable (see below for a full list), e.g: `PLUGINS=wolfram,weather` | - | -| `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | +| Parameter | Description | Default value | +|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| +| `ENABLE_FUNCTIONS` | Whether to use functions (aka plugins). You can read more about functions [here](https://openai.com/blog/function-calling-and-other-api-updates) | `true` (if available for the model) | +| `FUNCTIONS_MAX_CONSECUTIVE_CALLS` | Maximum number of back-to-back function calls to be made by the model in a single response, before displaying a user-facing message | `10` | +| `PLUGINS` | List of plugins to enable (see below for a full list), e.g: `PLUGINS=wolfram,weather` | - | +| `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | #### Available plugins | Name | Description | Required environment variable(s) | @@ -132,9 +134,6 @@ Check out the [Budget Manual](https://github.com/n3d1117/chatgpt-telegram-bot/di | `DUCKDUCKGO_SAFESEARCH` | DuckDuckGo safe search (`on`, `off` or `moderate`) (optional, applies to `ddg_web_search` and `ddg_image_search`) | `moderate` | | `DEEPL_API_KEY` | DeepL API key (required for the `deepl` plugin, you can get one [here](https://www.deepl.com/pro-api?cta=header-pro-api)) | - | - -Check out the [official API reference](https://platform.openai.com/docs/api-reference/chat) for more details. - ### Installing Clone the repository and navigate to the project directory: From ae3cf1ec83f41bd429a100421f01ac1da630f8db Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 3 Jul 2023 22:37:23 +0200 Subject: [PATCH 41/52] fix imports --- bot/plugin_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 2d0ad3f8..2a4d3346 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -1,7 +1,7 @@ import json -from bot.plugins.dice import DicePlugin -from bot.plugins.youtube_audio_extractor import YouTubeAudioExtractorPlugin +from plugins.dice import DicePlugin +from plugins.youtube_audio_extractor import YouTubeAudioExtractorPlugin from plugins.ddg_image_search import DDGImageSearchPlugin from plugins.ddg_translate import DDGTranslatePlugin from plugins.spotify import SpotifyPlugin From 06aecfc77bf0db40509a628246b07fcd436614d5 Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 3 Jul 2023 22:38:14 +0200 Subject: [PATCH 42/52] fix another import --- bot/openai_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/openai_helper.py b/bot/openai_helper.py index 11f8e4e3..eff21ac6 100644 --- a/bot/openai_helper.py +++ b/bot/openai_helper.py @@ -14,7 +14,7 @@ from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type -from bot.utils import is_direct_result +from utils import is_direct_result from plugin_manager import PluginManager # Models can be found here: https://platform.openai.com/docs/models/overview From b86ede44bf2213be3c4e8c236d16b890b4f9d8e3 Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 3 Jul 2023 22:44:12 +0200 Subject: [PATCH 43/52] log failure from yt audio extractor --- bot/plugins/youtube_audio_extractor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/plugins/youtube_audio_extractor.py b/bot/plugins/youtube_audio_extractor.py index 5d61fd5c..da552ceb 100644 --- a/bot/plugins/youtube_audio_extractor.py +++ b/bot/plugins/youtube_audio_extractor.py @@ -1,3 +1,4 @@ +import logging from typing import Dict from pytube import YouTube @@ -40,5 +41,6 @@ async def execute(self, function_name, **kwargs) -> Dict: 'value': output } } - except: + except Exception as e: + logging.warning(f'Failed to extract audio from YouTube video: {str(e)}') return {'result': 'Failed to extract audio'} From 41b92ff9104979c59b7bd48d5645de494e534c2c Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 3 Jul 2023 23:31:41 +0200 Subject: [PATCH 44/52] support direct sending of remote files --- bot/utils.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bot/utils.py b/bot/utils.py index 8220325d..6ce2e98e 100644 --- a/bot/utils.py +++ b/bot/utils.py @@ -349,16 +349,19 @@ async def handle_direct_result(config, update: Update, response: any): if kind == 'photo': if format == 'url': await update.effective_message.reply_photo(**common_args, photo=value) - elif kind == 'gif': + elif format == 'path': + await update.effective_message.reply_photo(**common_args, photo=open(value, 'rb')) + elif kind == 'gif' or kind == 'file': if format == 'url': await update.effective_message.reply_document(**common_args, document=value) - elif kind == 'file': if format == 'path': await update.effective_message.reply_document(**common_args, document=open(value, 'rb')) - cleanup_intermediate_files(response) elif kind == 'dice': await update.effective_message.reply_dice(**common_args, emoji=value) + if format == 'path': + cleanup_intermediate_files(response) + def cleanup_intermediate_files(response: any): """ @@ -368,9 +371,9 @@ def cleanup_intermediate_files(response: any): response = json.loads(response) result = response['direct_result'] - kind = result['kind'] format = result['format'] value = result['value'] - if kind == 'file' and format == 'path': - os.remove(value) \ No newline at end of file + if format == 'path': + if os.path.exists(value): + os.remove(value) \ No newline at end of file From fda6cb4068fcc90f8d4c0e59aeaceb8cf1fafae2 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 6 Jul 2023 12:04:57 +0200 Subject: [PATCH 45/52] added gtts, make remove plugin dependencies from requirements, updated readme --- README.md | 29 +++++++++-------- bot/plugin_manager.py | 4 ++- bot/plugins/gtts_text_to_speech.py | 44 ++++++++++++++++++++++++++ bot/plugins/youtube_audio_extractor.py | 3 +- bot/usage_tracker.py | 2 +- requirements.txt | 6 +--- 6 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 bot/plugins/gtts_text_to_speech.py diff --git a/README.md b/README.md index 0ddd65c6..d13a52d6 100644 --- a/README.md +++ b/README.md @@ -109,19 +109,22 @@ Check out the [official API reference](https://platform.openai.com/docs/api-refe | `SHOW_PLUGINS_USED` | Whether to show which plugins were used for a response | `false` | #### Available plugins -| Name | Description | Required environment variable(s) | -|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| -| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | - | -| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | -| `ddg_web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `ddg_translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `ddg_image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | - | -| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | - | -| `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | -| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `WORLDTIME_DEFAULT_TIMEZONE` | -| `dice` | Send a dice in the chat! | - | -| `youtube_audio_extractor` | Extract audio from YouTube videos | - | -| `deepl_translate` | Translate text to any language (powered by [DeepL](https://deepl.com)) - by [@LedyBacer](https://github.com/LedyBacer) | `DEEPL_API_KEY` | +| Name | Description | Required environment variable(s) | Dependency | +|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------| +| `weather` | Daily weather and 7-day forecast for any location (powered by [Open-Meteo](https://open-meteo.com)) | - | | +| `wolfram` | WolframAlpha queries (powered by [WolframAlpha](https://www.wolframalpha.com)) | `WOLFRAM_APP_ID` | `pip install wolframalpha~=5.0.0` | +| `ddg_web_search` | Web search (powered by [DuckDuckGo](https://duckduckgo.com)) | - | `pip install duckduckgo_search~=3.8.3` | +| `ddg_translate` | Translate text to any language (powered by [DuckDuckGo](https://duckduckgo.com)) | - | `pip install duckduckgo_search~=3.8.3` | +| `ddg_image_search` | Search image or GIF (powered by [DuckDuckGo](https://duckduckgo.com)) | - | `pip install duckduckgo_search~=3.8.3` | +| `crypto` | Live cryptocurrencies rate (powered by [CoinCap](https://coincap.io)) - by [@stumpyfr](https://github.com/stumpyfr) | - | | +| `spotify` | Spotify top tracks/artists, currently playing song and content search (powered by [Spotify](https://spotify.com)). Requires one-time authorization. | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REDIRECT_URI` | `pip install spotipy~=2.23.0` | +| `worldtimeapi` | Get latest world time (powered by [WorldTimeAPI](https://worldtimeapi.org/)) | `WORLDTIME_DEFAULT_TIMEZONE` | | +| `dice` | Send a dice in the chat! | - | | +| `youtube_audio_extractor` | Extract audio from YouTube videos | - | `pip install pytube~=15.0.0` | +| `deepl_translate` | Translate text to any language (powered by [DeepL](https://deepl.com)) - by [@LedyBacer](https://github.com/LedyBacer) | `DEEPL_API_KEY` | | +| `gtts_text_to_speech` | Text to speech (powered by Google Translate APIs) | - | `pip install gtts~=2.3.2` | + +**Note**: some plugins have additional dependencies that are not listed in the `requirements.txt` file. If you plan on using these plugins, you can install them manually using the command above (see the `Dependency` column). #### Environment variables | Variable | Description | Default value | diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 2a4d3346..6b5830be 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -1,5 +1,6 @@ import json +from bot.plugins.gtts_text_to_speech import GTTSTextToSpeech from plugins.dice import DicePlugin from plugins.youtube_audio_extractor import YouTubeAudioExtractorPlugin from plugins.ddg_image_search import DDGImageSearchPlugin @@ -31,7 +32,8 @@ def __init__(self, config): 'worldtimeapi': WorldTimeApiPlugin, 'youtube_audio_extractor': YouTubeAudioExtractorPlugin, 'dice': DicePlugin, - 'deepl_translate': DeeplTranslatePlugin + 'deepl_translate': DeeplTranslatePlugin, + 'gtts_text_to_speech': GTTSTextToSpeech, } self.plugins = [plugin_mapping[plugin]() for plugin in enabled_plugins if plugin in plugin_mapping] diff --git a/bot/plugins/gtts_text_to_speech.py b/bot/plugins/gtts_text_to_speech.py new file mode 100644 index 00000000..50f01207 --- /dev/null +++ b/bot/plugins/gtts_text_to_speech.py @@ -0,0 +1,44 @@ +import datetime +from typing import Dict + +from gtts import gTTS + +from .plugin import Plugin + + +class GTTSTextToSpeech(Plugin): + """ + A plugin to convert text to speech using Google Translate's Text to Speech API + """ + + def get_source_name(self) -> str: + return "gTTS" + + def get_spec(self) -> [Dict]: + return [{ + "name": "google_translate_text_to_speech", + "description": "Translate text to speech using Google Translate's Text to Speech API", + "parameters": { + "type": "object", + "properties": { + "text": {"type": "string", "description": "The text to translate to speech"}, + "lang": { + "type": "string", "description": "The language of the text to translate to speech." + "Infer this from the language of the text.", + }, + }, + "required": ["text", "lang"], + }, + }] + + async def execute(self, function_name, **kwargs) -> Dict: + tts = gTTS(kwargs['text'], lang=kwargs.get('lang', 'en')) + output = f'gtts_{datetime.datetime.now().timestamp()}.mp3' + tts.save(output) + return { + 'direct_result': { + 'kind': 'file', + 'format': 'path', + 'value': output + } + } diff --git a/bot/plugins/youtube_audio_extractor.py b/bot/plugins/youtube_audio_extractor.py index da552ceb..5fc4d801 100644 --- a/bot/plugins/youtube_audio_extractor.py +++ b/bot/plugins/youtube_audio_extractor.py @@ -1,4 +1,5 @@ import logging +import re from typing import Dict from pytube import YouTube @@ -32,7 +33,7 @@ async def execute(self, function_name, **kwargs) -> Dict: try: video = YouTube(link) audio = video.streams.filter(only_audio=True, file_extension='mp4').first() - output = video.title + '.mp4' + output = re.sub(r'[^\w\-_\. ]', '_', video.title) + '.mp3' audio.download(filename=output) return { 'direct_result': { diff --git a/bot/usage_tracker.py b/bot/usage_tracker.py index c733f9eb..c7bb7b33 100644 --- a/bot/usage_tracker.py +++ b/bot/usage_tracker.py @@ -74,7 +74,7 @@ def add_chat_tokens(self, tokens, tokens_price=0.002): :param tokens_price: price per 1000 tokens, defaults to 0.002 """ today = date.today() - token_cost = round(tokens * tokens_price / 1000, 6) + token_cost = round(float(tokens) * tokens_price / 1000, 6) self.add_current_costs(token_cost) # update usage_history diff --git a/requirements.txt b/requirements.txt index bb30eed3..1c523d39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,8 +4,4 @@ tiktoken==0.4.0 openai==0.27.8 python-telegram-bot==20.3 requests~=2.31.0 -tenacity==8.2.2 -wolframalpha==5.0.0 -duckduckgo_search==3.8.3 -spotipy==2.23.0 -pytube==15.0.0 \ No newline at end of file +tenacity==8.2.2 \ No newline at end of file From 2e9f742c17a969e18d1026df6a5a7f2dc70173b9 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 6 Jul 2023 12:10:31 +0200 Subject: [PATCH 46/52] fix import --- bot/plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 6b5830be..7faea106 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -1,6 +1,6 @@ import json -from bot.plugins.gtts_text_to_speech import GTTSTextToSpeech +from plugins.gtts_text_to_speech import GTTSTextToSpeech from plugins.dice import DicePlugin from plugins.youtube_audio_extractor import YouTubeAudioExtractorPlugin from plugins.ddg_image_search import DDGImageSearchPlugin From 0b8317fca3a7b861af30f8c74a21b1ca9b3ee81a Mon Sep 17 00:00:00 2001 From: Juhani Naskali Date: Sat, 15 Jul 2023 19:52:00 +0300 Subject: [PATCH 47/52] added whois plugin --- bot/plugin_manager.py | 2 ++ bot/plugins/whois.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 bot/plugins/whois.py diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 7faea106..9e60b0a0 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -12,6 +12,7 @@ from plugins.wolfram_alpha import WolframAlphaPlugin from plugins.deepl import DeeplTranslatePlugin from plugins.worldtimeapi import WorldTimeApiPlugin +from plugins.whois import WhoisPlugin class PluginManager: @@ -34,6 +35,7 @@ def __init__(self, config): 'dice': DicePlugin, 'deepl_translate': DeeplTranslatePlugin, 'gtts_text_to_speech': GTTSTextToSpeech, + 'whois': WhoisPlugin, } self.plugins = [plugin_mapping[plugin]() for plugin in enabled_plugins if plugin in plugin_mapping] diff --git a/bot/plugins/whois.py b/bot/plugins/whois.py new file mode 100644 index 00000000..34c00ef4 --- /dev/null +++ b/bot/plugins/whois.py @@ -0,0 +1,32 @@ +from typing import Dict +from .plugin import Plugin +import whois + +class WhoisPlugin(Plugin): + """ + A plugin to query whois database + """ + def get_source_name(self) -> str: + return "Whois" + + def get_spec(self) -> [Dict]: + return [{ + "name": "get_whois", + "description": "Get whois registration and expiry information for a domain", + "parameters": { + "type": "object", + "properties": { + "domain": {"type": "string", "description": "Domain name"} + }, + "required": ["domain"], + }, + }] + + async def execute(self, function_name, **kwargs) -> Dict: + try: + whois_result = whois.whois(kwargs['domain']) + return whois_result + except whois.parser.PywhoisError as e: + return {'result': 'No such domain found'} + except Exception as e: + return {'error': 'An unexpected error occurred: ' + str(e)} From eeaaad12584c33045ce016a68a7b5dbcd4d6c180 Mon Sep 17 00:00:00 2001 From: Juhani Naskali Date: Sat, 15 Jul 2023 19:53:09 +0300 Subject: [PATCH 48/52] Fix possible TypeError domain plugin query json sometimes causes 'TypeError: Object of type datetime is not JSON serializable' --- bot/plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index 9e60b0a0..a27652bd 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -52,7 +52,7 @@ async def call_function(self, function_name, arguments): plugin = self.__get_plugin_by_function_name(function_name) if not plugin: return json.dumps({'error': f'Function {function_name} not found'}) - return json.dumps(await plugin.execute(function_name, **json.loads(arguments))) + return json.dumps(await plugin.execute(function_name, **json.loads(arguments)), default=str) def get_plugin_source_name(self, function_name) -> str: """ From d1dd1c9e2514fb06fc6ea7c00ee7255febac2ce3 Mon Sep 17 00:00:00 2001 From: Juhani Naskali Date: Sat, 15 Jul 2023 19:54:07 +0300 Subject: [PATCH 49/52] update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d13a52d6..e294d924 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ Check out the [official API reference](https://platform.openai.com/docs/api-refe | `youtube_audio_extractor` | Extract audio from YouTube videos | - | `pip install pytube~=15.0.0` | | `deepl_translate` | Translate text to any language (powered by [DeepL](https://deepl.com)) - by [@LedyBacer](https://github.com/LedyBacer) | `DEEPL_API_KEY` | | | `gtts_text_to_speech` | Text to speech (powered by Google Translate APIs) | - | `pip install gtts~=2.3.2` | +| `whois` | Query the whois domain database | - | `pip install whois~=0.9.27` | **Note**: some plugins have additional dependencies that are not listed in the `requirements.txt` file. If you plan on using these plugins, you can install them manually using the command above (see the `Dependency` column). From d64859dabce041f0b8cc99b02e6f6e1aaa25ba7c Mon Sep 17 00:00:00 2001 From: Juhani Naskali Date: Mon, 17 Jul 2023 08:33:33 +0300 Subject: [PATCH 50/52] updated whois calls to use query --- bot/plugins/whois.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/plugins/whois.py b/bot/plugins/whois.py index 34c00ef4..417f122d 100644 --- a/bot/plugins/whois.py +++ b/bot/plugins/whois.py @@ -24,9 +24,9 @@ def get_spec(self) -> [Dict]: async def execute(self, function_name, **kwargs) -> Dict: try: - whois_result = whois.whois(kwargs['domain']) - return whois_result - except whois.parser.PywhoisError as e: - return {'result': 'No such domain found'} + whois_result = whois.query(kwargs['domain']) + if whois_result is None: + return {'result': 'No such domain found'} + return whois_result.__dict__ except Exception as e: return {'error': 'An unexpected error occurred: ' + str(e)} From 3b735f3987d035b6be1fed6f4fefc38db8cdb204 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 28 Jul 2023 21:48:31 +0200 Subject: [PATCH 51/52] minor updates --- README.md | 2 +- bot/plugin_manager.py | 2 +- bot/plugins/{whois.py => whois_.py} | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) rename bot/plugins/{whois.py => whois_.py} (99%) diff --git a/README.md b/README.md index e294d924..8246368d 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Check out the [official API reference](https://platform.openai.com/docs/api-refe | `youtube_audio_extractor` | Extract audio from YouTube videos | - | `pip install pytube~=15.0.0` | | `deepl_translate` | Translate text to any language (powered by [DeepL](https://deepl.com)) - by [@LedyBacer](https://github.com/LedyBacer) | `DEEPL_API_KEY` | | | `gtts_text_to_speech` | Text to speech (powered by Google Translate APIs) | - | `pip install gtts~=2.3.2` | -| `whois` | Query the whois domain database | - | `pip install whois~=0.9.27` | +| `whois` | Query the whois domain database - by [@jnaskali](https://github.com/jnaskali) | - | `pip install whois~=0.9.27` | **Note**: some plugins have additional dependencies that are not listed in the `requirements.txt` file. If you plan on using these plugins, you can install them manually using the command above (see the `Dependency` column). diff --git a/bot/plugin_manager.py b/bot/plugin_manager.py index a27652bd..635f056c 100644 --- a/bot/plugin_manager.py +++ b/bot/plugin_manager.py @@ -12,7 +12,7 @@ from plugins.wolfram_alpha import WolframAlphaPlugin from plugins.deepl import DeeplTranslatePlugin from plugins.worldtimeapi import WorldTimeApiPlugin -from plugins.whois import WhoisPlugin +from plugins.whois_ import WhoisPlugin class PluginManager: diff --git a/bot/plugins/whois.py b/bot/plugins/whois_.py similarity index 99% rename from bot/plugins/whois.py rename to bot/plugins/whois_.py index 417f122d..7814558a 100644 --- a/bot/plugins/whois.py +++ b/bot/plugins/whois_.py @@ -1,7 +1,9 @@ from typing import Dict from .plugin import Plugin + import whois + class WhoisPlugin(Plugin): """ A plugin to query whois database From 3e4c228edbb8aae74a7d8db4f4c1adb893a67e90 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 4 Aug 2023 18:39:04 +0200 Subject: [PATCH 52/52] update README.md --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ba4e1f9d..383f71f9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -> **Note** -> Support for functions (aka plugins) is being added! Check out [this discussion](https://github.com/n3d1117/chatgpt-telegram-bot/discussions/345) for more info and contribute! - # ChatGPT Telegram Bot ![python-version](https://img.shields.io/badge/python-3.9-blue.svg) [![openai-version](https://img.shields.io/badge/openai-0.27.8-orange.svg)](https://openai.com/) @@ -10,8 +7,13 @@ A [Telegram bot](https://core.telegram.org/bots/api) that integrates with OpenAI's _official_ [ChatGPT](https://openai.com/blog/chatgpt/), [DALL·E](https://openai.com/product/dall-e-2) and [Whisper](https://openai.com/research/whisper) APIs to provide answers. Ready to use with minimal configuration required. ## Screenshots + +### Demo ![demo](https://user-images.githubusercontent.com/11541888/225114786-0d639854-b3e1-4214-b49a-e51ce8c40387.png) +### Plugins +![plugins](https://github.com/n3d1117/chatgpt-telegram-bot/assets/11541888/83d5e0cd-e09a-463d-a292-722f919e929f) + ## Features - [x] Support markdown in answers - [x] Reset conversation with the `/reset` command @@ -33,7 +35,7 @@ A [Telegram bot](https://core.telegram.org/bots/api) that integrates with OpenAI - To use this feature, enable inline queries for your bot in BotFather via the `/setinline` [command](https://core.telegram.org/bots/inline) - [x] (NEW!) Support *new models* [announced on June 13, 2023](https://openai.com/blog/function-calling-and-other-api-updates) - [x] (NEW!) Support *functions* (plugins) to extend the bot's functionality with 3rd party services - - See [here](#available-plugins) for a list of available plugins + - Weather, Spotify, Web search, text-to-speech and more. See [here](#available-plugins) for a list of available plugins ## Additional features - help needed! If you'd like to help, check out the [issues](https://github.com/n3d1117/chatgpt-telegram-bot/issues) section and contribute!