Skip to content

Instantly share code, notes, and snippets.

@okumurakengo
Last active October 6, 2024 03:30
Show Gist options
  • Save okumurakengo/bfd57b33e36a3410a181ba9e52a7a060 to your computer and use it in GitHub Desktop.
Save okumurakengo/bfd57b33e36a3410a181ba9e52a7a060 to your computer and use it in GitHub Desktop.
適当にslackのChatGPTボットのapi
<?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('メッセージ送信失敗');
}
}
{
"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