Line Chat Bot 實作 - 使用 Line Message API 及 Google DialogFlow

由於最近上面的大老闆對AI的議題非常感興趣,身為一個不專業的「高級打工仔」自然而然被指派研究AI相關的議題

既然要研究AI,那就從比較好入手的Chat Bot開始吧

以下紀錄Chat Bot的開發與架設過程:

Step 1 至Line Developer頁面https://developers.line.me/en/ 申請Line@測試帳號,申請完成後會出現以下畫面

Step 1.1 Channel ID、Channel Secret、Channel access token 及 Webhock URL 會在開發Chat Bot時用到

Step 2 Dialogflow 帳號申請,連結: https://dialogflow.com

Step 3 在Dialogflow上建立Agent,此部份請參考連結: https://tinyurl.com/y9h4lstr

Step 4 進行Chat Bot 開發

Step 4.1 程式架構

Maven POM

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.foxlink</groupId>
  <artifactId>FoxlinkChatBot</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
    <dependencies>
  	<!-- Line SDK -->
  	<dependency>
  		<groupId>com.linecorp.bot</groupId>
    		<artifactId>line-bot-api-client</artifactId>
    		<version>1.11.0</version>
  	</dependency>
  	<dependency>
  		<groupId>com.linecorp.bot</groupId>
    		<artifactId>line-bot-model</artifactId>
    		<version>1.11.0</version>
  	</dependency>
  	<!-- log -->
  	<dependency>
		<groupId>log4j</groupId>
		<artifactId>log4j</artifactId>
		<version>1.2.17</version>
	</dependency>
	<dependency>
    		<groupId>com.googlecode.json-simple</groupId>
    		<artifactId>json-simple</artifactId>
    		<version>1.1.1</version>
	</dependency>
	<!-- Spring Framework -->
	<dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.2.5.RELEASE</version>
    </dependency>
    <dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-web</artifactId>
		<version>4.2.5.RELEASE</version>
	</dependency>
	<dependency>
  		<groupId>org.springframework</groupId>
  		<artifactId>spring-aop</artifactId>
  		<version>4.2.5.RELEASE</version>
  	</dependency>
  	<dependency>
  		<groupId>org.springframework</groupId>
  		<artifactId>spring-webmvc</artifactId>
  		<version>4.2.5.RELEASE</version>
  	</dependency>

	<dependency>
    		<groupId>org.springframework</groupId>
    		<artifactId>spring-jdbc</artifactId>
    		<version>4.2.5.RELEASE</version>
	</dependency>
	<!-- JSON  -->
	<dependency>
    		<groupId>com.google.code.gson</groupId>
    		<artifactId>gson</artifactId>
    		<version>2.7</version>
	</dependency>
	<dependency>
  		<groupId>javax.servlet</groupId>
  		<artifactId>jstl</artifactId>
  		<version>1.2</version>
  	</dependency>
  	<!-- lombok -->
  	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.16.16</version>
		<scope>provided</scope>
	</dependency>
  	<!-- Jackson-databind -->
	<dependency>
    		<groupId>com.fasterxml.jackson.core</groupId>
    		<artifactId>jackson-databind</artifactId>
    		<version>2.9.2</version>
	</dependency>

	<dependency>
    		<groupId>com.fasterxml.jackson.datatype</groupId>
    		<artifactId>jackson-datatype-jsr310</artifactId>
    		<version>2.9.2</version>
	</dependency>
	
	<dependency>
    		<groupId>com.fasterxml.jackson.core</groupId>
    		<artifactId>jackson-annotations</artifactId>
    		<version>2.9.2</version>
	</dependency>

	<dependency>
    		<groupId>com.google.guava</groupId>
    		<artifactId>guava</artifactId>
    		<version>23.2-jre</version>
	</dependency>

	<dependency>
    		<groupId>org.projectlombok</groupId>
    		<artifactId>lombok</artifactId>
    		<version>0.10.1</version>
    		<scope>provided</scope>
	</dependency>
	
	<dependency>
    		<groupId>commons-digester</groupId>
    		<artifactId>commons-digester</artifactId>
    		<version>2.1</version>
	</dependency>
	
	<dependency>
    		<groupId>ai.api</groupId>
    		<artifactId>libai</artifactId>
    		<version>1.6.12</version>
	</dependency>

	<dependency>
    		<groupId>mysql</groupId>
    		<artifactId>mysql-connector-java</artifactId>
    		<version>6.0.6</version>
	</dependency>
	
	<dependency>
    		<groupId>javax.servlet</groupId>
    		<artifactId>servlet-api</artifactId>
    		<version>2.5</version>
    		<scope>provided</scope>
	</dependency>
	
	<dependency>
    		<groupId>org.apache.tomcat</groupId>
    		<artifactId>tomcat-util</artifactId>
    		<version>8.5.23</version>
	</dependency>
	
	<dependency>
    		<groupId>commons-io</groupId>
    		<artifactId>commons-io</artifactId>
    		<version>2.6</version>
	</dependency>
	<!-- Oracle JDBC -->
	<dependency>
    		<groupId>com.oracle</groupId>
    		<artifactId>ojdbc6</artifactId>
    		<version>11.2.0</version>
	</dependency>
	
	<dependency>
    		<groupId>org.slf4j</groupId>
    		<artifactId>slf4j-api</artifactId>
    		<version>1.7.7</version>
	</dependency>
	
	<dependency>
    		<groupId>org.slf4j</groupId>
    		<artifactId>slf4j-simple</artifactId>
    		<version>1.7.7</version>
    		<scope>test</scope>
	</dependency>
	
	<dependency>
    		<groupId>javax.mail</groupId>
    		<artifactId>mail</artifactId>
    		<version>1.4.7</version>
	</dependency>
  </dependencies>
  <build>
  	<resources>
      <resource>
        <directory>src</directory>
        <includes>
          <include>Beans.xml</include>
          <include>log4j.xml</include>
        </includes>
      </resource>
    </resources>
    <sourceDirectory>src</sourceDirectory>
    <plugins>
      <plugin>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.0.0</version>
        <configuration>
          <warSourceDirectory>WebContent</warSourceDirectory>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Step 4.2 Controller建立
