paint-brush
独自の AnythingGPT を作成する方法 — 希望どおりに応答するボット@balakhonoff
1,545 測定値
1,545 測定値

独自の AnythingGPT を作成する方法 — 希望どおりに応答するボット

Kirill Balakhonov24m2023/07/25
Read on Terminal Reader

長すぎる; 読むには

大規模な知識ベースを考慮して、質問に答える ChatGPT のカスタマイズされたバージョンの作成について説明します。 OpenAI のコンテキスト埋め込みを使用します (ナレッジ ベースから関連する質問を真に高品質に検索するため)。回答は人間の自然言語でフォーマットされます。
featured image - 独自の AnythingGPT を作成する方法 — 希望どおりに応答するボット
Kirill Balakhonov HackerNoon profile picture
0-item
1-item
2-item


こんにちは、みんな!最近、長い間試してみたかった興味深いソリューションを練習中に適用しました。これで、他のタスクでも同様のものを作成する方法を説明する準備が整いました。プロンプトのサイズによって長さが制限されない大規模な知識ベースを考慮して、質問に答える ChatGPT のカスタマイズされたバージョンの作成について説明します (つまり、各質問の前にすべての情報を ChatGPT に単純に追加することはできません)。


これを実現するために、OpenAI のコンテキスト埋め込み (ナレッジ ベースから関連する質問を真に高品質に検索するため) と ChatGPT API 自体 (人間の自然言語で回答をフォーマットするため) を使用します。


さらに、アシスタントは、明示的に記載された Q&A の質問だけでなく、Q&A に精通している人が回答できる質問にも回答できると想定されます。大規模なナレッジ ベースを使用して応答するシンプルなボットの作成方法を学習することに興味がある場合は、詳細へようこそ。


このタスクをフレームワークの形で解決しようとするライブラリ プロジェクトがいくつかあることを指摘しておきたいと思います。たとえば、 LangChain です。私もそれを使用してみました。ただし、開発の初期段階にあるフレームワークと同様に、場合によっては、物事を簡素化するのではなく、制限する傾向があります。特に、このタスクを解決する最初の段階から、データで何をしたいのかを理解し、それを自分で行う方法 (コンテキストベースの検索、プロンプトでの正しいコンテキストの設定、情報ソースの結合など) を理解しました。


しかし、許容できるレベルの品質でそれを正確に実行するようにフレームワークを構成することはできず、フレームワークのデバッグはこのタスクにはやりすぎのように思えました。最終的に、私は独自の定型コードを作成し、このアプローチに満足しました。

タスク

私が取り組んでいたタスクについて簡単に説明します。データ ソースとプロンプトを自分に合ったものに置き換えて、同じコードを自分のタスクで使用できます。ボットのロジックを完全に制御できます。


コードを書くとき、私は ChatGPT をよく使います (恥ずかしくないです 🙂)。ただし、2022 年以降のデータが不足しているため、比較的新しいテクノロジーでは問題が発生することがあります。


特に、The Graph プロトコル (EVM 互換のブロックチェーンからインデックス付きデータを取得するための ETLを構築する最も一般的な方法です。詳細については、以前の記事 [ 1 ] および [ 2 ] を参照してください) のサブグラフを開発する場合、ライブラリ自体にいくつかの互換性を破る変更が加えられました。 ChatGPT からの「古い」回答はもう役に立たず、正しい回答を不足しているドキュメントか、最悪の場合開発者の Discord で探す必要がありますが、これはあまり便利ではありません (StackOverflow とは違います)。


問題の 2 番目の部分は、ChatGPT がサブグラフのトピックから逸れ、GraphQL、SQL、または高等数学にジャンプすることがよくあるため、毎回会話コンテキストを正しく提供する必要があることです (「グラフ」、「サブグラフ」などは固有の用語ではなく、多くの異なる解釈やトピックがあります)。


したがって、サブグラフ コードのエラーを修正するために ChatGPT で短期間苦労した後、ナレッジ ベースと開発者 Discord からのメッセージを考慮して、常に正しいコンテキストにあり、回答を試みる独自のSubgraphGPTボットを作成することにしました。


PS.私は Web3 インフラストラクチャ プロバイダーであるchainstack.comでリード プロダクト マネージャーとして働いており、サブグラフ ホスティングサービスの開発を担当しています。そのため、この比較的新しいテクノロジーをユーザーが理解できるように、サブグラフを頻繁に操作する必要があります。

トップレベルのソリューション

