Bonjour à tous! Récemment, j'ai appliqué une solution intéressante pendant ma pratique que je voulais essayer depuis longtemps, et maintenant je suis prêt à expliquer comment vous pouvez créer quelque chose de similaire pour n'importe quelle autre tâche. Nous parlerons de la création d'une version personnalisée de ChatGPT qui répond aux questions, en tenant compte d'une large base de connaissances qui n'est pas limitée en longueur par la taille de l'invite (ce qui signifie que vous ne pourriez pas simplement ajouter toutes les informations avant chaque question à ChatGPT).
Pour ce faire, nous utiliserons les intégrations contextuelles d'OpenAI (pour une recherche de qualité de questions pertinentes dans la base de connaissances) et l'API ChatGPT elle-même (pour formater les réponses en langage humain naturel).
De plus, il est supposé que l'assistant peut répondre non seulement aux questions Q&R explicitement énoncées , mais également aux questions auxquelles une personne familière avec les Q&R pourrait répondre. Si vous souhaitez apprendre à créer des bots simples qui répondent à l'aide d'une large base de connaissances, bienvenue dans les détails.
Je voudrais souligner qu'il existe des projets de bibliothèque qui tentent de résoudre cette tâche sous la forme d'un framework, par exemple, LangChain , et j'ai également essayé de l'utiliser. Cependant, comme tout cadre qui en est à un stade précoce de développement, dans certains cas, il a tendance à limiter plutôt qu'à simplifier les choses. En particulier, dès le début de la résolution de cette tâche, j'ai compris ce que je voulais faire avec les données et j'ai su le faire moi-même (y compris la recherche contextuelle, la définition du contexte correct dans les invites, la combinaison de sources d'informations).
Mais je ne pouvais pas configurer le framework pour faire exactement cela avec un niveau de qualité acceptable, et le débogage du framework semblait exagéré pour cette tâche. En fin de compte, j'ai créé mon propre code passe-partout et j'ai été satisfait de cette approche.
Permettez-moi de décrire brièvement la tâche sur laquelle je travaillais, et vous pouvez utiliser le même code dans vos propres tâches, en remplaçant les sources de données et les invites par celles qui vous conviennent . Vous aurez toujours un contrôle total sur la logique du bot.
Lors de l'écriture de code, j'utilise souvent ChatGPT (et je n'en ai pas honte🙂). Cependant, en raison du manque de données pour l'année 2022+, il y a parfois des problèmes avec des technologies relativement nouvelles.
En particulier, lors du développement de sous-graphes pour le protocole The Graph (le moyen le plus populaire de créer un ETL pour récupérer des données indexées à partir de chaînes de blocs compatibles EVM, vous pouvez en savoir plus à ce sujet dans mes articles précédents [ 1 ] et [ 2 ]), les bibliothèques elles-mêmes ont subi plusieurs changements de compatibilité avec rupture. Les "anciennes" réponses de ChatGPT ne sont plus utiles, et je dois chercher les bonnes réponses soit dans la documentation rare, soit, au pire, dans le Discord des développeurs, ce qui n'est pas très pratique (ce n'est pas comme StackOverflow).
La deuxième partie du problème est que chaque fois que vous devez fournir correctement le contexte de la conversation, car ChatGPT s'écarte souvent du sujet des sous-graphes, passant à GraphQL, SQL ou à des mathématiques supérieures ("Le graphique", "sous-graphes", etc. ne sont pas des termes uniques et ont de nombreuses interprétations et sujets différents).
Par conséquent, après une courte période de lutte avec ChatGPT pour corriger les erreurs dans le code de sous-graphe, j'ai décidé de créer mon propre bot SubgraphGPT , qui sera toujours dans le bon contexte et tentera de répondre, en tenant compte de la base de connaissances et des messages des développeurs discord.
PS. Je travaille en tant que chef de produit principal chez chainstack.com , un fournisseur d'infrastructure Web3, et je suis responsable du développement du service d'hébergement de sous-graphes . Je dois donc beaucoup travailler avec des sous-graphes, pour aider les utilisateurs à comprendre cette technologie relativement nouvelle.
Au final, pour résoudre ce problème, j'ai décidé d'utiliser deux sources :
Une base de connaissances compilée manuellement de questions et de réponses, sélectionnée en mode semi-aveugle (souvent, je prenais le titre du sujet de la documentation comme question, et le paragraphe entier d'information comme réponse).
Messages exportés des développeurs de protocoles Discord des 2 dernières années (pour couvrir la période manquante à partir de fin 2021).
Ensuite, différentes approches ont été utilisées pour chaque source pour composer une requête à l'API ChatGPT, en particulier :
Pour les questions-réponses compilées manuellement,
pour chaque question, un encastrement contextuel est généré (un vecteur décrivant cette question dans un état multidimensionnel), obtenu grâce au modèle text-embedding-ada-002 ,
puis, à l'aide d'une fonction de recherche de distance cosinus, les 3 questions les plus similaires de la base de connaissances sont trouvées (au lieu de 3, le nombre le plus approprié pour votre jeu de données peut être utilisé),
les réponses à ces 3 questions sont ajoutées à l'invite finale avec une description approximative de " N'utilisez cet extrait de questions et réponses que s'il est pertinent pour la question donnée ".
Pour les messages exportés depuis Discord, l'algorithme suivant a été utilisé :
pour chaque message contenant un point d'interrogation, une intégration contextuelle est également générée (selon le même modèle),
puis, de manière similaire, les 5 questions les plus similaires sont sélectionnées,
et comme contexte de la réponse, on ajoute les 20 messages suivant cette question, supposés avoir une certaine probabilité de contenir la réponse à la question,
et cette information a été ajoutée à l'invite finale à peu près comme ceci : " Si vous n'avez pas trouvé de réponse explicite à la question dans l'extrait de Q&R ci-joint, les fragments de discussion suivants du développeur peuvent vous être utiles pour répondre à la question d'origine... "
De plus, si le sujet n'est pas explicitement indiqué, la présence d'extraits de questions-réponses et de chats peut entraîner une ambiguïté dans les réponses, qui peuvent ressembler, par exemple, à :
Ainsi, il comprend que la question a été détachée du contexte et la réponse a également été acceptée détachée du contexte. Ensuite, il a été dit que de telles données pouvaient être utilisées, et il les résume comme suit :
Pour éviter cela, nous introduisons le concept de sujet, qui est explicitement défini et inséré au début de l'invite comme suit :
"J'ai besoin d'obtenir une réponse à une question liée au sujet 'Le développement du sous-graphe graphique' : {{{qu'est-ce qu'un sous-graphe ?}}}"
De plus, dans la dernière phrase, j'ajoute également ceci:
Enfin, seulement si les informations ci-dessus ne sont pas suffisantes, vous pouvez utiliser vos connaissances dans le sujet 'Le développement du sous-graphe graphique' pour répondre à la question.
Au final, l'invite complète (à l'exclusion de la partie obtenue à partir des chats) se présente comme suit :
==I need to get an answer to the question related to the topic of "The Graph subgraph development": {{{what is a subgraph?}}}.== ==Possibly, you might find an answer in these Q&As \[use the information only if it is actually relevant and useful for the question answering\]:== ==Q: <What is a subgraph?>== ==A: <A subgraph is a custom API built on blockchain data. Subgraphs are queried using the GraphQL query language and are deployed to a Graph Node using the Graph CLI. Once deployed and published to The Graph's decentralized network, Indexers process subgraphs and make them available to be queried by subgraph consumers.>== ==Q: <Am I still able to create a subgraph if my smart contracts don't have events?>== ==A: <It is highly recommended that you structure your smart contracts to have events associated with data you are interested in querying. Event handlers in the subgraph are triggered by contract events and are by far the fastest way to retrieve useful data. If the contracts you are working with do not contain events, your subgraph can use call and block handlers to trigger indexing. Although this is not recommended, as performance will be significantly slower.>== ==Q: <How do I call a contract function or access a public state variable from my subgraph mappings?>== ==A: <Take a look at Access to smart contract state inside the section AssemblyScript API. https://thegraph.com/docs/en/developing/assemblyscript-api/>== ==Finally, only if the information above was not enough you can use your knowledge in the topic of "The Graph subgraph development" to answer the question.==
La réponse à la requête ci-dessus avec cette invite semi-auto-générée à l'entrée semble correcte dès le début :
Dans ce cas, le bot répond immédiatement avec la bonne clé et ajoute des informations plus pertinentes, de sorte que la réponse ne semble pas aussi simple que dans Q&A (je vous rappelle que cette question est exactement dans la liste des questions et réponses), mais avec des explications raisonnables qui répondent en partie aux questions suivantes.
Je dois noter tout de suite qu'il y aura un lien vers le référentiel à la fin , vous pouvez donc exécuter le bot tel quel, en remplaçant "topic" par le vôtre, le fichier de base de connaissances Q&A par le vôtre et en fournissant vos propres clés API pour OpenAI et le bot Telegram. La description ici n'est donc pas destinée à correspondre entièrement au code source sur GitHub, mais plutôt à mettre en évidence les principaux aspects du code.
Créons un nouvel environnement virtuel et installons les dépendances depuis requirements.txt :
virtualenv -p python3.8 .venv source .venv/bin/activate pip install -r requirements.txt
Comme mentionné ci-dessus, on suppose qu'il existe une liste de questions et réponses, en l'occurrence sous le format d'un fichier Excel du type suivant :
Afin de trouver la question la plus similaire à celle donnée, nous devons ajouter une incorporation de la question (un vecteur multidimensionnel dans l'espace d'état) à chaque ligne de ce fichier. Nous utiliserons le fichier add_embeddings.py pour cela. Le script se compose de plusieurs parties simples.
Importation de bibliothèques et lecture d'arguments de ligne de commande :
import pandas as pd import openai import argparse # Create an Argument Parser object parser = argparse.ArgumentParser(description='Adding embeddings for each line of csv file') # Add the arguments parser.add_argument('--openai_api_key', type=str, help='API KEY of OpenAI API to create contextual embeddings for each line') parser.add_argument('--file', type=str, help='A source CSV file with the text data') parser.add_argument('--colname', type=str, help='Column name with the texts') # Parse the command-line arguments args = parser.parse_args() # Access the argument values openai.api_key = args.openai_api_key file = args.file colname = args.colname
Ensuite, lisez le fichier dans une base de données pandas et filtrez les questions en fonction de la présence d'un point d'interrogation. Cet extrait de code est courant pour gérer une base de connaissances ainsi que les flux de messages bruts de Discord, donc en supposant que les questions sont souvent dupliquées, j'ai décidé de conserver une méthode aussi simple de filtrage grossier sans question.
if file[-4:] == '.csv': df = pd.read_csv(file) else: df = pd.read_excel(file) # filter NAs df = df[~df[colname].isna()] # Keep only questions df = df[df[colname].str.contains('\?')]
Et enfin - une fonction pour générer une incorporation en appelant l'API du modèle text-embedding-ada-002 , quelques requêtes répétées puisque l'API peut parfois être surchargée et peut répondre avec une erreur, et en appliquant cette fonction à chaque ligne de la trame de données.
def get_embedding(text, model="text-embedding-ada-002"): i = 0 max_try = 3 # to avoid random OpenAI API fails: while i < max_try: try: text = text.replace("\n", " ") result = openai.Embedding.create(input=[text], model=model)['data'][0]['embedding'] return result except: i += 1 def process_row(x): return get_embedding(x, model='text-embedding-ada-002') df['ada_embedding'] = df[colname].apply(process_row) df.to_csv(file[:-4]+'_question_embed.csv', index=False)
Au final, ce script peut être appelé avec la commande suivante :
python add_embeddings.py \ --openai_api_key="xxx" \ --file="./subgraphs_faq.xlsx" \ --colname="Question"
configurer la clé API OpenAI, le fichier avec la base de connaissances et le nom de la colonne où se trouve le texte de la question. Le fichier final créé, subgraphs_faq._question_embed.csv, contient les colonnes "Question", "Answer" et "ada_embedding ".
Si vous êtes intéressé par un bot simple qui répond uniquement sur la base de connaissances collectées manuellement, vous pouvez ignorer cette section et la suivante. Cependant, je vais brièvement fournir ici des exemples de code pour collecter des données à la fois d'un canal Discord et d'un groupe Telegram. Le fichier discord-channel-data-collection.py se compose de deux parties. La première partie comprend l'importation de bibliothèques et l'initialisation des arguments de ligne de commande :
import requests import json import pandas as pd import argparse # Create an Argument Parser object parser = argparse.ArgumentParser(description='Discord Channel Data Collection Script') # Add the arguments parser.add_argument('--channel_id', type=str, help='Channel ID from the URL of a channel in browser https://discord.com/channels/xxx/{CHANNEL_ID}') parser.add_argument('--authorization_key', type=str, help='Authorization Key. Being on the discord channel page, start typing anything, then open developer tools -> Network -> Find "typing" -> Headers -> Authorization.') # Parse the command-line arguments args = parser.parse_args() # Access the argument values channel_id = args.channel_id authorization_key = args.authorization_key
La seconde est la fonction permettant de récupérer les données du canal et de les enregistrer dans une trame de données pandas, ainsi que son appel avec des paramètres spécifiés.
def retrieve_messages(channel_id, authorization_key): num = 0 limit = 100 headers = { 'authorization': authorization_key } last_message_id = None # Create a pandas DataFrame df = pd.DataFrame(columns=['id', 'dt', 'text', 'author_id', 'author_username', 'is_bot', 'is_reply', 'id_reply']) while True: query_parameters = f'limit={limit}' if last_message_id is not None: query_parameters += f'&before={last_message_id}' r = requests.get( f'https://discord.com/api/v9/channels/{channel_id}/messages?{query_parameters}', headers=headers ) jsonn = json.loads(r.text) if len(jsonn) == 0: break for value in jsonn: is_reply = False id_reply = '0' if 'message_reference' in value and value['message_reference'] is not None: if 'message_id' in value['message_reference'].keys(): is_reply = True id_reply = value['message_reference']['message_id'] text = value['content'] if 'embeds' in value.keys(): if len(value['embeds'])>0: for x in value['embeds']: if 'description' in x.keys(): if text != '': text += ' ' + x['description'] else: text = x['description'] df_t = pd.DataFrame({ 'id': value['id'], 'dt': value['timestamp'], 'text': text, 'author_id': value['author']['id'], 'author_username': value['author']['username'], 'is_bot': value['author']['bot'] if 'bot' in value['author'].keys() else False, 'is_reply': is_reply, 'id_reply': id_reply, }, index=[0]) if len(df) == 0: df = df_t.copy() else: df = pd.concat([df, df_t], ignore_index=True) last_message_id = value['id'] num = num + 1 print('number of messages we collected is', num) # Save DataFrame to a CSV file df.to_csv(f'../discord_messages_{channel_id}.csv', index=False) if __name__ == '__main__': retrieve_messages(channel_id, authorization_key)
D'après les informations utiles ici, il y a un détail que je ne peux pas trouver à chaque fois que j'en ai besoin - l'obtention d'une clé d'autorisation. Considérant que le channel_id peut être obtenu à partir de l'URL du canal Discord ouvert dans le navigateur (le dernier nombre long dans le lien), l' autorisation_key ne peut être trouvée qu'en commençant à taper un message dans le canal, puis en utilisant les outils de développement pour trouver l'événement nommé " taper " dans la section Réseau et extraire le paramètre de l'en-tête.
Après avoir reçu ces paramètres, vous pouvez exécuter la commande suivante pour collecter tous les messages du canal (remplacez vos propres valeurs) :
python discord-channel-data-collection.py \ --channel_id=123456 \ --authorization_key="123456qwerty"
Étant donné que je télécharge souvent diverses données à partir de chats/canaux dans Telegram, j'ai également décidé de fournir un code pour cela, qui génère un fichier CSV au format similaire (compatible en termes de script add_embeddings.py ). Ainsi, le script telegram-group-data-collection.py ressemble à ceci. Importation de bibliothèques et initialisation d'arguments depuis la ligne de commande :
import pandas as pd import argparse from telethon import TelegramClient # Create an Argument Parser object parser = argparse.ArgumentParser(description='Telegram Group Data Collection Script') # Add the arguments parser.add_argument('--app_id', type=int, help='Telegram APP id from https://my.telegram.org/apps') parser.add_argument('--app_hash', type=str, help='Telegram APP hash from https://my.telegram.org/apps') parser.add_argument('--phone_number', type=str, help='Telegram user phone number with the leading "+"') parser.add_argument('--password', type=str, help='Telegram user password') parser.add_argument('--group_name', type=str, help='Telegram group public name without "@"') parser.add_argument('--limit_messages', type=int, help='Number of last messages to download') # Parse the command-line arguments args = parser.parse_args() # Access the argument values app_id = args.app_id app_hash = args.app_hash phone_number = args.phone_number password = args.password group_name = args.group_name limit_messages = args.limit_messages
Comme vous pouvez le constater, vous ne pouvez pas simplement télécharger tous les messages du chat sans vous autoriser en tant que première personne. En d'autres termes, en plus de créer une application via https://my.telegram.org/apps (en obtenant APP_ID et APP_HASH), vous devrez également utiliser votre numéro de téléphone et votre mot de passe pour créer une instance de la classe TelegramClient à partir de la bibliothèque Telethon.
De plus, vous aurez besoin du nom de groupe public du chat Telegram et spécifiez explicitement le nombre de derniers messages à récupérer. Dans l'ensemble, j'ai effectué cette procédure plusieurs fois avec n'importe quel nombre de messages exportés sans recevoir d'interdictions temporaires ou permanentes de l'API Telegram, contrairement à l'envoi trop fréquent de messages à partir d'un seul compte.
La seconde partie du script contient la fonction proprement dite d'export des messages et son exécution (avec le filtrage nécessaire pour éviter les erreurs critiques qui arrêteraient la collecte à mi-chemin) :
async def main(): messages = await client.get_messages(group_name, limit=limit_messages) df = pd.DataFrame(columns=['date', 'user_id', 'raw_text', 'views', 'forwards', 'text', 'chan', 'id']) for m in messages: if m is not None: if 'from_id' in m.__dict__.keys(): if m.from_id is not None: if 'user_id' in m.from_id.__dict__.keys(): df = pd.concat([df, pd.DataFrame([{'date': m.date, 'user_id': m.from_id.user_id, 'raw_text': m.raw_text, 'views': m.views, 'forwards': m.forwards, 'text': m.text, 'chan': group_name, 'id': m.id}])], ignore_index=True) df = df[~df['user_id'].isna()] df = df[~df['text'].isna()] df['date'] = pd.to_datetime(df['date']) df = df.sort_values('date').reset_index(drop=True) df.to_csv(f'../telegram_messages_{group_name}.csv', index=False) client = TelegramClient('session', app_id, app_hash) client.start(phone=phone_number, password=password) with client: client.loop.run_until_complete(main())
Au final, ce script peut être exécuté avec la commande suivante (remplacez les valeurs par les vôtres) :
python telegram-group-data-collection.py \ --app_id=123456 --app_hash="123456qwerty" \ --phone_number="+xxxxxx" --password="qwerty123" \ --group_name="xxx" --limit_messages=10000
La plupart du temps, j'enveloppe mes projets favoris dans des robots Telegram, car leur lancement nécessite un minimum d'efforts et montre immédiatement un potentiel. Dans ce cas, j'ai fait la même chose. Je dois dire que le code du bot ne contient pas tous les cas de coin que j'utilise dans la version de production du bot SubgraphGPT , car il a beaucoup de logique héritée d'un autre de mes projets favoris. Au lieu de cela, j'ai laissé la quantité minimale de code de base qui devrait être facile à modifier selon vos besoins.
Le script telegram-bot.py se compose de plusieurs parties. Tout d'abord, comme précédemment, les bibliothèques sont importées et les arguments de la ligne de commande sont initialisés.
import threading import telegram from telegram.ext import Updater, CommandHandler, MessageHandler, Filters import openai from openai.embeddings_utils import cosine_similarity import numpy as np import pandas as pd import argparse import functools # Create an Argument Parser object parser = argparse.ArgumentParser(description='Run the bot which uses prepared knowledge base enriched with contextual embeddings') # Add the arguments parser.add_argument('--openai_api_key', type=str, help='API KEY of OpenAI API to create contextual embeddings for each line') parser.add_argument('--telegram_bot_token', type=str, help='A telegram bot token obtained via @BotFather') parser.add_argument('--file', type=str, help='A source CSV file with the questions, answers and embeddings') parser.add_argument('--topic', type=str, help='Write the topic to add a default context for the bot') parser.add_argument('--start_message', type=str, help="The text that will be shown to the users after they click /start button/command", default="Hello, World!") parser.add_argument('--model', type=str, help='A model of ChatGPT which will be used', default='gpt-3.5-turbo-16k') parser.add_argument('--num_top_qa', type=str, help="The number of top similar questions' answers as a context", default=3) # Parse the command-line arguments args = parser.parse_args() # Access the argument values openai.api_key = args.openai_api_key token = args.telegram_bot_token file = args.file topic = args.topic model = args.model num_top_qa = args.num_top_qa start_message = args.start_message
Veuillez noter que dans ce cas, vous aurez également besoin d'une clé API OpenAI, car pour trouver la question la plus similaire à celle que l'utilisateur vient de saisir dans la base de connaissances, vous devez d'abord obtenir l'intégration de cette question en appelant l'API comme nous l'avons fait pour la base de connaissances elle-même.
De plus, vous aurez besoin de :
Vient ensuite le chargement du fichier de la base de connaissances et l'initialisation des plongements de questions.
# reading QA file with embeddings df_qa = pd.read_csv(file) df_qa['ada_embedding'] = df_qa.ada_embedding.apply(eval).apply(np.array)
Pour faire une requête à l'API ChatGPT, sachant qu'elle répond parfois par une erreur due à une surcharge, j'utilise une fonction avec relance automatique de la requête en cas d'erreur.
def retry_on_error(func): @functools.wraps(func) def wrapper(*args, **kwargs): max_retries = 3 for i in range(max_retries): try: return func(*args, **kwargs) except Exception as e: print(f"Error occurred, retrying ({i+1}/{max_retries} attempts)...") # If all retries failed, raise the last exception raise e return wrapper @retry_on_error def call_chatgpt(*args, **kwargs): return openai.ChatCompletion.create(*args, **kwargs)
Selon la recommandation d'OpenAI, avant de convertir le texte en incorporations, les nouvelles lignes doivent être remplacées par des espaces.
def get_embedding(text, model="text-embedding-ada-002"): text = text.replace("\n", " ") return openai.Embedding.create(input=[text], model=model)['data'][0]['embedding']
Pour rechercher les questions les plus similaires, nous calculons la distance cosinus entre les plongements de deux questions, extraites directement de la bibliothèque openai.
def search_similar(df, question, n=3, pprint=True): embedding = get_embedding(question, model='text-embedding-ada-002') df['similarities'] = df.ada_embedding.apply(lambda x: cosine_similarity(x, embedding)) res = df.sort_values('similarities', ascending=False).head(n) return res
Après avoir reçu une liste des paires question-réponse les plus similaires à celle donnée, vous pouvez les compiler en un seul texte, en le marquant de manière à ce que ChatGPT puisse déterminer sans ambiguïté ce qui est quoi.
def collect_text_qa(df): text = '' for i, row in df.iterrows(): text += f'Q: <'+row['Question'] + '>\nA: <'+ row['Answer'] +'>\n\n' print('len qa', len(text.split(' '))) return text
Après cela, il est déjà nécessaire de rassembler les "morceaux" de l'invite décrite au tout début de l'article en un tout.
def collect_full_prompt(question, qa_prompt, chat_prompt=None): prompt = f'I need to get an answer to the question related to the topic of "{topic}": ' + "{{{"+ question +"}}}. " prompt += '\n\nPossibly, you might find an answer in these Q&As [use the information only if it is actually relevant and useful for the question answering]: \n\n' + qa_prompt # edit if you need to use this also if chat_prompt is not None: prompt += "---------\nIf you didn't find a clear answer in the Q&As, possibly, these talks from chats might be helpful to answer properly [use the information only if it is actually relevant and useful for the question answering]: \n\n" + chat_prompt prompt += f'\nFinally, only if the information above was not enough you can use your knowledge in the topic of "{topic}" to answer the question.' return prompt
Dans ce cas, j'ai supprimé la partie utilisant les messages de Discord, mais vous pouvez toujours suivre la logique si chat_prompt != None.
De plus, nous aurons besoin d'une fonction qui divise la réponse reçue de l'API ChatGPT en messages Telegram (pas plus de 4096 caractères) :
def telegram_message_format(text): max_message_length = 4096 if len(text) > max_message_length: parts = [] while len(text) > max_message_length: parts.append(text[:max_message_length]) text = text[max_message_length:] parts.append(text) return parts else: return [text]
Le bot commence par une séquence typique d'étapes, attribuant deux fonctions à déclencher par la commande /start et recevant un message personnel de l'utilisateur :
bot = telegram.Bot(token=token) updater = Updater(token=token, use_context=True) dispatcher = updater.dispatcher dispatcher.add_handler(CommandHandler("start", start, filters=Filters.chat_type.private)) dispatcher.add_handler(MessageHandler(~Filters.command & Filters.text, message_handler)) updater.start_polling()
Le code pour répondre à /start est simple :
def start(update, context): user = update.effective_user context.bot.send_message(chat_id=user.id, text=start_message)
Et pour répondre à un message de forme libre, ce n'est pas tout à fait clair.
Tout d'abord , pour éviter de bloquer les threads de différents utilisateurs, "séparons-les" immédiatement en processus indépendants à l'aide de la bibliothèque de threads .
def message_handler(update, context): thread = threading.Thread(target=long_running_task, args=(update, context)) thread.start()
Deuxièmement , toute la logique se produira à l'intérieur de la fonction long_running_task . J'ai intentionnellement enveloppé les fragments principaux dans try/except pour localiser facilement les erreurs lors de la modification du code du bot.
def long_running_task(update, context): user = update.effective_user context.bot.send_message(chat_id=user.id, text='🕰️⏰🕙⏱️⏳...') try: question = update.message.text.strip() except Exception as e: context.bot.send_message(chat_id=user.id, text=f"🤔It seems like you're sending not text to the bot. Currently, the bot can only work with text requests.") return try: qa_found = search_similar(df_qa, question, n=num_top_qa) qa_prompt = collect_text_qa(qa_found) full_prompt = collect_full_prompt(question, qa_prompt) except Exception as e: context.bot.send_message(chat_id=user.id, text=f"Search failed. Debug needed.") return
Étant donné qu'il peut y avoir des erreurs lors du remplacement de la base de connaissances et de la rubrique par les vôtres, par exemple en raison du formatage, une erreur lisible par l'homme s'affiche.
Ensuite, la requête est envoyée à l'API ChatGPT avec un message système principal qui a déjà fait ses preuves : " Vous êtes un assistant utile. " La sortie résultante est divisée en plusieurs messages si nécessaire et renvoyée à l'utilisateur.
try: print(full_prompt) completion = call_chatgpt( model=model, n=1, messages=[{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": full_prompt}] ) result = completion['choices'][0]['message']['content'] except Exception as e: context.bot.send_message(chat_id=user.id, text=f'It seems like the OpenAI service is responding with errors. Try sending the request again.') return parts = telegram_message_format(result) for part in parts: update.message.reply_text(part, reply_to_message_id=update.message.message_id)
Cela conclut la partie avec le code.
Désormais, un prototype d'un tel bot est disponible dans un format limité sur le lien suivant . Comme l'API est payante, vous pouvez faire jusqu'à 3 requêtes par jour, mais je ne pense pas que cela limitera qui que ce soit, car la chose la plus intéressante n'est pas un bot spécialisé axé sur un sujet étroit, mais le code du projet AnythingGPT, qui est disponible sur GitHub avec une courte instruction sur la façon de créer votre propre bot pour résoudre votre tâche spécifique avec votre base de connaissances basée sur cet exemple. Si vous avez lu jusqu'au bout, j'espère que cet article vous a été utile.