特別注意,在Line Developer文件中有提到:

Response

Your server should return the status code 200 for a HTTP POST request sent by a webhook.

package com.foxlink.chatbot.controller;

import java.util.Iterator;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.ServletContextAware;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import ai.api.AIConfiguration;
import ai.api.AIDataService;
import ai.api.model.AIRequest;
import ai.api.model.AIResponse;

@Controller
public class CallbackController implements ServletContextAware {
	private String DialogflowToken,LineID,LineSecret,LineToken;
	private static Logger logger = Logger.getLogger(CallbackController.class);
	
	@RequestMapping(value="/callback",method=RequestMethod.POST)
	public ResponseEntity handleLineCallBack(HttpServletRequest request, HttpServletResponse response) {
		String replyToken=null;
		LineMessageUtils lineUtils=new LineMessageUtils();
		List<LineClientMsg> LineResponseParams=null;
		try {
			request.setCharacterEncoding("UTF-8");
			LineResponseParams=lineUtils.FilterLineClient2ServerMsg(request, LineSecret);
			logger.info("Line Message from Client: "+LineResponseParams);
			System.out.println("Line Message from Client: "+LineResponseParams);
			Iterator<LineClientMsg> ClientMsgIterator=LineResponseParams.iterator();
			AiUtils aiUtil=new AiUtils(this.DialogflowToken);
			while(ClientMsgIterator.hasNext()) {
				LineClientMsg clientMsg=ClientMsgIterator.next();
				replyToken=clientMsg.getReplyToken();
				System.out.println("Message from Client: "+clientMsg.getMsgsFromClient());
				AiResponse AiAgentResponse=aiUtil.filterAiResponseMsg(clientMsg.getMsgsFromClient());
				String[] systemName=AiAgentResponse.getIntentName().split("-");
				logger.info("Ai Intent : "+AiAgentResponse.getIntentName()+" System Name: "+systemName[0]);
				System.out.println("Ai Intent : "+AiAgentResponse.getIntentName()+" System Name: "+systemName[0]);
				IAiService aiService=null;
				JsonArray MsgArray=null;
				JsonObject Msg2LineServer=null;
				switch (systemName[0]) {
					case "SFC":
						aiService=new SFCAi();
						break;
					case "RealTime":
						aiService=new RealTimeAi();
						break;
					case "ERP":
						aiService=new ERPAi();
						break;
					case "Agentflow":
						aiService=new AgentflowAi();
						break;
					case "Notes":
						aiService=new NotesAi();
						break;
					case "BI":
						aiService=new BIAi();
						break;
					case "PLM":
						aiService=new PLMAi();
						break;
					default:
						break;
				}
				
				if(aiService !=null) {
					//有找到對應的AI Service
					aiService.setParameters(AiAgentResponse.getParameters(),AiAgentResponse.getIntentName());
					MsgArray=lineUtils.GenerateMsgArray(aiService.SendSingleResponseToClient(systemName[1]));
					logger.info("Message to line client: "+MsgArray);
				}
				else {
					JsonObject noAiFound=new JsonObject();
					MsgArray=new JsonArray();
					noAiFound.addProperty("type", "text");
					if(systemName[0].equals("SAY_HELLO")) {
						noAiFound.addProperty("text", "Hello 您好~有任何問題都能問我");
					}
					else {
						noAiFound.addProperty("text", "我不太懂您的意思,請再輸入一次");
					}
					MsgArray.add(noAiFound);
				}
				Msg2LineServer=lineUtils.GenerateLineReplyClientMsg(replyToken, MsgArray);
				logger.info("Message to Line Server: "+Msg2LineServer);
				System.out.println("Message to Line Server: "+Msg2LineServer+" , and reply token: "+replyToken);
				lineUtils.ReplyLineMsg2Client(replyToken, LineToken, Msg2LineServer);
			}
		}
		catch(Exception ex) {
			logger.error("error",ex);
			System.out.println(ex);
		}
		return new ResponseEntity(HttpStatus.OK);
	}
}

