Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

友達を作ろう

Last updated at Posted at 2024-12-06

はじめに

出オチです。

SLP Advent Calendar 2024 12月17日目です!
ゆっくり書いていたら書きすぎました。
今回はgeminiAPIとdiscord.pyを用いて、下のような会話できる友達を作ります!
全部読めばたぶん誰でも友達を作れるはずです。たぶん

スクリーンショット 2024-12-16 23.37.45.png
今回PythonとGitHubを使用しますが、そちらの環境構築は省いています。

実行環境

M1 Mac Sonoma
Python3.10.12

discordbotの作成

まず会話する友達に肉体を与えます!今回はDiscordBotを使用。
DiscordBotの作成方法ですが、同Advent Calendar12月11日の方で解説していらっしゃるので、そちらを見てください。基本的には同じ工程です。

参考程度に、以下のような友達ちゃんを作成しました!
与えた権限は「Administrator」です。
スクリーンショット 2024-11-29 23.18.53.png

Gemini

友達ちゃんの心を創っていきます。今回はgoogle様が提供する生成AI、geminiを使用します。geminiを使う理由は無料だからです!友情は見返りを求めません!
まずGoogle AI Studioにログインしてください。ここではgeminiとチャットしたり、設定ができます。
ログインして以下のような画面が出たら、Get API Keyに遷移します。

スクリーンショット 2024-11-30 0.45.13.png
「APIキーを作成」を押すとAPIが作成されます。青色の文字列をクリックでコピーできるのでメモしてください。

スクリーンショット 2024-11-30 0.47.41.png
これにて友達の心を作成できました。

Pythonで魂を作る

今回永続化のためにRender.comを使用するため、GitHubにリポジトリを作成する必要があります。適当なフォルダでサクッと作ってください。(私は"FriendDiscordBot"という名前で作成しました。)

ここでPythonのバージョンが3.10より低いとgeminiAPIが上手く機能しないので、Pythonのバージョンは3.10以上を使用してください。(私はPython3.10.12を使用しました。)

仮想環境の作成

venvで仮想環境に入ります。

terminal
$ pip install virtualenv
$ virtualenv -p python3 任意の名前
$ source 同じ名前/bin/activate

windowsの場合は最後の行を

powershell
同じ名前\Scripts\activate

にしてください。

必要なモジュールをインストール

今回使用するモジュールをインストールします。

terminal
$ pip install google-generativeai discord.py flask python-dotenv

それぞれのモジュールを簡単に説明します。
google-generativeai : geminiAIを操作
discord.py : discordBotを操作
flask : botを24時間維持するのに必要(詳しくは後述)
dotenv : 環境変数を操作

pythonを叩く

本題のコードを書いていきます。
ディレクトリは以下のように構成
仮想環境のフォルダはさっき作ったので、他の要素を作成してください。

FriendDiscordBot
|
├─さっきの仮想環境フォルダ
|
├─.env
|
├─requirements.txt
|
└─Myapp
    ├─main.py
    ├─gemini.py
    └─keep.py

実際に本文を書いていきましょう!

.env
TOKEN="メモしたBotトークン"
GOOGLE_API_KEY="メモしたgeminiAPIトークン"

環境変数を設定する.envファイルです。これでスクリプト内で環境変数を記述することによってトークンを使用できます。
記述する際、ダブルクォーテーションは必要ありません。

main.py
#必要なモジュールのインストール
import discord
import os
from keep import keep_alive
from dotenv import load_dotenv
from gemini import geminiCreateText

# .envファイルの読み込み
load_dotenv()

#botの初期設定
global isStartingUp 
isStartingUp = False
token = os.getenv("TOKEN")
intents = discord.Intents.default()
intents.message_content = True

client = discord.Client(intents=intents)

@client.event
async def on_ready():
    print(f'Logged in as {client.user}')

#メッセージを受け取った時に実行
@client.event
async def on_message(message):
    print(f'送信: {message.author}: {message.content}')
    global isStartingUp
    if message.author == client.user or message.author.bot:
        return
    elif message.author == client.user:
        return
    if str(client.user.id) in message.content:
        isStartingUp = not isStartingUp
        if isStartingUp:
            await message.channel.send("はーい")
        else:
            await message.channel.send("ばいばい")
    elif isStartingUp:
            await message.channel.send(geminiCreateText(message.content))

