ChatGPTのAPIを使ってしりとり作ってみた

研究室の先生からChatGPTのAPIをもらったので、試しにChatGPTとしりとりができるコードを作ってみた。
## ChatGPTの現状
![IMG_2570.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3474661/a3303595-5c52-23e1-b3ed-89e4a77041e3.jpeg)


・・・全然ダメやん！
こんな感じで、しりとりのルールを教えても全然うまくいかないのです。
そんなChatGPTのAPIを使って、きちんとしりとりができるようなものを作ってみた、というものです。
## 実行結果
とりあえず最終的にはこんなものができましたよ、というものです。
・名詞以外が来たらダメです
![結果_名詞.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3474661/9e16eea9-9ef6-18b4-0e28-82c29eb7c49f.png)
・一度ゲームに出てきた単語もダメです
![結果2_既出.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3474661/9d945808-07d4-4837-d160-0375a26796ba.png)
・最後が「ん」ももちろんダメです
![結果3_「ん」.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3474661/72f03bf0-4a5f-8da5-f5aa-81983e175c05.png)
・「ヨーヨー」のように、最後に「ー」が付いてても対応しています。
また、例えば「最初」→「予防接種」というように、最後が「ょ」の場合は「よ」に変換するようにしています。
![結果1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3474661/b87edecb-8619-539c-f5ea-1e0b2ee2f249.png)

## 使ったライブラリ
上から、Juman++という形態素解析を行うライブラリ、ChatGPTのAPIを使うためにopenAIのインストール、jaconvというひらがなとカタカタの相互変換を行ってくれるやつです。
```
from pyknp import Juman
import openai
import jaconv
```
## 方針
①ユーザーが言葉を入力します。

②その言葉がしりとりのルールを満たしているかどうかをチェック→満たしていなければ負けと出力しゲーム終了。満たしていればChatGPTの回答に移る。

③ ChatGPTへのプロンプトとしては、「ユーザーが入力した言葉の最後の文字から始まる言葉を一つ言ってください」だけ。重要なのはこの後で、これだけではChatGPTは平気で全然関係ない言葉を言うので、しりとりのルールを満たす言葉を言うまでwhile文を回して聞き続ける。(だからChatGPTが負ける可能性がない)

④条件を満たす言葉を言ったら、それをChatGPTの回答とする

⑤以上の操作を繰り返す

と言った感じです。しりとりのルールを満たしているかどうかの判定が作る上で一番めんどかった。(名詞ではない、最後の文字からは始まってない、最後が「ん」で終わらない、同じ言葉を2回言う...など)
## コードについて
### クラスとインスタンス変数
word_listは、ゲームの実行中に出てきた言葉を管理するリスト(重複なし)です。これから書く関数は全てShiritoriGameのクラス下にあります。
```python3
class ShiritoriGame:
    def __init__(self):
        self.N = 100
        self.word_list = set()
        self.juman = Juman()
        self.last_word = ""
        self.openai_organization = "org-xxx"
        self.openai_api_key = "sk-xxx"
```
### APIの利用
これでChatGPTのAPIを使ってChatGPTを呼び出しています。
```python3
   def ask_ChatGPT(self, message):
        openai.organization = self.openai_organization
        openai.api_key = self.openai_api_key
        completion = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[
                {
                    "role": "user",
                    "content": message,
                }
            ],
            max_tokens=1024,
            n=1,
            stop=None,
            temperature=0.5,
        )
        response = completion.choices[0].message.content
        return response

```
### しりとりの条件を満たしているかを判定する
最後の文字が「ん」でないかを判別
```python3
 def check_last(self, word):
        if word == "":
            return False
        result = self.juman.analysis(word)
        mrph_list = result.mrph_list()
        mrph = mrph_list[-1]
        s=jaconv.kata2hira(mrph.yomi)
        if s[-1] == "ん":
            return False
        return True
```
名詞かどうかを判別