最終的に、この問題を解決するために、次の 2 つのソースを使用することにしました。

  1. セミブラインド モードで選択された、手動で編集された質問と回答のナレッジ ベース (ドキュメントのトピック タイトルを質問として、情報の段落全体を回答として取得することがよくありました)。

  2. 過去 2 年間のプロトコル開発者 Discord からのメッセージをエクスポートしました (2021 年末からの欠落期間をカバーするため)。


次に、各ソースに対してさまざまなアプローチを使用して、ChatGPT API へのリクエストを作成しました。具体的には次のとおりです。


手動でまとめられた Q&A については、

  1. 質問ごとに、 text-embedding-ada-002モデルを通じて取得されたコンテキスト埋め込み (この質問を多次元状態で記述するベクトル) が生成されます。

  2. 次に、コサイン距離検索関数を使用して、ナレッジ ベースから最も類似した質問の上位 3 つが検索されます (3 つの代わりに、データセットに最適な数を使用できます)。

  3. これら 3 つの質問に対する回答は、「指定された質問に関連する場合にのみ、この Q&A スニペットを使用してください」という大まかな説明とともに最終プロンプトに追加されます。


    Discord からエクスポートされたメッセージには、次のアルゴリズムが使用されました。

  4. 疑問符を含むメッセージごとに、コンテキスト埋め込みも (同じモデルを使用して) 生成されます。

  5. 次に、同様の方法で、最も類似した質問の上位 5 つが選択されます。

  6. そして、回答のコンテキストとして、その質問に続く 20 個のメッセージが追加されます。これらのメッセージには、質問に対する回答が一定の確率で含まれていると想定されます。

  7. この情報は、最終プロンプトに次のように追加されました。「添付の Q&A スニペットで質問に対する明確な回答が見つからなかった場合は、開発者による次のチャットの断片が元の質問に答えるのに役立つ可能性があります...」


さらに、トピックが明示的に指定されていない場合、Q&A の抜粋やチャットの存在により、回答が曖昧になる可能性があります。たとえば、次のようになります。



つまり、質問が文脈から切り離されており、回答も文脈から切り離されて受け入れられたことがわかります。そして、そのようなデータが使えると言われ、それを次のようにまとめています。

  1. 実際のところ、答えは次のようになります...
  2. 文脈を考慮すると、次のようになります...


これを回避するために、トピックの概念を導入します。トピックは、次のように明示的に定義され、プロンプトの先頭に挿入されます。

「トピック『グラフのサブグラフ開発』に関連する質問への回答が必要です: {{{サブグラフとは何ですか?}}}」


さらに、最後の文にはこうも付け加えます。

最後に、上記の情報では不十分な場合にのみ、トピック「グラフのサブグラフの開発」の知識を利用して質問に答えることができます。


最終的に、完全なプロンプト (チャットから取得した部分を除く) は次のようになります。

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


入力にこの半自動生成されたプロンプトを使用した上記のリクエストに対する応答は、最初から正しいように見えます。



この場合、ボットは正しいキーですぐに応答し、より関連性の高い情報を追加するため、答えは Q&A ほど単純ではありません (この質問は質問と回答のリストに正確に含まれていることを思い出してください) が、次の質問に部分的に対処する合理的な説明が付いています。

ソースコード

最後にリポジトリへのリンクがあるので、「トピック」を独自のものに、Q&A ナレッジ ベース ファイルを独自のものに置き換えて、OpenAI と Telegram ボット用の独自の API キーを指定して、ボットをそのまま実行できることにすぐに注意してください。したがって、ここでの説明は、GitHub のソース コードに完全に対応することを目的としたものではなく、コードの主な側面を強調することを目的としています。

1 - 仮想環境の準備

新しい仮想環境を作成し、requirements.txt から依存関係をインストールしましょう。


 virtualenv -p python3.8 .venv source .venv/bin/activate pip install -r requirements.txt

2 - ナレッジベース、手動で収集

上で述べたように、質問と回答のリストがあることを前提としています。この場合は、次のタイプの Excel ファイル形式です。



指定された質問に最も類似した質問を見つけるには、このファイルの各行に質問の埋め込み (状態空間の多次元ベクトル) を追加する必要があります。これにはadd_embeddings.pyファイルを使用します。スクリプトはいくつかの単純な部分で構成されています。

ライブラリのインポートとコマンドライン引数の読み取り:


 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


次に、ファイルを pandas データフレームに読み取り、疑問符の存在に基づいて質問をフィルタリングします。このコード スニペットは、ナレッジ ベースおよび Discord からの生のメッセージ ストリームの処理に一般的であるため、質問が重複することが多いと想定して、質問以外の大まかなフィルタリングのこのような単純な方法を維持することにしました。


 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('\?')]


