Azure FunctionsとChatGPT APIで作ったSlack Botをコンテキスト対応しました
ChatGPT すごい
株式会社ジェイテックジャパン CTOの高丘 @tomohisaです。3/1からChatGPT APIが使用可能になり、翌日の3/2に以下の記事に記したChatGPT Botを作成しました。
この記事では先週作成した簡易のChatGptのボットを1週間でどのように改善したかに関して説明しています。
便利に使われている
会社として、ChatGPTのAPIは学習には使われないものの、ログは残るため、個人情報を含む情報はChatGPTには送らないようにしています。そのため、ベータ版の出ているOpenAIによるオフィシャルアプリのような、要約のためではなく、アイデアのまとめやコーディングのヘルプに使用しています。プログラミング言語の挙動や、今まであまり使っていなかった機能の使い方など、多くの方法で使用されています。これまであまり使われていなかった社内の #helpmeチャンネルも、ChatGPTに質問して、さらに社内のメンバーからも返信があったりして、コミュニケーションが活性化されています。
やはりコンテキスト機能があると便利そう
初期は単純に質問に答えるだけのBotにしていたのですが、やはり質問の答えを要約したり、補足の情報を聞いたりするのに、スレッド内の過去の質問と答えも含めて質問をできる方が便利ですね。それでコンテキストもSlackのスレッドから読み込み、Botとの会話、またChatGPTの返信を含めた会話としてChatGPTに質問するように機能追加しました。
仕様としては以下のとおりです。
- Slackにアプリとして追加可能
- 各チャンネルに追加して、 @chatgptbotを付けて質問を送ったら、スレッドを作って返信する
- OpenAIのChatGPT API "gpt-3.5-turbo"を使用する
- スレッド内でさらに質問したらそのスレッド内に返信する
- 複数の質問でコンテキストを理解して返答する
- コンテキストは @chatgptbotを含んだSlackのメッセージ、また @chatgptbotが送信したメッセージとする
- @chatgptbotを含まないSlackのメッセージがあっても、OpenAIには送信しない
- コンテキストの件数はFunctionsの設定で指定可能とする(弊社では20件)
コンテキスト対応版の実装
基本的に以下の記事と同様の方法で設定をしていきます。すでに行なっている場合は新たに設定は不要です。
Azure Functionsの関数のコード
- Azure Funcitonsの「関数」-「chatGPTresponse(対象の関数名)」-「コードとテスト」内のソースコードを以下のように書き換えます。
- 設定は後述の「設定」-「構成」から設定しますので、コード内での設定は不要です。
const { WebClient } = require('@slack/web-api');
const { ChatCompletionRequestMessageRoleEnum, Configuration, OpenAIApi } = require("openai");
const openaiClient = new OpenAIApi(new Configuration({ apiKey: process.env.OPENAI_API_KEY }));
const slackClient = new WebClient(process.env.SLACK_BOT_TOKEN);
const GPT_BOT_USER_ID = process.env.GPT_BOT_USER_ID;
const CHAT_GPT_SYSTEM_PROMPT = process.env.CHAT_GPT_SYSTEM_PROMPT;
const GPT_THREAD_MAX_COUNT = process.env.GPT_THREAD_MAX_COUNT;
module.exports = async function (context, req) {
const postMessage = async (channel, text, threadTs) => {
await slackClient.chat.postMessage({
channel: channel,
text: text,
thread_ts: threadTs
});
context.log(text)
};
const createCompletion = async (messages) => {
try {
const response = await openaiClient.createChatCompletion({
model: "gpt-3.5-turbo",
messages: messages,
top_p: 0.5,
frequency_penalty: 0.5,
});
return response.data.choices[0].message.content;
} catch(err) {
context.log.error(err);
return err.response.statusText;
}
};
// Ignore retry requests
if (req.headers['x-slack-retry-num']) {
context.log('Ignoring Retry request...');
return { statusCode: 200, body: JSON.stringify({ message: "No need to resend" }) };
}
const body = eval(req.body);
if (body.challenge) {
context.res = {
body: body.challenge
};
return;
}
context.log(req.body);
const event = body.event;
const threadTs = event.thread_ts || event.ts;
if (event.type === 'app_mention') {
try {
const threadMessagesResponse = await slackClient.conversations.replies({
channel: event.channel,
ts: threadTs,
});
if (threadMessagesResponse.ok !== true) {
await postMessage(event.channel,'[Bot]メッセージの取得に失敗しました。',threadTs);
return;
}
const botMessages = threadMessagesResponse.messages.sort(
(a, b) => Number(a.ts) - Number(b.ts)
).filter(message => message.text.includes(GPT_BOT_USER_ID) || message.user == GPT_BOT_USER_ID).slice(GPT_THREAD_MAX_COUNT * -1).map(m => {
const role = m.bot_id ? ChatCompletionRequestMessageRoleEnum.Assistant : ChatCompletionRequestMessageRoleEnum.User
return { role: role, content: m.text.replace(/<@[^>]+>/g, '') }
});
if (botMessages.length < 1) {
await postMessage(event.channel,'[Bot]質問メッセージが見つかりませんでした。@chatgptbot を付けて質問してみて下さい。',threadTs);
return;
}
context.log(botMessages);
var postMessages = [
{ role: ChatCompletionRequestMessageRoleEnum.System, content: CHAT_GPT_SYSTEM_PROMPT },
...botMessages
];
const openaiResponse = await createCompletion(postMessages);
if (openaiResponse == null || openaiResponse == '') {
await postMessage(context, event.channel,'[Bot]ChatGPTから返信がありませんでした。この症状は、ChatGPTのサーバーの調子が悪い時に起こります。少し待って再度試してみて下さい。',threadTs);
return { statusCode: 200 };
}
await postMessage(event.channel, openaiResponse, threadTs);
context.log('ChatGPTBot function post message successfully.');
return { statusCode: 200 };
} catch (error) {
context.log(await postMessage(event.channel, `Error happened: ${error}`, threadTs));
}
}
context.res = {
status: 200
};
};
こちらの実装を作成するのに、以下の記事を参考にさせていただきました。ありがとうございます。
Azure Functionの設定を行う
以下の設定をAzure Functionsの「設定」-「構成」-「アプリケーション設定」から行います。
- CHAT_GPT_SYSTEM_PROMPT : 最初にChatGPTへ送るシステムメッセージ、Chatの設定のようなものです。私は以下のようにしています。
You are an excellent AI assistant Slack Bot.
Please output your response message according to following format.
- bold: "*bold*"
- italic: "_italic_"
- strikethrough: "~strikethrough~"
- code: " \`code\` "
- link: "<https://slack.com|link text>"
- block: "\`\`\` code block \`\`\`"
- bulleted list: "* item1" Be sure to include a space before and after the single quote in the sentence.
ex) word\`code\`word -> word \`code\` word
And Answer in language user uses.
Let's begin.
-
GPT_BOT_USER_ID : Slack AppのユーザーID。@などは付けません。私たちの環境では
U04RJRK****
のような文字列となっています。 - OPENAI_API_KEY : OpenAIのAPIキー
上記のページから[Create new Secret Key]をして、コピーする。
Azureの「設定」ー「構成」ー「アプリケーション設定」ー「新しいアプリケーション設定」新規の設定を追加する。
名称:OPENAI_API_KEY
値:上記でコピーしたOpenAI Secret Key
を設定する。
-
GPT_THREAD_MAX_COUNT : スレッドに含むメッセージの最大数。私たちの環境では
20
を入れています。 -
SLACK_BOT_TOKEN : Slack Botのトークンは
xoxb-**********-*******************************
の形式でSlackのサイトから取得可能です。
設定は以上です。
既知の問題点
-
@メッセージをボットに送っても、何もメッセージが返ってこないことがありました。こちらのバージョンではOpenAIからのレスポンスがエラーの場合にエラーメッセージを返すようにしています。その状況でもSlackのBotがメッセージを返さない場合があり、引き続き調査していきます。
-
非常に長い英文の翻訳を1スレッド内で連続で行っていると、ある地点から翻訳をしなくなり、400 Bad Requestとなりました。入っている文字が悪いのか、それとも長さが悪いのかは調査中です。
まとめ
ChatGPTだけでなく、画像生成、画像の読み取り、音声からのテキスト変換など、多くのAIが用意されています。私たちはAIモデルの専門家ではありませんが、アプリケーションを作成するのを得意にしています。私たち自身の問題解決に加えて、顧客の問題解決にもこれからAIを大いに使用できるようになってきました。引き続きAIを最大限に活用していく方法を色々模索していきます。
Discussion