```python3
    def check_hinsi(self, word):
        result = self.juman.analysis(word)
        mrph_list = result.mrph_list()
        if not mrph_list:
            return False
        mrph = mrph_list[-1]
        if mrph.hinsi == "名詞":
            return True
        return False
```
小さいひらがなかカタカナ(「ぁ」や「ョ」など)であることを判別
```python3
   def check_small(self,character):
        small_list=["ぁ","ぃ","ぅ","ぇ","ぉ","ゃ","ゅ","ょ","ァ","ィ","ゥ","ェ","ォ","ャ","ュ","ョ"]
        if character in small_list:
            return True
        return False
```
小さいひらがな、カタカナを、大きいひらがな、カタカナに直す
```python3
    def change_small(self,s):
        if s=="ぁ" or s=="ァ":
            return "あ"
        elif s=="ぃ" or s=="ィ":
            return "い"
        elif s=="ぅ" or s=="ゥ":
            return "う"
        elif s=="ぇ" or s=="ェ":
            return "え"
        elif s=="ぉ" or s=="ォ":
            return "お"
        elif s=="ゃ" or s=="ャ":
            return "や"
        elif s=="ゅ" or s=="ュ":
            return "ゆ"
        elif s=="ょ" or s=="ョ":
            return "よ"
        return s
```
単語の最後の文字を返します。次のターンではこの文字から始めないと負けになります。
```python3
   def get_lastletter(self, current_word):
        result = self.juman.analysis(current_word)
        mrph_list = result.mrph_list()
        mrph = mrph_list[-1]
        if mrph.yomi[-1]=="ー":
            return mrph.yomi[-2:-1]
        return mrph.yomi[-1]
```
これらの関数は、以下の「最後の文字からスタートしているか」(りんご→コアラはNG)のチェックの際の補足として使っています。
具体的には、例えば
「シーシャ」だと「ャ」では始まれないので「や」からスタートする言葉を探しています。
また、「ヨーヨー」といった、最後に伸ばし棒がある単語はその一個前の「よ」からスタートするようにしています。