最後に、モデルtext-embedding-ada-002の API を呼び出してエンベディングを生成する関数です。API は時折オーバーロードされる可能性があり、エラーで応答する可能性があるため、いくつかのリクエストを繰り返し、この関数をデータフレームの各行に適用します。


 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)


最終的に、このスクリプトは次のコマンドで呼び出すことができます。


 python add_embeddings.py \ --openai_api_key="xxx" \ --file="./subgraphs_faq.xlsx" \ --colname="Question"


OpenAI API キー、ナレッジ ベースを含むファイル、質問テキストが配置されている列の名前を設定します。最終的に作成されたファイルsubgraphs_faq._question_embed.csv には、 「Question」、「Answer」、および「ada_embedding 」列が含まれています。

3 - Discord からのデータ収集 (オプション)

手動で収集したナレッジ ベースのみに基づいて応答する単純なボットに興味がある場合は、このセクションと次のセクションをスキップしてください。ただし、ここでは、Discord チャネルと Telegram グループの両方からデータを収集するためのコード例を簡単に示します。ファイルdiscord-channel-data-collection.pyは 2 つの部分で構成されます。最初の部分には、ライブラリのインポートとコマンド ライン引数の初期化が含まれます。


 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


2 つ目は、チャネルからデータを取得してパンダ データフレームに保存する関数と、指定されたパラメーターを使用したその呼び出しです。


 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)


ここでの有益な情報のうち、必要なときに毎回見つけることができない詳細は、認証キーの取得です。 channel_id がブラウザーで開かれた Discord チャンネルの URL (リンクの最後の長い数字) から取得できることを考慮すると、 authorization_keyはチャンネルでメッセージの入力を開始し、開発者ツールを使用してネットワーク セクションで「入力」という名前のイベントを見つけ、ヘッダーからパラメーターを抽出することによってのみ見つけることができます。



これらのパラメーターを受け取った後、次のコマンドを実行して、チャネルからすべてのメッセージを収集できます (独自の値に置き換えます)。


 python discord-channel-data-collection.py \ --channel_id=123456 \ --authorization_key="123456qwerty"


4 - Telegram からのデータ収集

Telegram のチャット/チャネルからさまざまなデータをダウンロードすることが多いため、同様の形式 ( add_embeddings.pyスクリプトに関して互換性のある) CSV ファイルを生成するコードも提供することにしました。したがって、 telegram-group-data-collection.pyスクリプトは次のようになります。コマンドラインからライブラリをインポートし、引数を初期化します。


 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


ご覧のとおり、自分自身を最初の人物として認証しない限り、チャットからすべてのメッセージを単純にダウンロードすることはできません。つまり、https://my.telegram.org/apps を通じてアプリを作成する (APP_ID と APP_HASH を取得する) 以外に、電話番号とパスワードを使用して Telethon ライブラリから TelegramClient クラスのインスタンスを作成する必要もあります。


さらに、Telegram チャットのパブリック group_name が必要になり、取得する最新メッセージの数を明示的に指定します。全体として、1 つのアカウントから頻繁にメッセージを送信する場合とは異なり、Telegram API から一時的または永久的な禁止を受けることなく、エクスポートされたメッセージの数に関係なくこの手順を何度も実行しました。


スクリプトの 2 番目の部分には、メッセージをエクスポートするための実際の関数とその実行が含まれています (収集を途中で停止する重大なエラーを回避するために必要なフィルタリングが必要です)。


 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())


最終的に、このスクリプトは次のコマンドで実行できます (値を独自の値に置き換えます)。


 python telegram-group-data-collection.py \ --app_id=123456 --app_hash="123456qwerty" \ --phone_number="+xxxxxx" --password="qwerty123" \ --group_name="xxx" --limit_messages=10000


5 - 実際に質問に答える Telegram ボット スクリプト

ほとんどの場合、私は自分のお気に入りのプロジェクトを Telegram ボットにラップします。これは、起動に最小限の労力で済み、すぐに可能性が示されるためです。この場合、私も同じことをしました。ボット コードには、 SubgraphGPTボットの実稼働バージョンで使用する例外的なケースがすべて含まれていないと言わざるを得ません。これは、私の別のプロジェクトから継承したロジックがかなり多く含まれているためです。代わりに、ニーズに合わせて簡単に変更できる最小限の基本コードを残しました。

telegram-bot.pyスクリプトはいくつかの部分で構成されています。まず、前と同様に、ライブラリがインポートされ、コマンド ライン引数が初期化されます。


 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


この場合、OpenAI API キーも必要になることに注意してください。ナレッジ ベースからユーザーが入力した質問に最も類似した質問を見つけるには、ナレッジ ベース自体の場合と同様に、まず API を呼び出してその質問の埋め込みを取得する必要があります。


