NetSuiteで5分で作ってみる、プライベートチャットボット

「NetSuite上でプライベートなチャットボットを作りたい」と思っても、外部サービスや複雑なコードが必要そうに見えます。ところが、NetSuiteが提供する N/llm モジュールを使えば、わずか数十行のSuiteScriptだけで、実際に動くチャットボットをあなたもディプロイすることができます。それをたたき台にして、御社のビジネスにどのように応用できる可能性があるか、ぜひ議論のネタとしていただけますと幸いです。
<br>

**このサンプルチャットボットの機能**
・ユーザーが質問を入力
・N/llmがその内容を解釈し、回答を生成
・過去のやり取りをフォーム上に表示し、チャット形式でのやり取りが可能
<br>

**5分間のスケジュール**

1.	ファイルキャビネットにスクリプトをアップロード
スクリプトを.jsファイルとしてNetSuiteのFile Cabinetへ上げます。
2.	Suiteletスクリプトを新規作成
「カスタマイズ → スクリプト → スクリプト」で、スクリプトタイプに「Suitelet」を選択し、先ほどアップロードした.jsファイルを割り当てます。
3.	デプロイメント
「Deployments」タブで「新規デプロイ」を作り、URLを確認。対象ユーザーのロールなども設定します。
4.	動作確認
作成されたデプロイメントのURLへアクセスすると、上記フォームが表示されます。テキストを入力して「送信」すると、N/llmが生成する回答が表示されます。
5.	メニューへの配置（任意）
「カスタマイズ → センターとタブ → センターリンク」で、先ほどのSuiteletのURLをメニューに登録すると、「リスト > カスタム > スイートボット」のように、簡単にチャットボットにアクセスできます。
<br>


