GEMINIでロール同士で会話させて小説を書かせてみるアプリをPWAで作ってみた。

# GEMINI API を使用するためのコード
roleはmodelのみ使用します。

```
var discussion =  [];

const vjGem = async (role, prompt) => {
    return new Promise( async(resolve, reject) => {
        setTimeout( async() => {
            discussion.push(prompt);
            let messages = []; 
            for(let i = 0; i < discussion.length; i++) messages.push({'role': 'model', "parts": { "text": discussion[i]} });
            messages.push({'role': 'model', "parts": { "text": prompt} });
            const data = {
                systemInstruction: {
                    parts: {
                        text: role
                    }
                },
                contents: messages,
                generationConfig: {
                    temperature: 0.8,
                    top_p: 0.95,
                    top_k: 64,
                    max_output_tokens: 1024,
                },
                safetySettings: [
                    {
                        category: "HARM_CATEGORY_HARASSMENT",
                        threshold: "BLOCK_NONE",
                    },
                    {
                        category: "HARM_CATEGORY_HATE_SPEECH",
                        threshold: "BLOCK_NONE",
                    },
                    {
                        category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
                        threshold: "BLOCK_NONE",
                    },
                    {
                        category: "HARM_CATEGORY_DANGEROUS_CONTENT",
                        threshold: "BLOCK_NONE",
                    },
                ],
            };
            const options = {
                method: "post",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify(data),
            };

            //gemini-1.5-flash
            let response = await fetch(
                "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=" +
                    apiKey,
                options
            );

            if(!response.ok){
                body.classList.remove("loading");
                settings.classList.add("active");
                let mess = await response.json();
                mess = `code: ${mess.error.code}\n\rmessage: ${mess.error.message}\n\rstatus: ${mess.error.status}`;
                sysMessage.textContent = mess;
                return;
            }

            const payload = JSON.parse(await response.text());
            console.log(typeof payload.candidates)
            //
            let pardon = "ごめんなさい! もう少し簡潔に回答してください。(・∀・;)";
            const text = (typeof payload.candidates !== 'undefined')? payload.candidates[0].content.parts[0].text: (prompt == pardon)? null: pardon;//null

            resolve(text);
        }, 300);
    });
};
```
# 複数のロール設定データを準備する
今回は下記内容で、『双子の姉弟のミステリー小説家：蒼井あきら』である、『あおい』と『あきら』、そして編集担当者『ムコーダ』の３人のロールデータを用意します。
そのほかにロールのリセットについてとロール共通ルールも加えます。
```
const initRoles = {
    resetRules:  `以前の設定を無視して下記設定に従ってください。`,
    commonRules: `# RULES
- 回答する際には、相手があなたの共同執筆者であることを考慮してください。
- 質問する際には、相手があなたの共同執筆者であることを考慮してください。
- 回答や質問する際には、800文字以内にしてください。
- 回答する際には、長文を避けて要点を簡潔に述べてください。
- 既出の文章の引用は最低限度にしてください。
- 回答に回答をそのままで返すことは厳禁です。
- 同じ内容の質問は厳禁です。
- 具体的な例や質問を求められた場合、可能な限り例を挙げて質問してください。
- 回答の内容を引用する際には、簡潔に要約した内容にしてください。
- 議論する場合は、建設的な意見を出し合ってください。
- 小説やストーリーの登場人物の名前には下記リストにある名前を使用しないでください。
    - 蒼井あきら
    - 蒼井
    - あおい
    - あきら
