kintoneから生成AIとAPI連携し、回答をレコードに自動保存するJavaScriptと活用案

![サムネイル.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211946/b2fedf62-8bbe-770e-363c-0ae3405b1bed.jpeg)

# 概要

kintoneをCRM（顧客管理）やSFA（営業支援）として利用している企業にとって、
顧客対応の効率化は永遠の課題であり、伸び代でもあります。

そこでkintoneと、今話題である**生成AI**（今回は`Gemini API`を使用）を連携し、
効率化させていきたいと思います。

生成AIと連携するkintoneプラグインはすでに下記のように市販されておりますが、
内容や利用規模によっては内製のAPI連携で十分構築できると感じました。

https://kintone-sol.cybozu.co.jp/integrate/msol026.html

まずはコードを紹介し、その後に活用案の一例を紹介します。

# kintoneへのコード設定

## 事前準備①

kintoneのUI上にて、
- ボタンを配置する`スペースフィールド`（フィールドコード例：`answerRequestButton`）
- Geminiに送るテキストを保存する`文字列フィールド`（フィールドコード例：`Question`）
- Geminiからの返答テキストを保存する`文字列フィールド`（フィールドコード例：`Answer`）
を配置して保存しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211946/17e30624-f940-2dbf-993a-67e0a68ac5ef.png)

また、APIトークンを発行し、閲覧と編集の権限を付与しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211946/b5cd05aa-de57-9968-9625-ec4841829363.png)

## 事前準備②

GCPからGeminiを使えるようにした上で、
[Google AI Studio](https://aistudio.google.com/app/apikey?hl=ja)にアクセスして、
APIキーを取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211946/55fa527f-b4b9-c56a-bde0-321b0acee074.png)


## JavaScript