#flaskの起動
keep_alive()
try:
    client.run(token)
except:
    os.system("kill")

discordbotを起動します。メンションされた際、「はーい」と返信して会話ができるようになります。会話が始まると、送信文はgeminiに送られます。もう一度メンションすると、「ばいばい」と返信して会話が終わります。
また、flaskを起動し、専用のサイトを立ち上げます。

追記:後程メンションによる会話の始動は好ましくないことに気づきました。スラッシュコマンドの実装にて変更してます。

gemini.py
import os
from dotenv import load_dotenv
import google.generativeai as genai
 
load_dotenv()

#.envに書かれているAPI-KEYの設定
GOOGLE_API_KEY=os.getenv('GOOGLE_API_KEY')
genai.configure(api_key=GOOGLE_API_KEY)
#好きなgeminiモデルを設定
gemini_pro = genai.GenerativeModel("gemini-1.5-flash-002")

def geminiCreateText(inputText):
    prompt = f"以下の送信されたテキストに対して、小学生のような口調で200字以内で返信してください「{inputText}"
    response = gemini_pro.generate_content(prompt)
    print(response.text)
    return response.text

discordで受け取った内容を、promptとしてgeminiに渡します。その際のテキストは自由に変更してください。返信はmain.pyに返され、discordに送信されます。

keep.py
from flask import Flask
from threading import Thread
import os

app = Flask(__name__)

@app.route('/')
def home():
    return "Hello, Render!"

def run():
    port = int(os.getenv("PORT", 4000)) 
    app.run(host='0.0.0.0', port=port)

def keep_alive():
    t = Thread(target=run)
    t.start()

サイトを立ち上げます。今はこのサイトを使用しませんが、後程重要になります。

requirements.txt
Flask==3.1.0
python-dotenv==1.0.1
google-generativeai==0.8.3
discord.py==2.4.0

render.comで使用するrequirements.txtです。後程使います。
上記は私の場合のバージョンなので、それぞれの環境に合わせて数字を変更してください。
一部バージョンが古いと機能しません。(2敗)

実際に起動してみる

main.pyを実行すると、DiscordBotとFlaskが起動します。
ターミナルの最終行に以下のような出力があれば成功です。

terminal
Logged in as "botの名前"

実際に会話してみました
スクリーンショット 2024-12-01 18.49.36.png
無事会話できました!今度イカ釣りに行けるようです。
botを停止させる際は、ターミナルで control+Cを2回押してください
ここまでできたら、作成したコードをGitHubにプッシュしてください。

24時間受け付けさせる。

無事起動が確認できましたが、今では起動中しか会話できません。main.pyを止めると友達ちゃんに無視されてしまいます。無視されると悲しいのはAIでも変わりません。
そこでRender.comとGoogle App Scriptを使用していつでも会話できるようにしていきます。

Render.comでデプロイする

Render.comは、アプリケーションを簡単にデプロイしてくれるプラットフォームです。Herokuが有料になったので、無料のRenderを使用します。
一応replitでもできるとのことで試してみたのですが、どうやら最近できなくなったっぽい?調べても出てこなかったので1週間ぐらい詰んでました。

まずRender.comにアクセスすると、アカウントの登録を求められるので、サクッと登録してください。その際、GitHubアカウントも連携してください。
アカウントを作成すると以下のような画面が出るので「New Web Services」を選択してください。
スクリーンショット 2024-12-01 19.11.59.png
次にGitHubアカウントの連携を求められます。
コードを読み込む際に必要なので連携しましょう。
スクリーンショット 2024-12-01 19.13.55.png
ここから画像データを紛失したので文字のみで説明します
作成したリポジトリを選択します。
「Configure and deploy your new Web Service」という初期設定画面が出るので、以下のように変更

項目 備考
Source Code 作成したリポジトリ アカウント名/リポジトリ名
Name 自由
Language Python3
Region singapore 日本から一番近い場所
Build Command pip install -r requirements.txt
Start Command python3.10 Myapp/main.py ファイル名が異なる場合は変更
Instance Type Free 無料プランです

また、環境変数も設定します。「+ Add Enviroment Variable」を押して、以下のように作成してください

NAME_OF_VARIABLE value
TOKEN メモしたdiscordAPIのトークン
GOOGLE_API_KEY メモしたgeminiAPIのトークン
PYTHON_VERSION 3.10.12