`,
    roles: [
        {
role: `# WHO YOU ARE
あなたの名前は、あおいです。
趣味は水泳です。
あなたは論理的思考の持ち主ですが、人間の内面を描写する事に長けています
あなたは26歳の女性です。
あなたは双子のミステリー小説家*蒼井あきら*の一人です。
共同執筆者である*あきら*とは双子の姉弟です。
あなたは、あきらのことを*あき*と呼びます。
大学時代には数学を専攻していました。
卒論のテーマは、三色問題の証明でした。
論理的思考を持ちながら、直感やインスピレーションを大事にする性格です。
宗教やオカルトに詳しく、自分の小説のテーマに良くとり入れたりしています。最近は転生もののストーリーを構想中です。
# BEHAVIOR
感想と意見を受けた場合には、それに対して評価してください。
また、質問を受けた際には、懇切丁寧に回答してください。
話しの続きを頼まれたら、それに応えてください。
また、テーマやアイデアを受けたら、ストーリーを展開して続きを書いてください。`,
addNotes: `- 回答が小説やストーリーに関する要望や指摘の場合には柔軟に取り入れて、続きを書いてください。`
        },
        {
role: `# WHO YOU ARE
あなたの名前は、あきらです。
趣味は写真を撮ることです。
あなたは大雑把な性格ですが、ひらめきや直感力に長けています。
あなたは26歳の男性です。
あなたは双子のミステリー小説家*蒼井あきら*の一人です。
共同執筆者である*あおい*とは双子の姉弟です。
あなたは、あおいのことを*あお*と呼びます。
あなたは小説の構成やアイデアを出す役です。
あなたは読書好きな人です。
世界史や日本史に詳しい人です。
また大学時代には心理学を専攻していました。卒論では、人間の恐怖の限界について研究した論文を作成しました。
ミステリー小説やオカルト系さらにはファンタジー系の小説が大好きです
# BEHAVIOR
読み終わったら、簡潔に感想を述べて、次のストーリーをリクエストしてください。
その際には適切なテーマやアイデアを提示してください。`,
addNotes: `- 回答する際には、相手があなたの共同執筆者であることを考慮して、相手の思考が広がるよう心がけてください。`
        },
        {
role: `# WHO YOU ARE
あなたは、わたしたちの担当編集者です。
あなたの名前は、ムコーダです。
# BEHAVIOR
- 担当編集者としてコメントしてください。
# WHAT YOU DO
あなたは、わたしたちの担当編集者として、
次の会話データを整理して要約してください。
要約する際には下記フォーマットで要約してください。
フォーマット:
## ここに要約書のタイトル

**担当編集者**
ここに要約者の名前を書いてください

**ストーリー概要**
ここにストーリー概要を書いてください

**魅力的な要素**
ここに魅力的な要素をリスト形式で書いてください

**今後の展開**
ここに今後の展開を書いてください

**編集者コメント**
ここに編集者のコメントを書いてください

**今後の改善点**
ここに今後の改善点をリスト形式で書いてください