Step 4.3 實作Line Utils,這邊列出接收並解析訊息的function 及 回傳訊息至Client的function

package com.foxlink.chatbot.Utils;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

public class LineMessageUtils {
	private static Logger logger=Logger.getLogger(LineMessageUtils.class);
	public void ReplyLineMsg2Client(String replyToken,String LineAccessToken,JsonObject Message2Client) {
		try {
			HttpHeaders headers=new HttpHeaders();
			headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
			headers.add("Authorization", "Bearer {"+LineAccessToken+"}");
			HttpEntity<String> entity=new HttpEntity<String>(Message2Client.toString(), headers);
			//Send Request and Parse Result
			RestTemplate restTemplate=new RestTemplate();
			ResponseEntity<String> lineServerResponse = restTemplate
					  .exchange("https://api.line.me/v2/bot/message/reply", HttpMethod.POST, entity, String.class);
			if(lineServerResponse.getStatusCode()==HttpStatus.OK) {
				logger.info("Send Message to Line Server is success.");
			}
			else {
				logger.info("Send Message to Line Server is failed, due to: "+lineServerResponse.getStatusCode()+",  "+lineServerResponse.getBody());
			}
		}
		catch(Exception ex) {
			logger.error("ReplyLineMsg2Client failed ",ex);
			ex.printStackTrace();
		}
	}
	