**1. ファイルキャビネットにスクリプトをアップロード**
```
/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 */
define(["N/ui/serverWidget", "N/llm", "N/log"], (serverWidget, llm, log) => {
  
    const onRequest = (context) => {
      const form = serverWidget.createForm({ title: "スイートボット（サンプル）" });
      const fieldGroup = form.addFieldGroup({
        id: "chat_group",
        label: "チャット履歴"
      });
      fieldGroup.isSingleColumn = true;
      
      // チャットメッセージ数を隠しフィールドで管理
      const historySize = parseInt(context.request.parameters.custpage_num_chats || "0");
      const numChats = form.addField({
        id: "custpage_num_chats",
        type: serverWidget.FieldType.INTEGER,
        label: "メッセージ数",
        container: "chat_group"
      });
      numChats.updateDisplayType({ displayType: serverWidget.FieldDisplayType.HIDDEN });
      numChats.defaultValue = historySize;
      
      // POST時は新しい入力を処理
      if (context.request.method === "POST") {
        const chatHistory = loadHistory(context, form, historySize);
        const userInput = context.request.parameters.custpage_input || "";
        
        if (userInput.trim()) {
          // ユーザー発言を画面に表示
          addMessageField(form, "custpage_hist" + historySize, "あなた", userInput);
          chatHistory.push({ role: llm.ChatRole.USER, text: userInput });
          
          // AIからの回答を取得
          const responseText = generateResponse(chatHistory);
          addMessageField(form, "custpage_hist" + (historySize + 1), "チャットボット", responseText);
          
          // ヒストリー件数を2増加（ユーザー＋チャットボット）
          numChats.defaultValue = historySize + 2;
        }
      }
      
      // 次回の質問入力欄
      const inputField = form.addField({
        id: "custpage_input",
        type: serverWidget.FieldType.TEXTAREA,
        label: "質問を入力してください",
        container: "chat_group"
      });
      inputField.setHelpText({
        help: "AIによる回答を生成するため、正確性は保証されません。"
      });
      
      // 送信ボタン
      form.addSubmitButton({ label: "送信" });
      
      context.response.writePage(form);
    };
  
    /**
     * 過去のメッセージを読み込み・フォームに表示
     */
    const loadHistory = (context, form, historySize) => {
      const chatHistory = [];
      for (let i = 0; i < historySize; i++) {
        const text = context.request.parameters["custpage_hist" + i] || "";
        const label = i % 2 === 0 ? "あなた" : "チャットボット";
        addMessageField(form, "custpage_hist" + i, label, text);
        chatHistory.push({
          role: i % 2 === 0 ? llm.ChatRole.USER : llm.ChatRole.CHATBOT,
          text: text
        });
      }
      return chatHistory;
    };
  
    /**
     * フィールド追加用のヘルパー
     */
    const addMessageField = (form, id, label, text) => {
      const fld = form.addField({
        id: id,
        type: serverWidget.FieldType.TEXTAREA,
        label: label,
        container: "chat_group"
      });
      fld.defaultValue = text;
      fld.updateDisplayType({ displayType: serverWidget.FieldDisplayType.INLINE });
    };
  
    /**
     * LLMモジュールを使って回答を生成
     */
    const generateResponse = (chatHistory) => {
      try {
        // チャット履歴を文字列のプロンプトに変換
        let prompt = "あなたはNetSuiteの専門家です。回答は必ず日本語で行ってください。\n\n";
        
        // 会話履歴をプロンプトに追加
        for (let i = 0; i < chatHistory.length; i++) {
          const role = chatHistory[i].role === llm.ChatRole.USER ? "ユーザー" : "チャットボット";
          prompt += `${role}: ${chatHistory[i].text}\n\n`;
        }
        
        // チャットボットの応答を促すフレーズを追加
        prompt += "チャットボット: ";
        
        // 動作確認済みの方法でllm.generateTextを呼び出し
        const result = llm.generateText({
          prompt: prompt,
          modelParameters: {
            maxTokens: 1000,
            temperature: 0.2,
            topK: 3,
            topP: 0.7,
            frequencyPenalty: 0.4,
            presencePenalty: 0
          }
        });
        
        return result.text || "";
      } catch (e) {
        log.error({
          title: 'LLM Error',
          details: e
        });
        return "エラーが発生しました。再度お試しください。";
      }
    };
  
    return { onRequest };
  });
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3431564/92f59ff0-3c2c-4d5b-8494-2d65024b7dda.png)
※動作確認は、Shift-JIS文字コードでのみ行っております。
<br>

**2. Suiteletスクリプトを新規作成**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3431564/fcd2ba68-84ff-4e4b-8378-dc8fa83a32b5.png)
<br>

**3. デプロイメント**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3431564/ca417b08-168d-4d4c-82e3-c2941fb56bfc.png)
※ディプロイメントのURLから、アプリを開きます。なお、NetSuite 2025.1より、ロールの割当てで「すべてを選択」をチェックすると、内部ロールのみが選択されるようになりました。
<br>


**4. 動作確認**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3431564/5e4e0d6d-5461-47b7-a2e0-f3445b1f4645.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3431564/ef321eb8-1e7b-4100-b391-393717015f8e.png)

<br>

**5. メニューへの配置（任意）**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3431564/dabc23cf-3df7-429a-b01d-f2a5f7848c1a.png)

※申し訳ございませんが、かなりNetSuiteのスクリプト開発に習熟している方でないと、さすがに上記の5つの手順は、「5分」では完了しませんね（私も全く無理です）。
<br>


**チャットボットのカスタマイズのアイディア**
•	精度向上（パラメーター）N/llmのmodelParametersを活用し、必要に応じてtemperatureやmaxTokensなどを調整すると、回答の長さやスタイルが変わります。

•	社内文書活用（プロンプト）: 社内文書や手順書のデータをプロンプトに追加することで、組織特有の質問に答えられるようになります。

•	NetSuiteデータ活用（N/search）: N/searchモジュールを使用して、リアルタイムのNetSuiteデータ（在庫状況、売上実績など）に基づいた回答が提供になります。
<br>

**まとめ**
NetSuiteのN/llmモジュールを活用することで、外部サービスに依存せず、セキュアな環境でカスタムチャットボットを構築できます。まだまだ今後の機能強化が望まれますが、様々なアイディアを組み合わせることで、近い将来、NetSuiteによる業務の効率化やユーザーのサポート強化に繋がれば幸いです。
<br>

**参考リンク**
N/llmは無料で使用を開始できます。有料モードの設定をしない限り追加料金は発生しませんので、安心してチャットボットをお試し下さい。使用モード（無料、オンデマンド、専用AIクラスタ）やその他のAI関連機能について、以下のコミュニティ記事を参考にされて下さい。
[NetSuite サポートコミュニティ› AI関連機能› SuiteScript 2.x 生成AI API](https://community.oracle.com/netsuite/japanese/discussion/4503315/suitescript-2-x-%E7%94%9F%E6%88%90ai-api#latest)
[NetSuite サポートコミュニティ› AI関連機能](https://community.oracle.com/netsuite/japanese/categories/ai%E9%96%A2%E9%80%A3%E6%A9%9F%E8%83%BD)

N/llmと、N/search のさらなる詳細につきましては、ヘルプセンターをご参照ください。
[Oracle NetSuite Help Center > N/llm Module](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/article_9123730083.html#subsect_1014032311)
[Oracle NetSuite Help Center > N/search Module](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4345764122.html#subsect_87180423808)


なお、NetSuiteの標準的なテキストボックスや、NetSuiteからのEmail作成画面で、文章を生成する「テキストエンハンス」機能、およびそのカスタマイズを行う「プロンプトスタジオ」を、リリース2025.1の新機能の一部としてご紹介しております。
[NetSuite サポートコミュニティ› [動画解説] リリース2025.1主要なアップデート](https://community.oracle.com/netsuite/japanese/discussion/4504569/%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B92025-1%E4%B8%BB%E8%A6%81%E3%81%AA%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88#latest)  
[Qiita > [動画解説] リリース2025.1主要なアップデート](https://qiita.com/fujitak/items/90ba19655d90aa1d34dd)  

※コミュニティのアカウントをお持ちでない方は、ぜひコミュニティのアカウントを作成下さい）。