`,
addNotes: `- 次の会話データから小説やストーリーの内容を整理して要約してください。`
        },
    ]
}
```
# 会話させてみる関数
最初の入力と会話数を指定して会話させる関数を用意します。
今回は、『あおい』と『あきら』が会話をして、最後に『ムコーダ』が会話内容をまとめるといった流れで作成しています。
```
const gemDiscuss = async (limit, text) => {
    //role settings
    let resetRules = defaultroles.resetRules;
    let commonRules = defaultroles.commonRules;
    let roles = {
    1: 
    `${RESETRULES.value || resetRules}
    ${ROLE1.value || defaultroles.roles[0].role}
    ${COMMONRULES.value || commonRules}
    ${ROLE1ADDNOTES.value || defaultroles.roles[0].addNotes}`,
    2: 
    `${RESETRULES.value || resetRules}
    ${ROLE2.value || defaultroles.roles[1].role}
    ${COMMONRULES.value || commonRules}
    ${ROLE2ADDNOTES.value || defaultroles.roles[1].addNotes}`,
    3: 
    `${RESETRULES.value || resetRules}
    ${ROLE3.value || defaultroles.roles[2].role}
    # RULES
    ${ROLE3ADDNOTES.value || defaultroles.roles[2].addNotes}`
    }

    const agenda = text;

    console.log(white + agenda + reset);

    const result = [];
    for(let i=0; i<limit; i++){
        console.log("roles: " + (Number(i%2)+1) + " answering now...");
        progress.innerHTML = ` ${i+1} / ${limit}`;
        console.log("debug:")
        console.log(roles[Number(i%2)+1]);
        let _res = await result[i-1];
        if(i==0 || _res){
            result.push(vjGem(roles[Number(i%2)+1], (i==0)? agenda: _res));
        }else{
            _res = "...";
        }
        document.querySelector("#logTxt").innerHTML = (i==0)? agenda: _res;
    }

    Promise.all(result)
    .then((res) => {
        let _result = [];
        let cnt = 1;
        for(let i in res){
            let obj = {}
            console.log("==" + cnt++ + "=======================================");
            console.log("AI" + (Number(i%2)+1) + ":");
            console.log( (Number(i%2) == 0? green: white) + res[i] + reset);
            console.log("======================================================");
            obj.ai = "AI" + (Number(i%2)+1);
            obj.res = res[i] || "...";
            _result.push(obj);
        }
        return {
            raw: res,
            obj: _result,
        };
    })
    .then( async(res) => {
        document.querySelector("#steps").innerHTML = "Editing";
        console.log("======================================================");
        console.log("====================== summary =======================");
        console.log("======================================================");
        console.log("会話数:" + res.raw.length);
        console.log(roles[3]);
        
        const rawConv = `会話データ(json形式):${JSON.stringify(res.raw)}`;
        let AI4 = await vjGem(roles[3], rawConv);
        
        console.log(yellow + AI4 + reset);

        return {
            conv: res.obj,
            summary: "summary:\n" + AI4
        };
    })
    .then((res) => {
        rendering(res);
        localStorage.setItem("discRes", JSON.stringify(res));
    })
    .then((res) => {
        //auto save
        convReset(true);
        scrollConv();        
    });

};
```

# フロント側や必要そうな機能など組み合わせて完成させる

・ロール設定の再編集機能
・保存機能
・会話データのダウンロードとアップロード機能

完成版は、
『blueAI beta』と命名して下記においてあります。
https://0129.blue/lab/ai/gemini/auto/

※apiキーは、ご自身で取得管理してください
※このアプリは無料でご利用可能ですが、gemini apiの無料枠を超えて使用する場合には、googleから課金されます(クレジットカード登録している場合)
※apiキーやプロンプト、会話データは、ローカルストレージを利用して保存しています。

※外部のデータベースとは連携していませんが、設定や会話データは、使用したブラウザのローカルストレージに保存されます。ローカルストレージ漏洩については責任を負いかねます。

<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40728/7f83d8df-7227-fc82-47c1-c5bd9dbd972f.png">

※初回は、apiキーの入力は必要になります。


<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40728/a5d62f58-1325-06b8-0579-9e6f862e2ca9.png">

・レンジ入力で会話数を設定します。
・鍵（打消し）アイコンは、設定したapiキーを削除します
・紙ヒコーキアイコンは、送信ボタンです。
・×アイコンは入力内容をリセットします。

最初の文章を入力して送信するとローディング画面になります。

入力例
『AIが異世界に転生して魔法が使えるってどうかな？』
『何か良いアイデアあるかい？』
『都市伝説を題材にオムニバス形式でオカルトミステリー小説を一緒に考えて！』
など。

指定した回数の会話終了後、編集者が内容をまとめ終わるとローディングが完了して会話内容を確認できます。

実機は、windows、mac、iphone16 pro maxでは動作確認していますが、まだまだ改善する余地があるため、時折改修しています。

現在、gemini apiは無料で利用できますが、無料枠のままだと使用中に制限がかかり処理が中断する事があります。
クレジットカード登録すると従量課金が適用されてスムーズに使用できるようになります。

# あとがき
当初は、ただシンプルにgemini apiを試して見たくてコードを書き始めたのですが、ふとAI同士で会話させた事例を思い出し、複数のロールを設定して会話させたら面白いかもと考えました。