	public JsonObject GenerateLineReplyClientMsg(String LineReplyToken,JsonArray Reply2ClientMsgs) {
		JsonObject Msg2Client=new JsonObject();
		try {
			Msg2Client.addProperty("replyToken", LineReplyToken);
			Msg2Client.add("messages", Reply2ClientMsgs);
		}
		catch(Exception ex) {
			System.out.println("GenerateLineReplyClientMsg Failed , due to: "+ex.toString());
			ex.printStackTrace();
		}
		return Msg2Client;
	}
	
	public List<LineClientMsg> FilterLineClient2ServerMsg(HttpServletRequest request,String lineChannelSecret){
		List<LineClientMsg> LineClientMsgs=null;
		String httpRequestBody=null;
		try {
			LineClientMsgs=new ArrayList<LineClientMsg>();
			InputStream inputStream=request.getInputStream();
			BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
			httpRequestBody=IOUtils.toString(reader);
			JsonParser parser=new JsonParser();
			JsonObject lineMsg=(JsonObject) parser.parse(httpRequestBody);
			JsonArray lineEvents=lineMsg.get("events").getAsJsonArray();
			Iterator<JsonElement> lineEventIterator=lineEvents.iterator();
			while(lineEventIterator.hasNext()) {
				LineClientMsg lineClientMsg=new LineClientMsg();
				JsonElement lineEvent=lineEventIterator.next();
				lineClientMsg.setReplyToken(lineEvent.getAsJsonObject().get("replyToken").getAsString());
				String msgType=lineEvent.getAsJsonObject().get("message").getAsJsonObject().get("type").getAsString();
				if(msgType.equals("text")) {
					lineClientMsg.setMsgsFromClient(lineEvent.getAsJsonObject().get("message").getAsJsonObject().get("text").getAsString());
				}
				LineClientMsgs.add(lineClientMsg);
			}
			/*Decrypt*/
			SecretKeySpec key = new SecretKeySpec(lineChannelSecret.getBytes(), "HmacSHA256");
			Mac mac = Mac.getInstance("HmacSHA256");
			mac.init(key);
			byte[] source = httpRequestBody.getBytes("UTF-8");
		}
		catch(Exception ex) {
			ex.printStackTrace();
		}
		return LineClientMsgs;
	}
}

Step 4.4 實作Dialogflow 的 Utils,列出解析Dialogflow回傳的訊息

package com.foxlink.chatbot.Utils;

import java.util.HashMap;

import com.google.gson.JsonElement;
import ai.api.AIConfiguration;
import ai.api.AIDataService;
import ai.api.model.AIRequest;
import ai.api.model.AIResponse;

public class AiUtils {
	private String DialogFlowToken;
	
	public AiUtils(String AiToken) {
		this.DialogFlowToken=AiToken;
	}
	
	public AiResponse filterAiResponseMsg(String msg2AiAgent) {
		AIConfiguration config=null;
		AIDataService dataService=null;
		AIRequest request=null;
		AIResponse response=null;
		AiResponse responseContent=null;
		try {
			config=new AIConfiguration(DialogFlowToken);
			dataService=new AIDataService(config);
			request=new AIRequest(msg2AiAgent);
			request.setLanguage("zh-TW");
			response=dataService.request(request);
			
			if(response.getStatus().getCode()==200) {
				String intent=response.getResult().getMetadata().getIntentName();
				HashMap<String,JsonElement> parameters=response.getResult().getParameters();
				responseContent=new AiResponse();
				responseContent.setIntentName(intent);
				responseContent.setParameters(parameters);
				System.out.println("AI Response Content: "+responseContent);
			}
			else {
				System.err.println(response.getStatus().getErrorDetails());
			}
		}
		catch(Exception ex) {
			ex.printStackTrace();
		}
		return responseContent;
	}
}

Step 5 實際測試

以上,就是簡易版本的Line Chat Bot 結合Dialogflow AI的作法,提供給大家參考