LOADING...

加載過慢請開啟緩存(瀏覽器默認開啟)

loading

用OpenAI與VueJS建立網頁聊天機器人

本文章實作以OpenAI 的chat GPT模型為基礎,以VueJS做為前端的聊天機器人,會詳細的介紹,包括申請Open AI API,還有後端、前端等等,最後會做出可根據上下文進行聊天對話的聊天機器人

  • 環境
    • 後端
      • NET Core 6.0
    • 前端
      • Node.js
      • VueJS前端

壹、預先準備

  • 首先最重要的是,要取得Open AI的API Key,注意這部分需要綁定信用卡,需要先預付一筆金額

  • 到OpenAI網頁來申請 https://openai.com/,點選右上角註冊登入

  • 登入之後,選擇右邊API,接著選的API keys

  • 選擇Create new secret key

  • 輸入Key的名稱,這裡取作MyTestKey,在選擇Project 名稱,這裡用預設的Default Project,最後選擇 Create secret key

  • 最後在這頁,把API KEY了複製下來,在這個頁面之後,將無法再看到的API KEY,所以如果忘記了,就要再新增一個API KEY了

貳、後端

  • 這裡我們用ASP .NET Core WebAPI 做為後端,首先要安裝Visual Studio ,而且必須安裝網頁開發相關的套件

建立ASP .NET Core WebAPI專案與secret key設定

  • 建立新的專案

  • 選擇ASP .NET Core Web API,注意: 若沒安裝ASP .NET網頁開發套件,則不會顯示 ASP .NET Core Web API 專案的選項

  • 名稱取作 chatbot_backend_api,建立

  • 先安裝OpenAI相關套件,打開 套件管理器主控台

  • 在下面的套件管理器主控台輸入指令安裝 OpenAI 的相關套件

    NuGet\Install-Package OpenAI -Version 1.11.0
  • 這裡我們要設定專案中的 appsettings.json檔案,先將openai 的 secret key填到 <your_openai_apikey> 的位置

    appsettings.json

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "openai_key": "<your_openai_apikey>",
      "AllowedHosts": "*"
    }