```javascript:collaborationGemini.js
(function () {
    'use strict';

    // レコード詳細画面が表示された時のイベント
    kintone.events.on('app.record.detail.show', function (event) {
        // ボタンを作成
        let submitButton = document.createElement('button'); // ボタンを作成
        submitButton.id = 'submit-button'; // ボタンにIDを付与
        submitButton.innerText = 'Geminiに回答してもらう'; // ボタンに表示するテキストを設定

        // ボタンを「answerRequestButton」フィールドに配置
        let buttonSpace = kintone.app.record.getSpaceElement('answerRequestButton'); // フィールドコード「answerRequestButton」に対応する要素を取得
        buttonSpace.appendChild(submitButton); // その要素にボタンを追加

        // ボタンがクリックされた時の処理
        submitButton.onclick = async function () {
            let record = kintone.app.record.get().record; // 現在のレコードデータを取得
            let questionText = record.Question.value; // レコードから「Question」フィールドの値（質問文）を取得

            if (!questionText) {
                alert('質問文が空です'); // 質問文が空の場合のアラート表示
                return;
            }

            // Gemini API情報
            let geminiApiKey = 'YourGeminiAPIKey'; // GeminiのAPIキー
            let geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-002:generateContent?key=${geminiApiKey}`; // Gemini APIのエンドポイント（利用するモデルに応じて変更）
            let requestData = {
                contents: [{
                    parts: [{
                        text: questionText // 質問文をAPIリクエストにセット
                    }],
                    role: "user"
                }],
                // Geminiのプロンプトを記載する
                systemInstruction: {
                    parts: [{
                        text: "お客様からの質問文に対して丁寧に返答してください" // 用途によってプロンプトを記述する
                    }],
                    role: "model"
                }
            };

            try {
                // Gemini APIにリクエストを送信
                let geminiResponse = await fetch(geminiUrl, {
                    method: 'POST', // POSTメソッドを使用
                    headers: {
                        'Content-Type': 'application/json' // リクエストの形式をJSONに設定
                    },
                    body: JSON.stringify(requestData) // リクエストのボディに質問データを含める
                });

                if (!geminiResponse.ok) {
                    throw new Error('Gemini APIのリクエストに失敗しました'); // エラーハンドリング
                }

                let geminiData = await geminiResponse.json(); // APIからのレスポンスをJSONとして取得
                console.log('Gemini APIのレスポンス:', geminiData); // レスポンスをコンソールに表示

                // Gemini APIの候補データが存在するか確認
                if (geminiData.candidates && geminiData.candidates.length > 0) {
                    // レスポンスデータから回答内容を取得
                    let answerContent = geminiData.candidates[0].content.parts.map(part => part.text).join('') || "回答が見つかりませんでした";

                    // kintone APIに回答を保存するデータを準備
                    let kintoneApiUrl = `https://hoge.cybozu.com/k/v1/record.json`; // "hoge"の部分を利用するkintoneサブドメインに変更する
                    let saveRecord = {
                        app: kintone.app.getId(), // アプリIDを取得
                        id: record.$id.value, // レコードIDを取得
                        record: {
                            Answer: {
                                value: answerContent // Geminiからの回答を「Answer」フィールドに保存
                            }
                        }
                    };

                    // kintone APIに回答を送信して保存
                    console.log('kintone APIへのリクエストボディ:', JSON.stringify(saveRecord)); // リクエストの内容をログ出力

                    let kintoneResponse = await fetch(kintoneApiUrl, {
                        method: 'PUT', // レコード更新はPUTメソッドを使用
                        headers: {
                            'Content-Type': 'application/json',
                            'X-Cybozu-API-Token': 'YourKintoneAPIKey' // 発行したkintoneのAPIトークン
                        },
                        body: JSON.stringify(saveRecord) // レコードデータをリクエストボディにセット
                    });

                    if (!kintoneResponse.ok) {
                        throw new Error('kintone APIへの保存に失敗しました'); // エラーハンドリング
                    }

                    // 成功時のダイアログに回答内容を表示し、ダイアログを閉じたらリロード
                    alert(`回答を保存しました:\n${answerContent}`); // 正常に保存された場合の通知
                    location.reload(); // ダイアログが閉じられたら画面をリロード
                } else {
                    console.error('Gemini APIのレスポンスに候補がありませんでした'); // エラーのログ出力
                    alert('回答が見つかりませんでした'); // 回答がなかった場合の通知
                }
            } catch (error) {
                console.error('エラーが発生しました:', error.message); // エラーログを表示
                alert('APIの処理中にエラーが発生しました'); // エラーメッセージの通知
            }
        };

        return event; // イベントを終了して戻り値を返す
    });
})();
```

:::note info
コードにAPIキーを直接入れるのはそれなりにリスクがあるため、
（特にGeminiは従量で料金が発生するので）
実際は環境変数などで行うことをオススメします
:::

※プロンプト部分、改行を含む場合は、ダブルクォーテーション`"`ではなく、
テンプレートリテラル ` でtextを囲んで下さい。

### コードの機能

このコードは、以下の機能を提供します。

- kintoneのレコード詳細画面にカスタムボタンを追加し、クリック時にGemini APIを用いて質問文に対する自動回答を取得し、それをレコードに保存します。
    - 具体的には、レコードの「Question」フィールドの値をGemini APIに送信。
    - その後、返り値の回答を取り出し、kintoneの「Answer」フィールドに保存します。
    - 成功した際はダイアログで回答内容を通知し、閉じると画面更新。



# 活用案

ではどんなシーンで使えそうかを実際の動作結果を交えて紹介していきます。

## 活用案1: メール作成

箇条書きなどのメモ書きから、顧客に送信するメール文の自動生成が可能です。
それにより購入後のフォローアップや問い合わせ対応を自動化・効率化します。
また、生成AIにメール文のトーンや内容を事前に設定することで、
どのスタッフが対応しても均一な顧客体験を提供できます。

## プロンプト

```
以下の情報を基に、購入者に送信するフォローアップメールを生成してください。
文体はビジネスライクかつ丁寧で、問題の詳細をお聞きしつつ、顧客の安心感を高めるために謝罪しつつも、丁寧にサポートし、解決まで協力することがわかるようにしてください。
また、最後に、サポート窓口への連絡方法も追記してください。
なお、送信者は「サポート担当 小林」で、0120-xx-xxxxが電話番号です。

- 購入者名
- 商品名
- 購入日
- 問い合わせの詳細（不具合や質問内容）
- 担当者名（送信者）
```

## 送信テキスト例

```
購入者名: 田中太郎  
商品名: スマートフォン連動オーブン SMA001
購入日: 2024年10月1日  
問い合わせ内容: オーブンの温度調整機能が正しく動作せず、レシピ通りに調理ができないとのこと
備考：松本より後ほど担当者からメールするとお伝え済み
```

## 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211946/cacc0de4-5596-867e-2262-c1865f461aa7.png)

メール文作成はプロンプトにどれだけ対応方針を入れ込めるか的な所がありますが、
プロンプトに力を入れれば送信前の確認だけでよくなりそうですね。

## 活用案2: FAQの自動生成

顧客から寄せられた質問への回答を生成し、問い合わせに対する解答を一元管理します。

また、回答をWebサイトや顧客対応用のチャットボットに反映させることにより、
顧客対応時間の短縮によるサポートコストの削減や、
顧客満足度の向上が期待できます。

## プロンプト
```
顧客からの問い合わせ内容に基づいて、FAQセクションを丁寧な文体で作成してください。
各質問に対して質問と回答を簡潔にまとめ、必要に応じて関連するサポートページや製品マニュアルのリンクを含めてください。
また、わからない場合は不明なのでサポートセンターへ電話する旨を記載してください。
なお、各項目には質問のカテゴリが[保証]、[料金]、[サポート]、[苦情]、[その他]のどれにあたるかをリストの質問の冒頭部分に分かりやすく記載してください。
また、長くなりすぎず簡潔にお願いします。不要な改行は使わないで下さい。
Markdown形式ではなく、通常のテキストで作成してください。