ここまでできたら、最下部のDeploy Web Servisを押して、デプロイしましょう。
すると以下のような画面になると思います。
スクリーンショット 2024-12-01 19.58.57.png
ここではrequirements.txtを読み込んで必要なモジュールのインストールをしたり、リポジトリを読み込んで実行されます。実行は先ほど設定したStart Commandのpython3.10 Myapp/main.pyから始まります。
一分ほど待って以下のようなコンソールが表示されたら成功です。
スクリーンショット 2024-12-01 20.02.44.png

実際に友達ちゃんと会話してみましょう
スクリーンショット 2024-12-01 20.04.40.png
無事会話できました!本物のタコを見たことがないそうです

また、コードを変更する際は、コード変更→GitHubにプッシュ→Renderでデプロイという流れになります。ただrenderのデプロイはとにかく長いので、ローカル環境でテストしてからGitHubにプッシュする方が効率的です。
もしrenderでの実行を一時停止したい場合は、Settingの一番下にあるSuspend Web Serviceを押してください。

renderでの作業は以上になります。

GAS(Google App Script)

renderでの設定が終わりましたが、renderはサイトを閉じた場合、15分程度でデプロイを終了させてしまいます。
じゃあローカル環境での実行と変わらないじゃん!と思ってしまいますが、解決策があります。
実はrenderを用いてWebサイトを作成したとき、そのサイトへのアクセスがあればデプロイが終了するまでの時間が延長されるのです。
既にflaskを用いてサイトを開いているので、あとはサイトのアクセスをGoogle App Scriptで自動化します。

Google App ScriptはGoogle様が制作したワークスペースプラットフォームや、そのスクリプトのことです。主にGoogleのサービスを自動化させます。今回はGASを使って、定期的なHTTPポストを送りましょう。

上記のURLを開くと、Google APP Scriptのホームページに飛ばされます。早速ログインしてください。すると、以下のような画面になるので「+新しいプロジェクト」を選択
スクリーンショット 2024-12-01 21.02.27.png
スクリーンショット 2024-12-01 21.04.05.png
上のような画面に飛ばされるので、名前を適当に設定しスクリプトを編集しましょう。

コード.gs
const keepRender = () => {
  const renderURL = PropertiesService.getScriptProperties().getProperty('renderURL')
  const data = {}
  const headers = { 'Content-Type': 'application/json; charset=UTF-8' }
  const params = {
    method: 'post',
    payload: JSON.stringify(data),
    headers: headers,
    muteHttpExceptions: true
  }

  response = UrlFetchApp.fetch(renderURL, params);
  console.log(response)
}

上記は適当なHTTPポストを送るスクリプトです
次に、画面左部分にあるプロジェクトの設定に移動します。
最下部にスクリプトプロパティから、変数でサイトのURLを取得できるよううにします。さっき書いたコードの二行目のやつですね。

ちなみにサイトのURLなのですが、renderでは専用のポートが用意されているためIPアドレスのURLは使用できません。

2枚目の画像にて青枠で囲ってある場所に記載されているURLをコピーして、スクリプトプロパティに貼り付けましょう
スクリーンショット 2024-12-01 21.12.11.png

スクリーンショット 2024-12-01 21.15.30.png

最後にトリガーを設定します。左にある「トリガー」から「+ トリガーを追加」を押し、以下の画像のように設定します。
スクリーンショット 2024-12-01 21.21.35.png

これにて、5分おきにkeepRender関数を実行し、サイトにHTTPPOSTを送ります。
実際に「実行数」タブから状況を見てみると、5分おきに実行されていることがわかります。

これにて24時間いつでも友達ちゃんと会話できるようになりました!無視されることはありません!
Bot自体の実装は以上になります。

機能を追加する

ここからは個人的に追加したい機能や修正を追加していきます。

①過去の会話を参照

gemini.pyを以下のように変更してください

gemini.py
import os
from dotenv import load_dotenv
import google.generativeai as genai
 
load_dotenv()

GOOGLE_API_KEY=os.getenv('GOOGLE_API_KEY')
genai.configure(api_key=GOOGLE_API_KEY)
#cacheで、基本的なgeminiの設定を行う
cache = genai.caching.CachedContent.create(
    model="gemini-1.5-flash-002",
    system_instruction="あなたはdiscordChatBotです。小学生のような口調で返信してください"
)
gemini_pro = genai.GenerativeModel.from_cached_content(cache)

