Unity×ChatGPTでAI彼女Botを作るチャレンジ備忘録

# はじめに

[ChatGPT API](https://platform.openai.com/docs/guides/chat)が公開され、開発者が様々なプラットフォームでChatGPTが活用できるようになりました。

この記事では、Unityエンジニア向けに、ChatGPT活用事例を共有したいと思います。

# 対象者

- Unityある程度できる人
    - REST API叩いたことある
    - Editor拡張やったことある
    - ScritableObjectつくったことある
- ChatGPT使ったことある
    - 雰囲気知っていればOK

# できたもの

## エディタ拡張で対話

https://twitter.com/iwaken71/status/1634630358946942976?s=20

## UI上で会話

https://twitter.com/iwaken71/status/1634813444951273472?s=20

# 難しい問題「彼女らしさ」

AI彼女Botを作るにあたって「彼女らしさ」をどう引き出すが大事だと思っています。例えば、[ChatGPT](https://chat.openai.com/chat)のサービスに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/043337df-604f-d049-27b5-0329a1c3e06b.png)

> 「やっほー今起きた」


と送って

> 「こんにちは！おはようございます。朝早起きは健康的な習慣ですね。今日は何か特別な予定がありますか？」



というアシスタント風で返されると、ちょっとテンション上がらないですよね。この彼女風にしゃべってもらうために、どう調整するかが肝になります。

- 「彼女らしさ」を発揮しながらしゃべるように、promptを仕込む
    - 丁寧語を辞めるためには
    - 長すぎる文章をやめてもらうには
    - ライトな口調でしゃべってもらうためには
- promptを何度も試しやすいようにする

この2つを、この記事では紹介します。

# 解決策

- System Contextをいじれるようにする (彼女らしさを作り出す命令)
- UserProfileで個人情報を入力する (名前を呼んでもらえるようにする)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/5551f38e-342b-14f9-a4de-eed97d88d0cf.png)

> あなたは19歳のuserのガールフレンドとしてuserに話しかけます。付き合って2年目です。語尾に"！"か"w"か"～"をつけて、丁寧語と敬語を使わないでください。レスポンスは50文字以内にしてください。(ARGirlProfileの中身)



![ChatGPTGame - ChatGPTTest - Windows, Mac, Lissnux - .png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/6f0dc6f0-679b-dd1c-9da6-799cb364b6b4.png)


こんな感じで、イテレーションを回しやすくします。

# やってみて気づき

- 構文
    - あなたは○○としてuserに話しかけます。
    - 語尾は○○です。
- 丁寧語を辞めてもらうために
    - 19歳 (年齢下げないと、アシスタント口調に寄ってしまう)
    - ガールフレンド (彼女、だと代名詞と間違われるのか、丁寧語になってしまうので、ガールフレンドという表現)
    - 付き合って2年目 (効果があるかはわからないが、入れてからいい感じになった気がする)
    - 語尾に"！"か"w"か"～"をつけて (気軽に効果が出やすい。お好みでチャレンジ)
- 文字数を減らすために
    - レスポンスは50文字以内 (なのに、平気で超えてくる)

# 実装パート

## 参考資料

ChatGPT APIをUnityから動かす。のはこちらの記事で環境構築おねがいします。このnoteを行っている前提で、この記事は書かれています。

https://note.com/negipoyoc/n/n88189e590ac3

# 彼女らしさを作り出すsystem role

ChatGPTのAPIにて、Messageを送るときに「role」と「content」があります。

- role
    - system ★今日はココ
        - アシスタントの行動を設定します。
    - user
        - アシスタントに指示するのに使います。エンドユーザーや開発者が設定します。
    - assistant
        - 事前の反応を保存するのに役立ちます。また、開発者が書き込むことで、望ましい動作の例を示すのに役立ちます。
- content
    - メッセージの中身


このsystemをroleにしてcontentを送る処理をすると、振る舞いが彼女らしさに近づいてきます。

# すっごい結論的なコード (追記)

negipoyocさんのnote記事の以下のコードの`content`の中身を変更すればよいです
```cs
   _messageList.Add(new ChatGPTMessageModel(){role = "system",content = "語尾に「にゃ」をつけて"});
```

それを、ScritableObjectから変更できるようにして、Editor拡張などで叩けるようにしたのが本記事です。なので面倒くさいことをしているかもしれません。
さくっと試したい方は上記のcontentの中身の文字列を、ご自身で変更するコードを書いてください。

ポイントは、`role="system"`となっているところです。これによりアシスタントの行動を設定します。

# 実装コード一部

### Enum
```cs:Enum
    public enum Role
    {
        system,
        user,
        assistant
    }
```

### ChatGPTConnectClient.cs
```cs:ChatGPTConnectClient.cs

using System;
using System.Collections.Generic;
using System.Text;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

public class ChatGPTConnectClient
{
    private readonly string _apiKey;
    private readonly List<ChatGPTMessageModel> _messageList = new List<ChatGPTMessageModel>();

    public ChatGPTConnectClient(string apiKey)
    {
        _apiKey = apiKey;
    }

    // ここで彼女ふるまいを仕込む！
    public void SetAIGirlProfile(AIGirlProfile profile)
    {
        if (profile.systemContext != String.Empty)
        {
            AddMessage(Role.system,profile.systemContext);
        }
    }

    public void SetUserProfile(UserProfile profile)
    {

        string message = $"自己紹介をします！私の名前は{profile.myName}です。" +
                         $"性別は{profile.gender}です。" +
                         $"ニックネームは{profile.nickName}です。" +
                         $"誕生日は{profile.dateOfBirth}の{profile.age}です。" +
                         $"出身は{profile.birthplace}で、現在は{profile.address}に住んでいます。" +
                         $"趣味は{profile.interest}です。" +
                         $"職業は{profile.occupation}です。" +
                         $"好きな食べ物は{profile.favoriteFood}です。" +
                         $"TwitterのIDは@{profile.TwitterID}";
        AddMessage(Role.user,message);
    }
    void AddMessage(Role role, string contennt)
    {
        AddMessage(new ChatGPTMessageModel {role = role.ToString(), content = contennt});
    }
    void AddMessage(ChatGPTMessageModel model)
    {
        SaveChat.AddChat(model);
        _messageList.Add(model);
    }
    public async UniTask<ChatGPTResponseModel> RequestAsync(string userMessage)
    {
        var apiUrl = "https://api.openai.com/v1/chat/completions";
        AddMessage(Role.user, userMessage);
        //OpenAIのAPIリクエストに必要なヘッダー情報を設定
        var headers = new Dictionary<string, string>
        {
            {"Authorization", "Bearer " + _apiKey},
            {"Content-type", "application/json"},
            {"X-Slack-No-Retry", "1"}
        };
        
        //文章生成で利用するモデルやトークン上限、プロンプトをオプションに設定
        var options = new ChatGPTCompletionRequestModel()
        {
            model = "gpt-3.5-turbo",
            messages = _messageList
        };
        var jsonOptions = JsonUtility.ToJson(options);
        Debug.Log("自分:" + userMessage);
        
        //OpenAIの文章生成(Completion)にAPIリクエストを送り、結果を変数に格納
        using var request = new UnityWebRequest(apiUrl, "POST")
        {
            uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(jsonOptions)),
            downloadHandler = new DownloadHandlerBuffer()
        };
        foreach (var header in headers)
        {
            request.SetRequestHeader(header.Key, header.Value);
        }
        await request.SendWebRequest();
        
        if (request.result == UnityWebRequest.Result.ConnectionError ||
            request.result == UnityWebRequest.Result.ProtocolError)
        {
            Debug.LogError(request.error);
            throw new Exception();
        }
        else
        {
            var responseString = request.downloadHandler.text;
            var responseObject = JsonUtility.FromJson<ChatGPTResponseModel>(responseString);
            Debug.Log("ChatGPT:" + responseObject.choices[0].message.content);
            AddMessage(responseObject.choices[0].message);
            return responseObject;
        }
    }
}
```


### AIGirlProfile (ScritableObject)
```cs:AIGirlProfile
using UnityEngine;

[CreateAssetMenu(fileName = "AIGirlProfile", menuName = "ScriptableObjects/AIGirlProfile", order = 1)]
public class AIGirlProfile : ScriptableObject
{
    [TextArea]
    public string systemContext;
}
```

これを作ると右クリックでAIGirlProfileをつくることができるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/1340afe6-2e59-782a-636b-b4a7c933d2a2.png)