■ 商品マニュアル
・保証期間は2年間だが、公式サイトに登録で3年間まで延長
・国外への配送はしていない。国内は無料配送。
・故障した際は、サポートセンター（0120-xx-xxxx）まで電話してほしい。営業時間は平日10時から18時まで。
・自然に使っていた以外の故障は対応できない。
・修理するまでは1ヶ月程度かかる場合がある。
・初期不良以外での返品は不可。
```

## 送信文例

```
・購入した掃除機を、子どもが乗って壊してしまったが無料で修正してくれますか？
・思っていた色と違うので交換してほしい。
・国外への配送には追加料金がかかりますか？難しい場合は個人で海外に送るので日本に配送してください。
・初期不良なのか使えないがどうすればいいか。返品できるか。
・普通に使っていて1年4ヶ月で故障したが無料で治してもらえるか？
```

## 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211946/af0bffa2-ec47-968b-084c-f68b9ab09e3b.png)

見やすくなりました。
また、json形式で返すもらうことでそのままデータベースに格納したりもできそうですね。

## 活用案3: アンケート分析

アンケートデータを分析し、顧客対応だけでなく、
今後のマーケティングや商品開発に役立つ洞察を提供します。
複雑なデータでも、生成AIが要点を抽出するため、確認時間の短縮にも役立ちます。

## プロンプト

```
アンケート結果を分析し、特に顧客の不満や満足点を抽出し、今後の製品改善やサービス向上に役立つ提言を作成してください。
特に、自由回答に記載された顧客の具体的な意見を重視し、全体のトレンドや改善すべき点についても言及してください。
また、良かった点と改善点でそれぞれ分けて箇条書きで記載するようにしてください。長くなりすぎず簡潔にお願いします。
Markdown形式ではなく、通常のテキストで作成してください。また、全体は300文字程度で収まるようにしてください。
```


## 送信文例

```
アンケート結果

商品: 高性能掃除機 X500
購入者名: 佐藤 一郎 様

・価格: 4/5  
　「他社の製品と比べて少し高い印象を受けましたが、性能を考えると納得できる価格です。ただ、同価格帯の他製品ともう少し比較したかった気もします。」

・配送: 3/5  
　「配送は指定日時通りに到着しましたが、配送業者の対応がやや雑で、箱が一部へこんでいたのが気になりました。また、注文から到着までの進捗状況がこまめに連絡されなかったため、少し不安に感じました。」

・設置: 2/5  
　「商品の説明書がわかりにくく、特に初期設定の部分でかなり手間取りました。公式サイトに説明動画もなく、最終的にはYouTubeのレビュー動画を参考にして設置しましたが、あまりにも複雑すぎる印象を受けました。もう少し簡単なマニュアルがあれば良かったと思います。」

・使用感: 5/5  
　「実際の吸引力は非常に満足しています。カーペットの奥に入り込んだゴミもきれいに取れるため、毎回掃除後に部屋が新しくなったような感覚になります。ただし、音がやや大きく感じるので、静音モードがあるともっと良かったです。」

・サポート: 3/5  
　「カスタマーサポートに問い合わせた際、担当者の対応は丁寧でしたが、回答が少し遅かったです。また、メールの文面が事務的で冷たい印象を受けました。もう少し親身な対応を期待していました。」

・全体的な満足度
　4/5
```

## 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211946/e154e286-08c0-3482-f2ce-e0ca9716e1bf.png)

めちゃくちゃ見やすくなりました！

# まとめ

上記のように、生成AIを導入することで、顧客対応が効率化されて、
スタッフの負担を軽減しつつ、より良い顧客体験を提供することが可能です。

ただし、ここで紹介したコードでは簡易的であり、kintoneの単一のフィールドからしか値を送信してないことや、
Gemini側も単純なプロンプトであり、外部の辞書データを参考にしてないなど、
もっと実務に沿った返答が返ってくるようにカスタマイズしていく必要があります。

ほぼすべての会社が生成AIのビジネス利用を検討していると思いますが、
kintoneという少しニッチな分野であっても、
生成AIを取り入れていくことでローコードツールとしての強みをより増せると思っています。

## 参考

https://zenn.dev/peishim/articles/2e2e8408888f59

ありがたい…