#会話履歴の変数を作成
conversation_history = []

def geminiCreateText(inputText):

    #geminiが扱う型で会話履歴を追加
    conversation_history.append({'role': 'user', 'parts': [inputText]})
    response = gemini_pro.generate_content(conversation_history)
    conversation_history.append(response.candidates[0].content)
    print(conversation_history)
    if len(conversation_history) > 10:
        conversation_history.pop(0)
    return response.text

#会話履歴をクリア
def deletechche():
    conversation_history.clear()

また、main.pyのon_messageを以下のようにしてください

main.py
async def on_message(message):
    global isStartingUp
    # メッセージがテキストでない場合は無視
    if not message.content:
        return
    
    if message.author == client.user or message.author.bot:
        return  # ボット自身および他のボットのメッセージには反応しない

    if str(client.user.id) in message.content:
        isStartingUp = not isStartingUp
        if isStartingUp:
            await message.channel.send("はーい")
        else:
            #会話をやめる時に会話履歴をクリア
            deletechche()
            await message.channel.send("ばいばい")
    elif isStartingUp:
        response = geminiCreateText(message.content)
        await message.channel.send(response)

cacheを使うことで、友達ちゃんの性格を決めます。また、会話履歴はContentとしてconversation_historyに追加されていきます。会話を終了する際、deletechche()が呼び出されconversation_historyのデータがクリアされます。
ついでにエラー防止のためif not message.content:でメッセージ以外の要素を受け取った際処理しないようにしました。

スクリーンショット 2024-12-02 10.02.27.png
実際に会話してみると、過去の会話が参照されていますね!
これで思い出を作れますね!

相手を名指しするようにする

実際にチャットしてみると、以下のような返信が返されることがあります。流石に寂しいので認知してもらいましょう。
スクリーンショット 2024-12-03 7.39.09.png

gemini.py
import os
from dotenv import load_dotenv
import google.generativeai as genai
 
# .envファイルの読み込み
load_dotenv()

# API-KEYの設定
GOOGLE_API_KEY=os.getenv('GOOGLE_API_KEY')
genai.configure(api_key=GOOGLE_API_KEY)

conversation_history = []
#usernameを引数で受け取る
def geminiCreateText(inputText,username):
    
    ##cacheを会話する度に作成する
    cache = genai.caching.CachedContent.create(
    model="gemini-1.5-flash-002",
    #会話相手の名前も同時に記述
    system_instruction=f"あなたはdiscordChatBotです。元気な口調で返信してください。語尾は「〜なのだ。〜のだ。」でお願いします。会話相手の名前は{username}です"
    )
    gemini_pro = genai.GenerativeModel.from_cached_content(cache)


    conversation_history.append({'role': 'user', 'parts': [inputText]})
    response = gemini_pro.generate_content(conversation_history)
    conversation_history.append(response.candidates[0].content)
    print(conversation_history)
    if len(conversation_history) > 10:
        conversation_history.pop(0)
    return response.text

def deletechche():
    conversation_history.clear()

また、main.pyのresponse = ~の箇所を以下のように変更してください

main.py
response = geminiCreateText(message.content,message.author.global_name)

スクリーンショット 2024-12-03 7.58.38.png
認知されましたね!これで承認欲求が満たされます!

スラッシュコマンドの実装

この友達ちゃんを二つのサーバーに入れたところ、片方で会話を開始した場合、もう片方でも会話が開始されることがわかりました。
メンションによる操作は諦めて、スラッシュコマンドを用いた操作を実装します。

まずは実装と初期化から

main.py
from discord import app_commands


tree = app_commands.CommandTree(client)  # スラッシュコマンドを管理するtree

#~~~省略~~~

#ログイン時にスラッシュコマンドを同期
@client.event
async def on_ready():
    print(f'Logged in as {client.user}')
    await tree.sync() 

#「f!~」と送信されても反応するようにする
@client.event
async def on_message(message):
    if not message.content:
        return
    if message.author == client.user or message.author.bot:
        return  
    #「f!」が含まれていたら、f!を除いてgeminiに渡す
    if message.content.startswith('f!'):
        inputText = message.content.strip('f')
        inputText = inputText.strip('!')
        inputText = inputText.strip(' ')
        user = message.author.display_name
        response = geminiCreateText(inputText,user)
        await message.channel.send(response)