建立API Controller

  • 在右邊的Controller資料夾上面右鍵,新增控制器

  • 選擇左側API,然後選API控制器-空白

  • 控制器取名為 OpenAIchatController.cs

  • 空的API控制器,如下圖所示,現在開始一步一步撰寫這個API controller

  • OpenAIchatController.cs的程式碼如下

    using Microsoft.AspNetCore.Mvc;
    using System.Text.Json;
    using OpenAI_API;
    using OpenAI_API.Models;
    using OpenAI_API.Chat;
    
    // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
    namespace chatbot_backend_api.Controllers
    {
        [Route("api/[controller]")]
        [ApiController]
        public class OpenAIchatController : ControllerBase
        {
            private readonly IConfiguration Configuration;
    
            public OpenAIchatController(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            // POST api/<OpenAIchatController>
            [HttpPost]
            public async Task<string> Post([FromBody] List<JsonElement> message_json)
            {
                string key = Configuration["openai_key"];
                OpenAIAPI api = new OpenAIAPI(key);
    
                List<ChatMessage> messages = new List<ChatMessage>();
                for (int i = 0; i < message_json.Count; ++i)
                {
                    if (message_json[i].TryGetProperty("role", out var role) && message_json[i].TryGetProperty("content", out var content))
                    {
                        // Process role and content
                        if (role.ToString() == "system")
                        {
                            messages.Add(new ChatMessage(ChatMessageRole.System, content.ToString()));
                        }
                        if (role.ToString() == "user")
                        {
                            messages.Add(new ChatMessage(ChatMessageRole.User, content.ToString()));
                        }
                        if (role.ToString() == "assistant")
                        {
                            messages.Add(new ChatMessage(ChatMessageRole.Assistant, content.ToString()));
                        }
                    }
                }
    
                var result = await api.Chat.CreateChatCompletionAsync(new ChatRequest()
                {
                    Model = Model.ChatGPTTurbo_1106,
                    Temperature = 0.1,
                    MaxTokens = 200,
                    Messages = messages
                });
                var reply = result.Choices[0].Message;
                return reply.TextContent.Trim();
            }   
        }
    }

    以下是OpenAIchatController.cs的詳細說明:

    • 再controller中新增 readonly 的 field Configuration 與建構子,當產生OpenAIchatController的時候,自動將組態設定檔Load到Configuration

      private readonly IConfiguration Configuration;
      
      public OpenAIchatController(IConfiguration configuration)
      {
          Configuration = configuration;
      }
    • 新增對應POST方法,處理使用者從網頁前端POST過來的資料。以下是controller函數的簽名,注意這個函數的簽名有async,是一個非同步的函數

      [HttpPost]
      public async Task<string> Post([FromBody] List<JsonElement> message_json)
      {
          return "";
      }
    • 我們預期網頁前端POST過來的資料,是一個JSON的 list,與OpenAI的 Chat Completions API的格式相同,格式如下:

      [
          {"role": "system", "content": "You are a helpful assistant."},
          {"role": "user", "content": "Who won the world series in 2020?"},
          {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
          {"role": "user", "content": "Where was it played?"}
      ]
      • 角色 "role" 包含

        • system : 通常讓GPT進行角色扮演設定
        • user : 使用者的提問
        • assistant : GPT的回應
      • 內容 "content"就是該對話的字串內容

    • 一開始傳入的json list命名為 message_json,透過專案中的appsettings.json檔案來提取 secret key,注意這裡的"openai_key"appsettings.json中設定的變數名稱對應。

      接著在利用string變數key來產生一個可以進行對話的OpenAIAPI物件

      string key = Configuration["openai_key"];            
      OpenAIAPI api = new OpenAIAPI(key);
    • JsonElementTryGetProperty 方法來透過key取得json中的內容,在進一步將對話的前後文List<JsonElement>,轉換成 chat completion 可以處理的格式 List<ChatMessage>

      List<ChatMessage> messages = new List<ChatMessage>();
      for (int i = 0; i < message_json.Count; ++i)
      {
          if (message_json[i].TryGetProperty("role", out var role) && message_json[i].TryGetProperty("content", out var content))
          {
              // Process role and content
              if (role.ToString() == "system")
              {
                  messages.Add(new ChatMessage(ChatMessageRole.System, content.ToString()));
              }
              if (role.ToString() == "user")
              {
                  messages.Add(new ChatMessage(ChatMessageRole.User, content.ToString()));
              }
              if (role.ToString() == "assistant")
              {
                  messages.Add(new ChatMessage(ChatMessageRole.Assistant, content.ToString()));
              }
          }
      }
    • 問題與前文的格式準備好了,只需要在呼叫 OpenAIAPI中的 Completion相關的非同步方法,來呼叫OpenAI即可

      var result = await api.Chat.CreateChatCompletionAsync(new ChatRequest()
      {
          Model = Model.ChatGPTTurbo_1106,
          Temperature = 0.1,
          MaxTokens = 200,
          Messages = messages
      });
      var reply = result.Choices[0].Message;

      一些 completion API相關的參數如下:

      • Model.ChatGPTTurbo_1106: 模型的編號

      • Temperature = 0.1: 模型的溫度,溫度越低產生的對話越死板

      • MaxTokens = 200,: 這裡我們限制最大回應的Tokens為200,希望API不要回應太長的文本,控制token的使用量

    • 最後再將回應整理好,去除空白之後回傳

      return reply.TextContent.Trim();
  • 另外還要設定與許跨域存取API,這樣才能在VueJS中呼叫在不同網域的後端(ASP .NET Core WebAPI)

    Program.cs中新增跨來源資源共用 Cross-Origin Resource Sharing (CORS)

    var builder = WebApplication.CreateBuilder(args);
    ....
    var app = builder.Build();
    // Enable CORS
    app.UseCors(c => c.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod());
    ....
    app.Run();

在Swagger上面測試後端 ASP .NET Core WebAPI

  • 按下F5執行,會跳出瀏覽器導向Swagger api的頁面

  • 在Swagger上測試API,並且貼上測試問對話的json的list格式

    [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Who won the world series in 2020?"},
        {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
        {"role": "user", "content": "Where was it played?"}
    ]
  • 收到API的回應,成功

參、前端

  • 這裡我們VueJS做為前端,首先要安裝Visual Studio Code與Node ,並且從網路上的vue chat bot專案抓下來修改

  • 首先下載 Node.js https://nodejs.org/en

    下載之後無腦下一步安裝即可

將AIChatbot 的 VUE專案跑起來

  • 下載AIChatbot專案,然後解壓縮到一個你記得的路徑

  • 用Visual Studio Code 開啟 aichat 資料夾(去你剛剛解壓縮的路徑),注意我們只用專案的前端部分來修改

  • 這裡要選Yes, I trust the authors

  • 接著打開Terminal panel

  • 在Visual Studio Code的Terminal中,在aichat路徑下使用npm install指令,安裝必要的vue開發相關的套件

  • 需要花一些時間來安裝,安裝成功畫面如下

  • npm run serve來執行專案,第一次編譯執行會花一些時間

  • 執行成功後,可以用瀏覽器訪問頁面http://localhost:8080/

  • 注意這邊輸入問題送出後會報錯,因為我們並沒有把後端API對接起來,這個AI Chatbot只有前端而已

修改AIChatbot專案,符合我們設計的ASP .NET Core WebAPI專案

  • 首先先設定proxy

    vue.config.js中設定proxy使VueJS前端,可以訪問不同源的 ASP .NET WebAPI

    注意這裡的7136port 需要試你ASP .NET WebAPI 執行時候的port來修改,會因為不同電腦執行而不同

    vue.config.js

    const { defineConfig } = require('@vue/cli-service')
    module.exports = defineConfig({
      transpileDependencies: true,
      devServer: {
        proxy: {
          '/api': {
            target: 'https://localhost:7136   ',
            changeOrigin: true,
            pathRewrite: {
              '^/api': "https://localhost:7136/api/OpenAIchat"
            }
          }
        }
      }
    })
  • 首先安裝 icon相關的套件,這裡要先用ctrl+c把正在執行的vue前端關掉,再安裝套件

    npm install primeicons
  • 修改src/components/ChatBox.vue

    先在<script> tag中引入 primeicons套件

    <script>
    import axios from 'axios';
    import 'primeicons/primeicons.css'
    ...
    </script>
  • <script>中重新設計vue中 data的資料 lien45 附近

    • currentMessage: 目前送出的問題
    • processing: 表示是否正在等待API回應
    • system_prompt: 表示用來讓Chat GPT 做角色扮演的系統訊息
    • messages: 表示目前要顯示在chat box中的對話紀錄,格式就是json的list格式
    export default {
      name: 'ChatBox',
      data() {
        return {
          currentMessage: '',
          system_prompt: {
            "role": "system",
            "content": "you are a helpful assiatant."
          },
          processing: false,
          messages: [],
        };
      },
  • 修改sumbit 按鈕,更換成icon顯示,大約在Line 15附近

    <div class="inputContainer">
      <input v-model="currentMessage" type="text" class="messageInput" :disabled="processing" placeholder="Please enter your question" @keyup.enter="sendMessage(currentMessage)" />
      <button :disabled="processing" @click="sendMessage(currentMessage)" class="askButton">
          <i class="pi pi-send" style="font-size: 1rem"></i>
      </button>
    </div>
  • 修改完成後,網頁的送出按鈕會變化

  • 重新設計sendMessage的非同步方法:

    async sendMessage(currentMessage) {
      if (currentMessage.trim().length === 0) {
      return
      }
      this.processing = true
      //update messages
      this.messages.push({
      role: 'user',
      content: currentMessage,
      });
    
      var submitted_messages = []
      submitted_messages.push(this.system_prompt) //Fristly, insert the system prompt 
      //only remember recently 7 content
      let remember_num = 7
      if (this.messages.length > remember_num) {
      for (let i = 0; i < remember_num; ++i) {
        submitted_messages[i + 1] = this.messages[this.messages.length - remember_num + i];
      }
      } else {
      for (let i = 0; i < this.messages.length; ++i) {
        submitted_messages[i + 1] = this.messages[i]
      }
      }
    
      this.messages.push({
      role: 'assistant',
      content: '',
      });
      await axios
      .post('https://localhost:7136/api/OpenAIchat', submitted_messages
      )
      .then((response) => {
        this.messages[this.messages.length - 1].content = response.data // Access the 'data' property of the response object
      }).catch(function (error) {
        console.log(error.toJSON());
      });
      this.currentMessage = ''
      this.processing = false
    } 

    以下是sendMessage函數的詳細說明:

    • 當目前的詢問問題currentMessage 是空白的時候,就直接retrun不浪費API的使用

      if (currentMessage.trim().length === 0) {
        return
      }
    • 設定成正在呼叫API的狀態,不讓使用者在繼續送出問題 disable button 與 input text

      this.processing = true
    • 然後在messages插入使用者目前詢問的問題

      //update messages
      this.messages.push({
        role: 'user',
        content: currentMessage,
      });
    • submitted_messages這個list中存放,要post到後端ASP .NET Core WebAPI的所有對話紀錄。

      這裡用一個迴圈做copy的動作,只取最近的7筆問答,也就是3組QA還有一個Q,不要忘記了最前面還要加上system prompt,也就是角色扮演的指令 ,總計有8個對話。

      submitted_messages.push(this.system_prompt) //Fristly, insert the system prompt 
      //only remember recently 7 content
      let remember_num = 7
      if (this.messages.length > remember_num) {
        for (let i = 0; i < remember_num; ++i) {
          submitted_messages[i + 1] = this.messages[this.messages.length - remember_num + i];
        }
      } else {
        for (let i = 0; i < this.messages.length; ++i) {
          submitted_messages[i + 1] = this.messages[i]
        }
      }
    • 最後在真正POST上去之前,先在要顯示在對話窗的 messages 中插入一個空白的回應

      this.messages.push({
        role: 'assistant',
        content: '',
      });
    • 這裡用axios做非同步的api呼叫,等待完成API呼叫後,將真正的Chat GPT的回應,寫入messages 的最後一的回應,也就是chat GPT的回應訊息,如果API呼叫失敗,我們的例外處理也會catch到錯誤的情況,在log中打印錯誤

      另外再提醒一下 https://localhost:7136/api/OpenAIchat這個 port 7136 每個人自己執行的時候號碼會不同,需將 port 7136 改成ASP .NET Core WebAPI執行時的url

      await axios
        .post('https://localhost:7136/api/OpenAIchat', submitted_messages
        )
        .then((response) => {
          this.messages[this.messages.length - 1].content = response.data // Access the 'data' property of the response object
        }).catch(function (error) {
          console.log(error.toJSON());
        });
      this.currentMessage = ''
      this.processing = false
  • 最後我們再來修改,顯示對話的部分,大約在Line 7 附近,把<template v-for="(message, index) in messages" :key="index"></template>中間都全部覆蓋掉

    <div :class="message.role == 'user'? 'messageFromUser' : 'messageFromChatGpt'">
      <div :class="(message.role == 'user' ||  message.content.trim().length !== 0)? 'userMessageWrapper' : 'chatGptMessageWrapper'">
        <div :class="message.role == 'user'  ? 'userMessageContent' : 'chatGptMessageContent'">
            {{ message.content }}
        </div>
      </div>
    </div>
  • 修改完成後,你可以重新用npm run serve 執行Vue.js專案,然後在網頁上面與Ai Chat Bot聊天測試

  • 到這裡你已經做出一個最簡單的聊天機器人了

    如果有錯誤,可以檢查一下sendMessage方法 與 vue.config.js中的URL是否正確,是否與跑起來的ASP .NET WebAPI 的URL一致

  • 最後如果想要完整程式碼專案,可以到這裡參考

    https://github.com/kung-bill/vueAIChatBot

Reference

img_show