apiの設定画面はこちらを参考にしました
Last active
October 6, 2024 03:30
-
-
Save okumurakengo/bfd57b33e36a3410a181ba9e52a7a060 to your computer and use it in GitHub Desktop.
適当にslackのChatGPTボットのapi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
$secretFile = __DIR__ . '/secret.json'; | |
$secrets = json_decode(file_get_contents($secretFile), true); | |
if ($secrets === null) { | |
die('json の読み込みに失敗しました'); | |
} | |
// SlackのトークンとボットのユーザーID | |
define('SLACK_TOKEN', $secrets['slackToken'] ?? ''); | |
define('BOT_USER_ID', $secrets['botUserId'] ?? ''); | |
define('TARGET_CHANNEL_ID', $secrets['targetChannelId'] ?? ''); | |
// ChatGPTのトークン | |
define('OPEN_AI_API_KEY', $secrets['openAiApiKey'] ?? ''); | |
date_default_timezone_set('Asia/Tokyo'); | |
$json_string = file_get_contents("php://input"); | |
$payload = json_decode($json_string, true); | |
$eventId = $payload['event_id'] ?? null; | |
$event = $payload['event'] ?? null; | |
try { | |
$pdo = new PDO('sqlite:db.sqlite'); | |
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); | |
createTableIfExists($pdo); | |
deleteOldData($pdo); | |
// アクセス数チェック用 | |
$cnt = incrementAccsessCount($pdo); | |
echo 'アクセスされた回数 : ' . $cnt . PHP_EOL; | |
if ($eventId !== null) { | |
// slack再送信の対策 | |
// slackが失敗したと判定して同じコメントで複数回アクセスすることがあるので、その場合は処理終了させる | |
$is_reaccess = isReAccess($pdo, $eventId); | |
if ($is_reaccess === true) { | |
exit; | |
} | |
// 再送信判定用にeventIdを保存 | |
saveEventId($pdo, $eventId); | |
} | |
// 保存されているeventId確認用 | |
foreach (getEventList($pdo) as $eventData) { | |
echo 'event_id : ' | |
. $eventData['event_id'] | |
. ' / target_date : ' | |
. $eventData['target_date'] | |
. ' / date_time : ' | |
. $eventData['date_time'] | |
. PHP_EOL; | |
} | |
if ($event === null) { | |
// slackからの送信ではないと思われるので終了 | |
exit; | |
} | |
if ($event['type'] !== 'message' || isset($event['text']) === false) { | |
// 投稿の文章が取得できないので終了 | |
exit; | |
} | |
if (strpos($event['text'], '<@' . BOT_USER_ID . '>') === false) { | |
// ボットへのメンションではないので終了 | |
exit; | |
} | |
// ボット自身のメッセージには反応しない | |
// この条件がないと slack bot の自身の投稿に反応して無限ループになる | |
if (isset($event['bot_id'])) { | |
exit; | |
} | |
if (isset($payload['event']['thread_ts'])) { | |
$threadTs = $payload['event']['thread_ts']; | |
} else { | |
// thread_tsがない場合、メッセージのタイムスタンプでスレッドIDを生成する | |
$threadTs = $payload['event']['ts']; | |
} | |
respondToMention("running ...", $event, $threadTs); | |
if ($event['channel'] !== TARGET_CHANNEL_ID) { | |
// 指定のチャンネルであった時のみ返信するので終了 | |
respondToMention('<#' . TARGET_CHANNEL_ID . '> でのみの返信に制限されています', $event, $threadTs); | |
exit; | |
} | |
$question = str_replace('<@' . BOT_USER_ID . '>', '', $event['text']); | |
$message = sendChatGpt($pdo, $question, $threadTs); | |
respondToMention($message, $event, $threadTs); | |
} catch (Throwable $t) { | |
var_dump($t); | |
} | |
function createTableIfExists(PDO $pdo): void { | |
// アクセス回数取得用 | |
$pdo->exec(<<<'EOL' | |
create table if not exists count_tbl ( | |
count integer not null primary key | |
); | |
EOL); | |
// slack 再送信判定用 | |
$pdo->exec(<<<'EOL' | |
create table if not exists event_ids ( | |
event_id text not null primary key, | |
target_date text not null, | |
date_time text not null | |
); | |
EOL); | |
// スレッド内容保存用 | |
$pdo->exec(<<<'EOL' | |
create table if not exists thread_tbl ( | |
thread_ts text not null, | |
role text not null, | |
question text not null, | |
timestamp_str text not null, | |
target_date text not null | |
); | |
EOL); | |
} | |
function deleteOldData(PDO $pdo): void { | |
$deleteStmt = $pdo->prepare("DELETE FROM event_ids WHERE target_date != ?"); | |
$deleteStmt->execute([(new DateTime())->format('Y-m-d')]); | |
$deleteStmt = $pdo->prepare("DELETE FROM thread_tbl WHERE target_date != ?"); | |
$deleteStmt->execute([(new DateTime())->format('Y-m-d')]); | |
} | |
function incrementAccsessCount(PDO $pdo): int { | |
$stmt = $pdo->prepare('select count from count_tbl limit 1;'); | |
$stmt->execute(); | |
$cnt = ($stmt->fetch()['count'] ?? 0); | |
$stmt = $pdo->prepare('delete from count_tbl;'); | |
$stmt->execute(); | |
$stmt = $pdo->prepare('insert into count_tbl values (?)'); | |
$stmt->execute([$cnt + 1]); | |
$stmt = $pdo->prepare('select count from count_tbl limit 1;'); | |
$stmt->execute(); | |
$cnt = (int)($stmt->fetch()['count'] ?? 0); | |
return $cnt; | |
} | |
function isReAccess(PDO $pdo, string $eventId): bool { | |
$stmt = $pdo->prepare('select count(1) as cnt from event_ids where event_id = ?;'); | |
$stmt->execute([$eventId]); | |
$event_cnt = (int)($stmt->fetch()['cnt'] ?? 0); | |
return $event_cnt > 0; | |
} | |
function saveEventId(PDO $pdo, string $eventId): void { | |
$stmt = $pdo->prepare('insert into event_ids values (?, ?, ?)'); | |
$stmt->execute([$eventId, (new DateTime())->format('Y-m-d'), (new DateTime())->format('Y-m-d H:i:s')]); | |
} | |
function getEventList(PDO $pdo): array { | |
$stmt = $pdo->prepare('select * from event_ids limit 10000;'); | |
$stmt->execute([]); | |
return $stmt->fetchAll(); | |
} | |
function saveThread(PDO $pdo, string $threadTs, string $role, string $question): void { | |
$stmt = $pdo->prepare('insert into thread_tbl values (?, ?, ?, ?, ?)'); | |
$stmt->execute([$threadTs, $role, $question, time(), (new DateTime())->format('Y-m-d')]); | |
} | |
function getThread(PDO $pdo, string $threadTs): array { | |
$stmt = $pdo->prepare('select * from thread_tbl where thread_ts = ? order by timestamp_str;'); | |
$stmt->execute([$threadTs]); | |
return $stmt->fetchAll(); | |
} | |
function sendChatGpt(PDO $pdo, string $question, string $threadTs): string { | |
//openAI APIエンドポイント | |
$endpoint = 'https://api.openai.com/v1/chat/completions'; | |
$headers = [ | |
'Content-Type: application/json', | |
'Authorization: Bearer ' . OPEN_AI_API_KEY, | |
]; | |
$thread_list = getThread($pdo, $threadTs); | |
$messageHistory = []; | |
foreach ($thread_list as $thread_data) { | |
$messageHistory[] = [ | |
"role" => $thread_data['role'], | |
"content" => $thread_data['question'], | |
]; | |
} | |
// 以前の会話履歴に新しい質問を追加 | |
$messageHistory[] = [ | |
"role" => "user", | |
"content" => $question, | |
]; | |
// リクエストのペイロード | |
$data = [ | |
'model' => 'gpt-3.5-turbo', | |
'messages' => $messageHistory, | |
]; | |
// cURLリクエストを初期化 | |
$ch = curl_init(); | |
// cURLオプションを設定 | |
curl_setopt($ch, CURLOPT_URL, $endpoint); | |
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
curl_setopt($ch, CURLOPT_POST, true); | |
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); | |
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); | |
// APIにリクエストを送信 | |
$response = curl_exec($ch); | |
// cURLリクエストを閉じる | |
curl_close($ch); | |
// 応答を解析 | |
$result = json_decode($response, true); | |
if (isset($result['error'])) { | |
$text = "エラーが発生しました\n```\n" . json_encode($result, JSON_PRETTY_PRINT) . "\n```"; | |
} else { | |
// 生成されたテキストを取得 | |
$text = $result['choices'][0]['message']['content'] ?? 'エラーが発生しました'; | |
saveThread($pdo, $threadTs, 'user', $question); | |
saveThread($pdo, $threadTs, 'assistant', $text); | |
} | |
return $text; | |
} | |
function respondToMention(string $message, array $event, string $threadTs): void { | |
// Slack APIを使ってメッセージを送信 | |
$channel = $event['channel']; | |
$url = "https://slack.com/api/chat.postMessage"; | |
$data = [ | |
'channel' => $channel, | |
'text' => $message, | |
'thread_ts' => $threadTs, // メンションされたメッセージのタイムスタンプ | |
]; | |
$options = [ | |
'http' => [ | |
'header' => [ | |
"Content-Type: application/json", | |
"Authorization: Bearer " . SLACK_TOKEN | |
], | |
'method' => 'POST', | |
'content' => json_encode($data), | |
], | |
]; | |
$context = stream_context_create($options); | |
$result = file_get_contents($url, false, $context); | |
if ($result === false) { | |
error_log('メッセージ送信失敗'); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"slackToken": "xxx-XXXXXX-XXXXX-XXXXX", | |
"botUserId": "UXXXXX", | |
"targetChannelId": "CXXXXX", | |
"openAiApiKey": "xx-xx-XXXXX-XXXXX" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment