initial functions support and added weather func

This commit is contained in:
ned
2023-06-14 19:46:02 +02:00
parent 72b5527e55
commit 97f58b30ff
6 changed files with 209 additions and 49 deletions

View File

@@ -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.

23
bot/functions.py Normal file
View File

@@ -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")

View File

@@ -3,7 +3,7 @@ import os
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.')

View File

@@ -14,6 +14,8 @@ from calendar import monthrange
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 @@ class OpenAIHelper:
: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 @@ class OpenAIHelper:
: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 @@ class OpenAIHelper:
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 @@ class OpenAIHelper:
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 @@ class OpenAIHelper:
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.

47
plugins/weather.py Normal file
View File

@@ -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'&current_weather=true')
return json.dumps(request.json())

View File

@@ -5,3 +5,4 @@ openai==0.27.8
python-telegram-bot==20.3
requests~=2.31.0
tenacity==8.2.2
geopy~=2.3.0