Hallo zusammen! Kürzlich habe ich in meiner Praxis eine interessante Lösung angewendet, die ich schon lange ausprobieren wollte, und jetzt möchte ich Ihnen erklären, wie Sie für jede andere Aufgabe etwas Ähnliches erstellen können. Wir werden über die Erstellung einer angepassten Version von ChatGPT sprechen, die Fragen beantwortet und dabei eine große Wissensdatenbank berücksichtigt, deren Länge nicht durch die Größe der Eingabeaufforderung begrenzt ist (was bedeutet, dass Sie nicht einfach alle Informationen vor jeder Eingabe hinzufügen können). Frage an ChatGPT).
Um dies zu erreichen, verwenden wir kontextbezogene Einbettungen von OpenAI (für eine wirklich hochwertige Suche nach relevanten Fragen aus der Wissensdatenbank) und die ChatGPT-API selbst (um die Antworten in natürlicher menschlicher Sprache zu formatieren).
Darüber hinaus wird davon ausgegangen, dass der Assistent nicht nur die explizit genannten Frage-und-Antwort-Fragen beantworten kann, sondern auch Fragen, die eine mit der Frage-und-Antwort-Runde vertraute Person beantworten könnte. Wenn Sie daran interessiert sind, zu erfahren, wie Sie einfache Bots erstellen, die mithilfe einer großen Wissensdatenbank reagieren, sind Sie bei den Details willkommen.
Ich möchte darauf hinweisen, dass es einige Bibliotheksprojekte gibt, die versuchen, diese Aufgabe in Form eines Frameworks zu lösen, zum Beispiel LangChain , und ich habe es auch versucht. Allerdings neigt es, wie jedes Framework, das sich in einem frühen Entwicklungsstadium befindet, in manchen Fällen eher dazu, Dinge einzuschränken als zu vereinfachen. Insbesondere verstand ich von Anfang an, als ich diese Aufgabe löste, was ich mit den Daten machen wollte und wusste, wie ich es selbst tun konnte (einschließlich kontextbasierter Suche, Einstellung des richtigen Kontexts in Eingabeaufforderungen, Kombination von Informationsquellen).
Aber ich konnte das Framework nicht so konfigurieren, dass es genau das mit einem akzeptablen Qualitätsniveau erledigt, und das Debuggen des Frameworks schien für diese Aufgabe übertrieben zu sein. Am Ende habe ich meinen eigenen Boilerplate-Code erstellt und war mit diesem Ansatz zufrieden.
Lassen Sie mich die Aufgabe, an der ich gearbeitet habe, kurz beschreiben . Sie können denselben Code in Ihren eigenen Aufgaben verwenden und die Datenquellen und Eingabeaufforderungen durch diejenigen ersetzen, die für Sie am besten geeignet sind . Sie haben weiterhin die volle Kontrolle über die Logik des Bots.
Beim Schreiben von Code verwende ich oft ChatGPT (und ich schäme mich nicht dafür 🙂). Aufgrund fehlender Daten für das Jahr 2022+ kommt es jedoch manchmal zu Problemen mit relativ neuen Technologien.
Insbesondere bei der Entwicklung von Untergraphen für das Graph-Protokoll (die beliebteste Methode zum Erstellen von ETL zum Abrufen indizierter Daten aus EVM-kompatiblen Blockchains, mehr darüber können Sie in meinen vorherigen Artikeln [ 1 ] und [ 2 ] lesen), die Bibliotheken selbst wurden mehreren wichtigen Kompatibilitätsänderungen unterzogen. Die „alten“ Antworten von ChatGPT sind nicht mehr hilfreich und ich muss nach den richtigen Antworten entweder in der spärlichen Dokumentation oder im schlimmsten Fall im Discord der Entwickler suchen, was nicht sehr praktisch ist (es ist nicht wie StackOverflow).
Der zweite Teil des Problems besteht darin, dass Sie den Konversationskontext jedes Mal korrekt bereitstellen müssen, da ChatGPT häufig vom Thema Untergraphen abweicht und zu GraphQL, SQL oder höherer Mathematik springt („The Graph“, „Subgraphs“ usw.). (keine eindeutigen Begriffe und viele verschiedene Interpretationen und Themen).
Deshalb habe ich mich nach einer kurzen Zeit des Kampfes mit ChatGPT um die Korrektur von Fehlern im Subgraph-Code entschieden, meinen eigenen SubgraphGPT- Bot zu erstellen, der sich immer im richtigen Kontext befindet und versucht, unter Berücksichtigung der Wissensdatenbank und der Meldungen von Discord-Entwicklern zu antworten .
PS. Ich arbeite als leitender Produktmanager bei chainstack.com , einem Web3-Infrastrukturanbieter, und bin für die Entwicklung des Subgraph-Hosting- Dienstes verantwortlich. Deshalb muss ich viel mit Untergraphen arbeiten, um den Benutzern das Verständnis dieser relativ neuen Technologie zu erleichtern.
Um dieses Problem zu lösen, habe ich mich letztendlich für die Verwendung von zwei Quellen entschieden:
Eine manuell zusammengestellte Wissensdatenbank mit Fragen und Antworten, die im Halbblindmodus ausgewählt wurden (oft habe ich den Thementitel aus der Dokumentation als Frage und den gesamten Informationsabschnitt als Antwort genommen).
Exportierte Nachrichten der Protokollentwickler Discord aus den letzten 2 Jahren (um den fehlenden Zeitraum ab Ende 2021 abzudecken).
Als nächstes wurden für jede Quelle unterschiedliche Ansätze verwendet, um eine Anfrage an die ChatGPT-API zu verfassen, insbesondere:
Für die manuell zusammengestellten Fragen und Antworten:
Für jede Frage wird eine kontextuelle Einbettung generiert (ein Vektor, der diese Frage in einem mehrdimensionalen Zustand beschreibt), der durch das Modell text-embedding-ada-002 erhalten wird.
Anschließend werden mithilfe einer Kosinus-Distanz-Suchfunktion die drei ähnlichsten Fragen aus der Wissensdatenbank gefunden (anstelle von drei kann die für Ihren Datensatz am besten geeignete Zahl verwendet werden).
Die Antworten auf diese drei Fragen werden der endgültigen Eingabeaufforderung mit der ungefähren Beschreibung „ Verwenden Sie dieses Q&A-Snippet nur, wenn es für die gegebene Frage relevant ist “ hinzugefügt.
Für die aus Discord exportierten Nachrichten wurde der folgende Algorithmus verwendet:
Für jede Nachricht, die ein Fragezeichen enthält, wird auch eine kontextbezogene Einbettung generiert (unter Verwendung desselben Modells).
Dann werden auf ähnliche Weise die fünf ähnlichsten Fragen ausgewählt.
und als Kontext für die Antwort werden die 20 Nachrichten nach dieser Frage hinzugefügt, von denen angenommen wird, dass sie mit einer bestimmten Wahrscheinlichkeit die Antwort auf die Frage enthalten.
und diese Informationen wurden der endgültigen Eingabeaufforderung ungefähr so hinzugefügt: „Wenn Sie im angehängten Q&A-Snippet keine explizite Antwort auf die Frage gefunden haben, können die folgenden Chatfragmente des Entwicklers für die Beantwortung der ursprünglichen Frage hilfreich sein. . "
Darüber hinaus kann das Vorhandensein von Q&A-Snippets und Chats bei nicht expliziter Themenangabe zu Mehrdeutigkeiten in den Antworten führen, die beispielsweise wie folgt aussehen können:
Es geht also davon aus, dass die Frage losgelöst vom Kontext gestellt wurde und die Antwort auch losgelöst vom Kontext akzeptiert wurde. Dann wurde gesagt, dass solche Daten verwendet werden könnten, und fasste es wie folgt zusammen:
Um dies zu vermeiden, führen wir das Konzept eines Themas ein, das explizit definiert und am Anfang der Eingabeaufforderung wie folgt eingefügt wird:
„Ich benötige eine Antwort auf eine Frage zum Thema „Entwicklung von The Graph-Subgraphen“: {{{Was ist ein Subgraph?}}}“
Darüber hinaus füge ich im letzten Satz noch Folgendes hinzu:
Nur wenn die oben genannten Informationen nicht ausreichen, können Sie Ihr Wissen im Thema „Entwicklung von Graph-Subgraphen“ zur Beantwortung der Frage nutzen.
Am Ende sieht die vollständige Eingabeaufforderung (mit Ausnahme des aus Chats erhaltenen Teils) wie folgt aus:
==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.==
Die Antwort auf die obige Anfrage mit dieser halbautomatisch generierten Eingabeaufforderung sieht von Anfang an korrekt aus:
In diesem Fall antwortet der Bot sofort mit dem richtigen Schlüssel und fügt weitere relevante Informationen hinzu, sodass die Antwort nicht so einfach aussieht wie in Fragen und Antworten (ich erinnere Sie daran, dass diese Frage genau in der Liste der Fragen und Antworten steht), sondern mit vernünftige Erklärungen, die teilweise auf die folgenden Fragen eingehen.
Ich sollte gleich darauf hinweisen, dass es am Ende einen Link zum Repository geben wird , sodass Sie den Bot so ausführen können, wie er ist, indem Sie „Thema“ durch Ihr eigenes, die Q&A-Wissensdatenbankdatei durch Ihr eigenes ersetzen und Ihre eigenen API-Schlüssel bereitstellen können für OpenAI und den Telegram-Bot. Daher soll die Beschreibung hier nicht vollständig dem Quellcode auf GitHub entsprechen, sondern vielmehr die Hauptaspekte des Codes hervorheben.
Lassen Sie uns eine neue virtuelle Umgebung erstellen und die Abhängigkeiten aus „requirements.txt“ installieren:
virtualenv -p python3.8 .venv source .venv/bin/activate pip install -r requirements.txt
Wie oben erwähnt wird davon ausgegangen, dass eine Liste mit Fragen und Antworten vorliegt, in diesem Fall im Format einer Excel-Datei des folgenden Typs:
Um die Frage zu finden, die der gegebenen Frage am ähnlichsten ist, müssen wir jeder Zeile dieser Datei eine Einbettung der Frage (einen mehrdimensionalen Vektor im Zustandsraum) hinzufügen. Wir verwenden hierfür die Datei add_embeddings.py . Das Skript besteht aus mehreren einfachen Teilen.
Bibliotheken importieren und Befehlszeilenargumente lesen:
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
Als nächstes lesen Sie die Datei in einen Pandas-Datenrahmen ein und filtern die Fragen basierend auf dem Vorhandensein eines Fragezeichens. Dieses Code-Snippet wird häufig für den Umgang mit einer Wissensdatenbank sowie rohen Nachrichtenströmen von Discord verwendet. Unter der Annahme, dass Fragen häufig dupliziert werden, habe ich mich entschieden, eine so einfache Methode der groben Nicht-Fragen-Filterung beizubehalten.
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('\?')]
Und schließlich – eine Funktion zum Generieren einer Einbettung durch Aufrufen der API des Modells text-embedding-ada-002 , ein paar wiederholte Anfragen, da die API gelegentlich überlastet sein kann und möglicherweise mit einem Fehler antwortet, und Anwenden dieser Funktion auf jede Zeile des Datenrahmens.
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)
Abschließend kann dieses Skript mit folgendem Befehl aufgerufen werden:
python add_embeddings.py \ --openai_api_key="xxx" \ --file="./subgraphs_faq.xlsx" \ --colname="Question"
Einrichten des OpenAI-API-Schlüssels, der Datei mit der Wissensdatenbank und des Namens der Spalte, in der sich der Fragetext befindet. Die endgültig erstellte Datei, subgraphs_faq._question_embed.csv, enthält die Spalten „Question“, „Answer“ und „ada_embedding “.
Wenn Sie an einem einfachen Bot interessiert sind, der nur auf der Grundlage einer manuell erfassten Wissensdatenbank antwortet, können Sie diesen und den folgenden Abschnitt überspringen. Ich werde hier jedoch kurz Codebeispiele zum Sammeln von Daten sowohl von einem Discord-Kanal als auch von einer Telegram-Gruppe bereitstellen. Die Datei discord-channel-data-collection.py besteht aus zwei Teilen. Der erste Teil umfasst das Importieren von Bibliotheken und das Initialisieren von Befehlszeilenargumenten:
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
Die zweite Funktion ist die Funktion zum Abrufen von Daten aus dem Kanal und deren Speicherung in einem Pandas-Datenrahmen sowie deren Aufruf mit angegebenen Parametern.
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)
Unter den nützlichen Informationen hier gibt es ein Detail, das ich nicht jedes Mal finden kann, wenn ich es brauche – den Erhalt eines Autorisierungsschlüssels. Wenn man bedenkt, dass die Kanal-ID über die URL des im Browser geöffneten Discord-Kanals (die letzte lange Zahl im Link) abgerufen werden kann, kann der Autorisierungsschlüssel nur gefunden werden, indem man beginnt, eine Nachricht in den Kanal einzugeben und dann mit Entwicklertools nach der Nachricht zu suchen Ereignis mit dem Namen „ typing “ im Abschnitt „Netzwerk“ und extrahieren Sie den Parameter aus dem Header.
Nachdem Sie diese Parameter erhalten haben, können Sie den folgenden Befehl ausführen, um alle Nachrichten vom Kanal zu sammeln (ersetzen Sie Ihre eigenen Werte):
python discord-channel-data-collection.py \ --channel_id=123456 \ --authorization_key="123456qwerty"
Da ich oft verschiedene Daten aus Chats/Kanälen in Telegram herunterlade, habe ich mich entschieden, auch hierfür Code bereitzustellen, der eine CSV-Datei im ähnlichen Format (kompatibel im Hinblick auf das Skript add_embeddings.py ) generiert. Das Skript telegram-group-data-collection.py sieht also wie folgt aus. Importieren von Bibliotheken und Initialisieren von Argumenten über die Befehlszeile:
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
Wie Sie sehen, können Sie nicht einfach alle Nachrichten aus dem Chat herunterladen, ohne sich als erste Person zu autorisieren. Mit anderen Worten: Sie müssen nicht nur eine App über https://my.telegram.org/apps erstellen (APP_ID und APP_HASH erhalten), sondern auch Ihre Telefonnummer und Ihr Passwort verwenden, um eine Instanz der TelegramClient-Klasse aus der Telethon-Bibliothek zu erstellen .
Darüber hinaus benötigen Sie den öffentlichen Gruppennamen des Telegram-Chats und geben explizit die Anzahl der letzten abzurufenden Nachrichten an. Insgesamt habe ich diesen Vorgang viele Male mit einer beliebigen Anzahl exportierter Nachrichten durchgeführt, ohne vorübergehende oder dauerhafte Sperren von der Telegram-API zu erhalten, anders als wenn man zu häufig Nachrichten von einem Konto sendet.
Der zweite Teil des Skripts enthält die eigentliche Funktion zum Exportieren von Nachrichten und deren Ausführung (mit der notwendigen Filterung, um kritische Fehler zu vermeiden, die die Sammlung auf halbem Weg stoppen würden):
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())
Am Ende kann dieses Skript mit dem folgenden Befehl ausgeführt werden (Werte durch eigene ersetzen):
python telegram-group-data-collection.py \ --app_id=123456 --app_hash="123456qwerty" \ --phone_number="+xxxxxx" --password="qwerty123" \ --group_name="xxx" --limit_messages=10000
Meistens verpacke ich meine Lieblingsprojekte in Telegram-Bots, weil der Start nur minimalen Aufwand erfordert und sofort Potenzial zeigt. In diesem Fall habe ich das Gleiche getan. Ich muss sagen, dass der Bot-Code nicht alle Eckfälle enthält, die ich in der Produktionsversion des SubgraphGPT- Bots verwende, da er ziemlich viel Logik von einem anderen Lieblingsprojekt von mir übernommen hat. Stattdessen habe ich die Mindestmenge an Basiscode belassen, die sich leicht an Ihre Bedürfnisse anpassen lässt.
Das Skript telegram-bot.py besteht aus mehreren Teilen. Zunächst werden wie zuvor Bibliotheken importiert und Befehlszeilenargumente initialisiert.
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
Bitte beachten Sie, dass Sie in diesem Fall auch einen OpenAI-API-Schlüssel benötigen, denn um in der Wissensdatenbank die Frage zu finden, die der gerade vom Benutzer eingegebenen Frage am ähnlichsten ist, müssen Sie zunächst die Einbettung dieser Frage per Aufruf erreichen die API, wie wir es für die Wissensdatenbank selbst getan haben.
Darüber hinaus benötigen Sie:
Anschließend erfolgt das Laden der Wissensdatenbankdatei und die Initialisierung der Frageneinbettungen.
# reading QA file with embeddings df_qa = pd.read_csv(file) df_qa['ada_embedding'] = df_qa.ada_embedding.apply(eval).apply(np.array)
Um eine Anfrage an die ChatGPT-API zu stellen, obwohl ich weiß, dass diese aufgrund einer Überlastung manchmal mit einem Fehler antwortet, verwende ich eine Funktion mit automatischer Wiederholung der Anfrage im Fehlerfall.
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)
Laut Empfehlung von OpenAI sollten vor der Umwandlung des Textes in Einbettungen neue Zeilen durch Leerzeichen ersetzt werden.
def get_embedding(text, model="text-embedding-ada-002"): text = text.replace("\n", " ") return openai.Embedding.create(input=[text], model=model)['data'][0]['embedding']
Um nach den ähnlichsten Fragen zu suchen, berechnen wir den Kosinusabstand zwischen den Einbettungen zweier Fragen, der direkt aus der OpenAI-Bibliothek entnommen wird.
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
Nachdem Sie eine Liste der dem angegebenen Frage-Antwort-Paar am ähnlichsten erhalten haben, können Sie diese in einem Text zusammenfassen und ihn so markieren, dass ChatGPT eindeutig bestimmen kann, was was ist.
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
Danach gilt es bereits, die zu Beginn des Artikels beschriebenen „Teile“ der Aufforderung zu einem Ganzen zusammenzufügen.
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
In diesem Fall habe ich den Teil mithilfe von Nachrichten aus Discord entfernt, aber Sie können weiterhin der Logik folgen, wenn chat_prompt != None.
Darüber hinaus benötigen wir eine Funktion, die die von der ChatGPT-API empfangene Antwort in Telegram-Nachrichten (nicht mehr als 4096 Zeichen) aufteilt:
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]
Der Bot beginnt mit einer typischen Schrittfolge, weist zwei Funktionen zu, die durch den /start -Befehl ausgelöst werden sollen, und empfängt eine persönliche Nachricht vom Benutzer:
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()
Der Code zum Antworten auf /start ist unkompliziert:
def start(update, context): user = update.effective_user context.bot.send_message(chat_id=user.id, text=start_message)
Und für die Antwort auf eine formlose Nachricht ist es nicht ganz klar.
Um zu vermeiden, dass Threads verschiedener Benutzer blockiert werden, „trennen“ wir sie zunächst mithilfe der Threading- Bibliothek sofort in unabhängige Prozesse.
def message_handler(update, context): thread = threading.Thread(target=long_running_task, args=(update, context)) thread.start()
Zweitens wird die gesamte Logik innerhalb der Funktion long_running_task ablaufen. Ich habe die Hauptfragmente absichtlich in try/exclusive verpackt, um Fehler beim Ändern des Bot-Codes leicht lokalisieren zu können.
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
Da es beim Ersetzen der Wissensdatenbank und des Themas durch Ihre eigene zu Fehlern kommen kann, beispielsweise aufgrund von Formatierungen, wird ein für Menschen lesbarer Fehler angezeigt.
Als nächstes wird die Anfrage an die ChatGPT-API mit einer führenden Systemnachricht gesendet, die sich bereits bewährt hat: „ Sie sind ein hilfreicher Assistent. “ Die resultierende Ausgabe wird bei Bedarf in mehrere Nachrichten aufgeteilt und an den Benutzer zurückgesendet.
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)
Damit ist der Teil mit dem Code abgeschlossen.
Nun ist ein Prototyp eines solchen Bots in begrenztem Format unter folgendem Link verfügbar. Da die API kostenpflichtig ist, können Sie bis zu 3 Anfragen pro Tag stellen, aber ich glaube nicht, dass dies irgendjemanden einschränkt, da das Interessanteste nicht ein spezialisierter Bot ist, der sich auf ein enges Thema konzentriert, sondern der Code des AnythingGPT-Projekts , das auf GitHub verfügbar ist, mit einer kurzen Anleitung, wie Sie basierend auf diesem Beispiel Ihren eigenen Bot erstellen, um Ihre spezifische Aufgabe mit Ihrer Wissensdatenbank zu lösen. Wenn Sie bis zum Ende gelesen haben, hoffe ich, dass dieser Artikel für Sie hilfreich war.