```python3
  def check_initial(self, last_word, current_word):
        if last_word == "":
            return True
        if current_word == "":
            return False
        result1 = self.juman.analysis(current_word)
        mrph_list1 = result1.mrph_list()
        mrph1 = mrph_list1[0]
        result2 = self.juman.analysis(last_word)
        mrph_list2 = result2.mrph_list()
        mrph2 = mrph_list2[-1]
        m1_new = jaconv.kata2hira(mrph1.yomi)
        m2_new = jaconv.kata2hira(mrph2.yomi)
        if m2_new[-1]=="ー":
            last=m2_new[-2:-1]
            if self.check_small(last):
                last_new=self.change_small(last)
                if m1_new[0] != last_new:
                    return False
                return True
            if m1_new[0] != last:
                return False
            else:
                return True
        if self.check_small(m2_new[-1])==False:
            if m1_new[0] != m2_new[-1]:
                return False
            return True
        if self.check_small(m2_new[-1]):
            yomi_new=self.change_small(m2_new[-1])
            if m1_new[0] != yomi_new:
                return False
            return True


```
上記の判定関数を組み合わせて、しりとりのルールをその単語が満たしているかどうかの結果を返します。満たしていなかったらその理由を出力するようにしています。
```python3
    def win_or_lose(self, last_word, current_word):
        if (
            current_word not in self.word_list
            and self.check_initial(last_word, current_word)
            and self.check_last(current_word)
            and self.check_hinsi(current_word)
        ):
            return True
        else:
            if current_word in self.word_list:
                print("既出です")
            if self.check_initial(last_word, current_word) == False:
                print("最後の文字から始まっていません")
            if self.check_last(current_word) == False:
                print("「ん」で終わっています")
            if self.check_hinsi(current_word) == False:
                print("名詞ではありません")
            return False
```
あとは、ゲームを始めるだけです。
```python3
def play(self):
    n = 0
    current_word = ""
    self.last_word = ""
    last_letter = None
    user = input("名前を教えてください: ")
    while n < self.N:
        word = input("単語を入力してください: ")
        current_word = word
        if self.win_or_lose(self.last_word, current_word) == False:
            print(f"{user}の負けです")
            exit()
        self.word_list.add(current_word)
        last_letter = self.get_lastletter(current_word)
        if self.check_small(last_letter):
            last_letter = self.change_small(last_letter)
        self.last_word = current_word
        current_word = ""
        while self.win_or_lose_for_chatgpt(self.last_word, current_word) == False:
            print(f"「{last_letter}」から始まる単語を探しています...")
            command = f"{last_letter}から始まる名詞を一つ言ってください"
            current_word = self.ask_ChatGPT(command)
            print("ChatGPT:", current_word)
            if self.win_or_lose(self.last_word, current_word) == False:
                print("Chatgptの負けです")
                exit()
            self.last_word = current_word
            self.word_list.add(current_word)
            n += 1

```
最後に、コード全体を置いておきます。
```python3
from pyknp import Juman
import openai
import jaconv

class ShiritoriGame:
    def __init__(self):
        self.N = 100
        self.word_list = set()
        self.juman = Juman()
        self.last_word = ""
        self.openai_organization = "org-xxx"
        self.openai_api_key = "sk-xxx"

    def ask_ChatGPT(self, message):
        openai.organization = self.openai_organization
        openai.api_key = self.openai_api_key
        completion = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[
                {
                    "role": "user",
                    "content": message,
                }
            ],
            max_tokens=1024,
            n=1,
            stop=None,
            temperature=0.5,
        )
        response = completion.choices[0].message.content
        return response

    def check_last(self, word):
        if word == "":
            return False
        result = self.juman.analysis(word)
        mrph_list = result.mrph_list()
        mrph = mrph_list[-1]
        s=jaconv.kata2hira(mrph.yomi)
        if s[-1] == "ん":
            return False
        return True

    def check_hinsi(self, word):
        result = self.juman.analysis(word)
        mrph_list = result.mrph_list()
        if not mrph_list:
            return False
        mrph = mrph_list[-1]
        if mrph.hinsi == "名詞":
            return True
        return False
    
    def check_small(self,character):
        small_list=["ぁ","ぃ","ぅ","ぇ","ぉ","ゃ","ゅ","ょ","ァ","ィ","ゥ","ェ","ォ","ャ","ュ","ョ"]
        if character in small_list:
            return True
        return False
    
    def change_small(self,s):
        if s=="ぁ" or s=="ァ":
            return "あ"
        elif s=="ぃ" or s=="ィ":
            return "い"
        elif s=="ぅ" or s=="ゥ":
            return "う"
        elif s=="ぇ" or s=="ェ":
            return "え"
        elif s=="ぉ" or s=="ォ":
            return "お"
        elif s=="ゃ" or s=="ャ":
            return "や"
        elif s=="ゅ" or s=="ュ":
            return "ゆ"
        elif s=="ょ" or s=="ョ":
            return "よ"
        return s

    def check_initial(self, last_word, current_word):
        if last_word == "":
            return True
        if current_word == "":
            return False
        result1 = self.juman.analysis(current_word)
        mrph_list1 = result1.mrph_list()
        mrph1 = mrph_list1[0]
        result2 = self.juman.analysis(last_word)
        mrph_list2 = result2.mrph_list()
        mrph2 = mrph_list2[-1]
        m1_new = jaconv.kata2hira(mrph1.yomi)
        m2_new = jaconv.kata2hira(mrph2.yomi)
        if m2_new[-1]=="ー":
            last=m2_new[-2:-1]
            if self.check_small(last):
                last_new=self.change_small(last)
                if m1_new[0] != last_new:
                    return False
                return True
            if m1_new[0] != last:
                return False
            else:
                return True
        if self.check_small(m2_new[-1])==False:
            if m1_new[0] != m2_new[-1]:
                return False
            return True
        if self.check_small(m2_new[-1]):
            yomi_new=self.change_small(m2_new[-1])
            if m1_new[0] != yomi_new:
                return False
            return True

    def win_or_lose(self, last_word, current_word):
        if (
            current_word not in self.word_list
            and self.check_initial(last_word, current_word)
            and self.check_last(current_word)
            and self.check_hinsi(current_word)
        ):
            return True
        else:
            if current_word in self.word_list:
                print("既出です")
            if self.check_initial(last_word, current_word) == False:
                print("最後の文字から始まっていません")
            if self.check_last(current_word) == False:
                print("「ん」で終わっています")
            if self.check_hinsi(current_word) == False:
                print("名詞ではありません")
            return False

    def win_or_lose_for_chatgpt(self, last_word, current_word):
        if (
            current_word not in self.word_list
            and self.check_initial(last_word, current_word)
            and self.check_last(current_word)
            and self.check_hinsi(current_word)
        ):
            return True
        return False

    def get_lastletter(self, current_word):
        result = self.juman.analysis(current_word)
        mrph_list = result.mrph_list()
        mrph = mrph_list[-1]
        if mrph.yomi[-1]=="ー":
            return mrph.yomi[-2:-1]
        return mrph.yomi[-1]

    def play(self):
        n = 0
        current_word = ""
        self.last_word = ""
        last_letter = None
        user = input("名前を教えてください: ")
        while n < self.N:
            word = input("単語を入力してください: ")
            current_word = word
            if self.win_or_lose(self.last_word, current_word) == False:
                print(f"{user}の負けです")
                exit()
            self.word_list.add(current_word)
            last_letter = self.get_lastletter(current_word)
            if self.check_small(last_letter):
                last_letter=self.change_small(last_letter)
            self.last_word = current_word
            current_word = ""
            while self.win_or_lose_for_chatgpt(self.last_word, current_word) == False:
                print(f"「{last_letter}」から始まる単語を探しています...")
                command = f"{last_letter}から始まる名詞を一つ言ってください"
                current_word = self.ask_ChatGPT(command)
                print("ChatGPT:", current_word)
                if self.win_or_lose(self.last_word, current_word) == False:
                    print("Chatgptの負けです")
                    exit()
                self.last_word = current_word
                self.word_list.add(current_word)
                n += 1
    
game = ShiritoriGame()
game.play()
```
## 感想
細かい判定がめんどすぎた。
コードは改善点だらけだと思いますが読んでくださった方ありがとうございました。駄文失礼しました。