```cs:AIGirlBot.cs
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class AIGirlBot : MonoBehaviour
{
    [SerializeField] private AIGirlProfile aiGirlProfile;
    [SerializeField] private UserProfile userProfile;
    public string sendMessage,responseMessge;
    private ChatGPTConnectClient client;

    void Start()
    {
        client = new ChatGPTConnectClient(config.apiKey);
        client.SetAIGirlProfile(aiGirlProfile);
        client.SetUserProfile(userProfile);
    }
    public async UniTask<ChatGPTResponseModel> SendMessage(string userMessage)
    {
        sendMessage = userMessage;
        responseMessge = "書き込み中...";
        var response = await client.RequestAsync(sendMessage);
        return response;
    }

    public async void SendGPT()
    {
        responseMessge = "書き込み中...";
        var response = await client.RequestAsync(sendMessage);
        responseMessge = response.choices[0].message.content;
    }
}

```

# Editor拡張から送信！

AIGirlBotEditor.csをEditorフォルダ以下に置く。
```cs:AIGirlBotEditor.cs
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(AIGirlBot))]
public class ChatGPTEditor : Editor
{
    public override void OnInspectorGUI(){
        //targetを変換して対象を取得
        AIGirlBot chatGpt = target as AIGirlBot;
        
 
        //元のInspector部分を表示
        base.OnInspectorGUI ();
        
        EditorGUILayout.LabelField("私のメッセージ");
        chatGpt.sendMessage = EditorGUILayout.TextArea(chatGpt.sendMessage, GUILayout.Height(EditorGUIUtility.singleLineHeight * 3));
      

        if (GUILayout.Button("AI彼女に送信！")){
            chatGpt.SendGPT();
        }
        EditorGUILayout.LabelField("AI彼女のメッセージ");
        EditorGUILayout.LabelField(chatGpt.responseMessge, EditorStyles.wordWrappedLabel);
    }
}

```

こうすると、AIGirlBotを見ると、ボタンが登場します！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96970/936369b6-2df1-961c-060c-4cd7d8e15bdd.png)

# UIの実装

また今度！

# 今後やりたい事

- キャラクターのビジュアルも込みで実現
- Promptの仕込みのチューニング
    - Assistant roleで理想の会話を仕込む


# 最後に

ざくっと記事を書いてしまったので、わかりにくかった人もいるかもしれないです。コメントで感想を教えていただけるとありがたいです。