#スラッシュコマンドでも会話できるように
@tree.command(name='fchat', description='会話するよ!(f!~でもできるよ)') 
async def chatCommand(interaction: discord.Interaction,text:str): 
  await interaction.response.defer()
  user = interaction.user.display_name
  response = geminiCreateText(text,user)
  await interaction.followup.send(response)

#思い出リセットコマンド
@tree.command(name='freset', description='思い出を消すよ!') 
async def resetMemoryCommand(interaction: discord.Interaction): 
  deletechche()
  await interaction.response.send_message('思い出を消したよ!')

#人物像変更コマンド
@tree.command(name='fcharacter', description='人物像を変えるよ!')
async def changeCharacterCommand(interaction: discord.Interaction,text:str): 
    resp = changeCharacter(text)
    deletechche()
    await interaction.response.send_message(resp)

#性格変更コマンド
@tree.command(name='fpersonality', description='細かい性格を変えるよ!')
async def changePersonalityCommand(interaction: discord.Interaction,text:str): 
    resp = changepersonality(text)
    deletechche()
    await interaction.response.send_message(resp)

#送信文字数変更コマンド
@tree.command(name='flimitword', description='文字数を変えるよ!')
async def changeLimitWordCommand(interaction: discord.Interaction,text:int): 
    resp = changeWordLimit(text)
    await interaction.response.send_message(f"文字数の限界を{resp}にしたよ!")

keep_alive()

try:
    client.run(token)
except:
    os.system("kill")
gemini.py
#~~~省略~~~

genai.configure(api_key=GOOGLE_API_KEY)

#geminiの設定を文字変数として保存
conversation_history = []
character = '限界大学生のような口調で'
personality = '語尾は「〜のだ」「〜なのだ」'
limitWordNum = ''
def geminiCreateText(inputText,username):

    cache = genai.caching.CachedContent.create(
    model="gemini-1.5-flash-002",
    system_instruction=f"あなたはdiscordChatBotです。{character}{limitWordNum}返信してください。会話相手の名前は「{username}」です。{personality}"
    )
    gemini_pro = genai.GenerativeModel.from_cached_content(cache)


    conversation_history.append({'role': 'user', 'parts': [inputText]})
    response = gemini_pro.generate_content(conversation_history)
    conversation_history.append(response.candidates[0].content)
    print(conversation_history)
    if len(conversation_history) > 10:
        conversation_history.pop(0)
    return response.text

def deletechche():
    conversation_history.clear()

#人物像を変更。'null','なし'だった場合は人物像を消す
def changeCharacter(inputText):
    global character
    if inputText in ['null', '無し']:
        character = ''
        return '人物像を消したよ!'
    else:
        character = f'{inputText}のような口調で'
        return f'人物像を{inputText}に変更したよ!'

#性格を変更。'null','なし'だった場合は性格を消す
def changepersonality(inputText):
    global personality
    inputText = inputText.strip(' ')
    if inputText in ['null', '無し']:
        personality = ''
        return '性格を消したよ!'
    else: 
        personality = f'細かいあなたの特徴は「{inputText}'
        return f'性格を{inputText}に変更したよ!'

#限界文字数を変更。数字が0以下なら無制限にする。
def changeWordLimit(inputNum):
    global limitWordNum
    if inputNum > 0:
        limitWordNum = f'{inputNum}字以下で'
        return inputNum
    else :
        limitWordNum = ''
        return '無制限'

スラッシュコマンドは、discordで使用できるコマンドです。チャット欄に「/」を打つと画像のようなコマンドが出てくるので、選ぶかコマンドを記述して実行できます。ただ、スラッシュコマンドでは送信者の文章が隠れて表示されるため、「f!~」でも会話を可能にしました。
スクリーンショット 2024-12-09 15.02.39.png

実際にコマンドを打ってみます。
スクリーンショット 2024-12-09 15.16.37.png
無事友達の性格を変えることができましたね!
これで友達ちゃんをマインドコントロールできます!

おわりに

無事友達ができましたね!
discord.pyもgeminiAPIも初めて使用したので、ゆっくり悩んでいたらこんなに書いていました。
次は「現実の友達を作ろう」でお会いしましょう
ご拝読頂きありがとうございました!

勉強させていただいたサイト

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?