さらに、次のものが必要になります。


  • telegram_bot_token - BotFather からの Telegram ボットのトークン
  • file - ナレッジベースファイルへのパス (ここでは Discord からのメッセージのケースを意図的に省略します。これはニッチなタスクであると想定しているためです。ただし、必要に応じてコードに簡単に統合できます)
  • topic - ボットが動作するトピック (記事の冒頭で述べた) のテキスト形式
  • start_message - /start をクリックしたユーザーに表示されるメッセージ (デフォルトでは、「Hello, World!」)
  • モデル- モデルの選択 (デフォルトで設定)
  • num_top_qa - ChatGPT リクエストのコンテキストとして使用される、ナレッジ ベースからの最も類似した質問と回答の数


次に、ナレッジ ベース ファイルのロードと質問の埋め込みの初期化が続きます。


 # reading QA file with embeddings df_qa = pd.read_csv(file) df_qa['ada_embedding'] = df_qa.ada_embedding.apply(eval).apply(np.array)


ChatGPT API にリクエストを行うには、オーバーロードによりエラーが返されることがあるので、エラーが発生した場合にリクエストを自動的に再試行する関数を使用します。


 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)


OpenAI の推奨に従って、テキストを埋め込みに変換する前に、新しい行をスペースに置き換える必要があります。


 def get_embedding(text, model="text-embedding-ada-002"): text = text.replace("\n", " ") return openai.Embedding.create(input=[text], model=model)['data'][0]['embedding']


最も類似した質問を検索するために、openai ライブラリから直接取得した 2 つの質問の埋め込み間のコサイン距離を計算します。


 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


指定されたものに最も類似した質問と回答のペアのリストを受け取った後、それらを 1 つのテキストにコンパイルし、ChatGPT が何が何であるかを明確に判断できるようにマークすることができます。


 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


その後、記事の冒頭で説明したプロンプトの「部分」を 1 つの全体に集める必要があります。


 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


この場合、Discord のメッセージを使用する部分を削除しましたが、chat_prompt != None の場合でもロジックに従うことができます。


さらに、ChatGPT API から受信した応答を Telegram メッセージ (4096 文字以内) に分割する関数が必要になります。


 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]


ボットは、 /startコマンドによってトリガーされる 2 つの機能を割り当て、ユーザーからの個人メッセージを受信するという典型的な一連のステップから始まります。


 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()


/startに応答するコードは簡単です。


 def start(update, context): user = update.effective_user context.bot.send_message(chat_id=user.id, text=start_message)


そして、自由形式のメッセージに応答する場合、それは完全には明確ではありません。


まず、別のユーザーからのスレッドがブロックされるのを避けるために、スレッドライブラリを使用してスレッドを独立したプロセスにすぐに「分離」しましょう。


 def message_handler(update, context): thread = threading.Thread(target=long_running_task, args=(update, context)) thread.start()


次に、すべてのロジックはlong_running_task関数内で発生します。ボットのコードを変更するときにエラーを簡単に特定できるように、主要なフラグメントを意図的に try/ excel でラップしました。


  • まず、メッセージを取得し、ユーザーがメッセージの代わりにファイルまたは画像を送信した場合のエラーを処理します。
  • 次に、 search_similarを使用して最も類似した質問と回答を検索します。
  • その後、 collect_text_qaを使用してすべての質問と回答を 1 つのテキストに収集します。
  • そして、 collect_full_promptを使用して ChatGPT API の最終プロンプトを生成します。


 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


ナレッジ ベースとトピックを独自のものに置き換える場合、たとえば書式設定などが原因でエラーが発生する可能性があるため、人間が判読できるエラーが表示されます。


次に、リクエストは、すでに証明されている主要なシステム メッセージ「あなたは役に立つアシスタントです。 」とともに ChatGPT API に送信されます。結果の出力は、必要に応じて複数のメッセージに分割され、ユーザーに送り返されます。


 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)



これでコードの部分は終わりです。

プロトタイプ

現在、このようなボットのプロトタイプは、次のリンクから限定的な形式で入手できます。 API は有料なので、1 日あたり最大 3 つのリクエストを行うことができますが、誰にも制限されるものではないと思います。最も興味深いのは、狭いトピックに焦点を当てた特殊なボットではなく、AnythingGPT プロジェクトのコードです。このプロジェクトは、この例に基づいたナレッジ ベースで特定のタスクを解決するための独自のボットを作成する方法に関する短い説明とともにGitHubで入手できます。最後まで読んでいただければ、この記事がお役に立てば幸いです。



ボットとの毎日のコミュニケーションのスクリーンショット