# Chapter8. 設計 LINE AI 聊天機器人 :+1: 完整程式碼在 https://github.com/iamalex33329/chatgpt-develop-guide-zhtw ## 其他章節 [Chapter1. OpenAI API 入門](https://hackmd.io/@U3f2IzHERbymAst2-lDdjA/S1cNMYi6T) [Chapter2. 使用 Python 呼叫 API](https://hackmd.io/@U3f2IzHERbymAst2-lDdjA/HyZBg5ia6) [Chapter3. API 參數解析與錯誤處理](https://hackmd.io/@U3f2IzHERbymAst2-lDdjA/BJWNtsh6p) [Chapter4. 打造自己的 ChatGPT](https://hackmd.io/@112356044/Hk81U96Tp) [Chapter5. 突破時空限制 - 整合搜尋功能](https://hackmd.io/@112356044/HkbVM-ApT) [Chapter6. 讓 AI 幫 AI - 自動串接流程](https://hackmd.io/@112356044/r1Ke-GR6T) [Chapter7. 網頁版聊天程式與文字生圖 Image API](https://hackmd.io/@112356044/Hyf-AvgAT) [Chapter9. 自媒體業者必看!使用 AI 自動生成高品質字幕](https://hackmd.io/@112356044/rJ2T37V0T) [Chapter10. 把 AI 帶到 Discord](https://hackmd.io/@112356044/Sy_L-B40T) [Chapter11. AI 客製化投資理財應用實戰](https://hackmd.io/@112356044/HkUE0rER6) [Chapter12. 用 LangChain 實作新書宣傳自動小編](https://hackmd.io/@112356044/SybvbdN0p) ## 目錄結構 [TOC] ## 設計簡易的 LINE 聊天機器人 > 本節的 Replit 範例在 https://replit.com/@flagtech/GPTDevLINE LINE BOT 主要由以下兩個部分構成: 1. Messaging API:負責作為 LINE 與開發者的中介來傳送訊息。 2. 後端 HTTP 伺服器:開發者負責撰寫程式,讓 Messaging API 可以轉送使用者的 LINE 訊息,以及透過 Messaging API 傳回回覆訊息到 LINE 機器人。 ### Messaging API 先到 [LINE Developers](https://developers.line.biz/en/) 申請一個帳號 ![Screenshot 2024-03-15 at 12.35.54 AM](https://hackmd.io/_uploads/SkvOwsx0p.png) 並且建立一個 Messagin API Channel,這邊建立一個名叫 `《GP》CHAT` 的 LINE BOT(以下省略申請步驟...) ![Screenshot 2024-03-15 at 12.41.36 AM](https://hackmd.io/_uploads/HJsadjx0T.png) 這裡需要記得三個密鑰: 1. Channel secret ![Screenshot 2024-03-15 at 12.46.32 AM](https://hackmd.io/_uploads/Hk7lcjgAp.png) 2. Your user ID ![Screenshot 2024-03-15 at 12.46.44 AM](https://hackmd.io/_uploads/H1JZcogAa.png) 3. Channel access token ![Screenshot 2024-03-15 at 12.45.31 AM](https://hackmd.io/_uploads/SJL3KoxAT.png) 最後透過 QR Code 來加入新建立的機器人吧 ![Screenshot 2024-03-15 at 12.49.28 AM](https://hackmd.io/_uploads/B1focilRp.png) ### Replit 線上開發環境 接著要撰寫與聊天機器人串接的後端程式,也就是可以處理 HTTP POST 方法的伺服器,而伺服器必須部署在公開網路上,因此我們採用: 1. 利用 Flask 框架作為伺服器程式撰寫 2. 使用 Replit 線上開發環境作為部署環境 我們可以到書本給的範例檔 https://replit.com/@flagtech/GPTDevLINE 來下載程式碼 ``` python= from flask import Flask, request, abort from linebot import LineBotApi, WebhookHandler from linebot.exceptions import InvalidSignatureError from linebot.models import ( MessageEvent, TextMessage, TextSendMessage, ) import os line_bot_api = LineBotApi(os.getenv('LINE_TOKEN')) handler = WebhookHandler(os.getenv('LINE_SECRET')) app = Flask(__name__) @app.post("/") def 處理回呼(): """ 處理 Line 平台的 Webhook 請求 """ # 取得 X-Line-Signature 標頭的電子簽章內容 簽章 = request.headers['X-Line-Signature'] # 以文字形式取得請求內容 請求內容 = request.get_data(as_text=True) app.logger.info("請求內容: " + 請求內容) # 檢查電子簽章並處理請求內容 try: handler.handle(請求內容, 簽章) except InvalidSignatureError: print("電子簽章錯誤,請檢查金鑰是否正確?") abort(400) return 'OK' @handler.add(MessageEvent, message=TextMessage) def 處理訊息事件(event): """ 處理收到的文字訊息事件 """ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=event.message.text)) if __name__ == "__main__": app.run(host='0.0.0.0', port=5000) ``` ### 設定環境變數 如同上述的程式碼,程式會使用**環境變數**來取得密鑰和 Token,因此要將這些資訊放到主機中: ![Screenshot 2024-03-15 at 1.02.50 AM](https://hackmd.io/_uploads/HJBT6sgRp.png) ### 執行程式 ![Screenshot 2024-03-15 at 1.04.04 AM](https://hackmd.io/_uploads/H1gfAoeRp.png) ### 串接程式與通道 在剛剛 LINE BOT 的頁面**點擊** Messaging API,並且找到 Webhook settings,**點擊** `Edit` 按鈕,將伺服器網址貼上 ![Screenshot 2024-03-15 at 1.09.02 AM](https://hackmd.io/_uploads/ByK412e0a.png) 變按下 `Verify` 確認是否成功 ![Screenshot 2024-03-15 at 1.09.38 AM](https://hackmd.io/_uploads/ryCI12gA6.png) 只要能成功讓 LINE BOT 復述你的話就沒問題嘍 ![image](https://hackmd.io/_uploads/BJdiy2lRp.png) ## 升級為 AI 聊天機器人 > 本節的 Replit 範例在 https://replit.com/@flagtech/GPTDevLINEbot ### 設定環境變數 這裡需附上 `OPENAI_API_KEY` ![Screenshot 2024-03-15 at 1.16.26 AM](https://hackmd.io/_uploads/B14g-hgCp.png) 完整程式碼: ``` python= from flask import Flask, request, abort from linebot import LineBotApi, WebhookHandler from linebot.exceptions import InvalidSignatureError from linebot.models import ( MessageEvent, TextMessage, TextSendMessage, ImageSendMessage) from flagchat import chat, func_table import openai import os api = LineBotApi(os.getenv('LINE_TOKEN')) handler = WebhookHandler(os.getenv('LINE_SECRET')) app = Flask(__name__) @app.post("/") def callback(): # 取得 X-Line-Signature 表頭電子簽章內容 signature = request.headers['X-Line-Signature'] # 以文字形式取得請求內容 body = request.get_data(as_text=True) app.logger.info("Request body: " + body) # 比對電子簽章並處理請求內容 try: handler.handle(body, signature) except InvalidSignatureError: print("電子簽章錯誤, 請檢查密鑰是否正確?") abort(400) return 'OK' def txt_to_img_url(prompt): response = openai.Image.create(prompt=prompt, n=1, size='1024x1024') return response['data'][0]['url'] func_table.append( { # 每個元素代表一個函式 "chain": False, # 生圖後不需要傳回給 API "func": txt_to_img_url, "spec": { # function calling 需要的函式規格 "name": "txt_to_img_url", "description": "可由文字生圖並傳回圖像網址", "parameters": { "type": "object", "properties": { "prompt": { "type": "string", "description": "描述要產生圖像內容的文字", } }, "required": ["prompt"], }, } } ) @handler.add(MessageEvent, message=TextMessage) def handle_message(event): for reply in chat('使用繁體中文的小助手', event.message.text): pass if reply.startswith('https://'): api.reply_message( event.reply_token, ImageSendMessage(original_content_url=reply, preview_image_url=reply)) else: api.reply_message(event.reply_token, TextSendMessage(text=reply)) if __name__ == "__main__": app.run(host='0.0.0.0', port=5000) ``` 並將剛剛的 Webhook 網址替換成新專案的網址,就能夠與你的 LINE BOT 進行對話嘍 ## OpenAI 變化圖像的功能 > 本節的 Replit 範例在 https://replit.com/@flagtech/GPTDevLINEvar ### 設定環境變數 > 這裡跟前面一樣(省略) 完整程式碼: ``` python= from flask import Flask, request, abort from linebot import LineBotApi, WebhookHandler from linebot.exceptions import InvalidSignatureError from linebot.models import ( MessageEvent, TextMessage, TextSendMessage, ImageMessage, ImageSendMessage) from PIL import Image from io import BytesIO from flagchat import chat, func_table import openai import os api = LineBotApi(os.getenv('LINE_TOKEN')) handler = WebhookHandler(os.getenv('LINE_SECRET')) app = Flask(__name__) @app.post("/") def callback(): # 取得 X-Line-Signature 表頭電子簽章內容 signature = request.headers['X-Line-Signature'] # 以文字形式取得請求內容 body = request.get_data(as_text=True) app.logger.info("Request body: " + body) # 比對電子簽章並處理請求內容 try: handler.handle(body, signature) except InvalidSignatureError: print("電子簽章錯誤, 請檢查密鑰是否正確?") abort(400) return 'OK' def txt_to_img_url(prompt): response = openai.Image.create(prompt=prompt, n=1, size='1024x1024') return response['data'][0]['url'] func_table.append({ "chain": False, # 生圖後不需要傳回給 API "func": txt_to_img_url, "spec": { # function calling 需要的函式規格 "name": "txt_to_img_url", "description": "可由文字生圖並傳回圖像網址", "parameters": { "type": "object", "properties": { "prompt": { "type": "string", "description": "描述要產生圖像內容的文字", } }, "required": ["prompt"], }, } }) def img_variation(): try: os.stat('image.png') except FileNotFoundError: return "你還沒上傳檔案" res = openai.Image.create_variation( image=open('image.png', 'rb'), n=1, size='1024x1024') return res['data'][0]['url'] func_table.append({ "chain": False, # 生圖後不需要傳回給 API "func": img_variation, "spec": { # function calling 需要的函式規格 "name": "img_variation", "description": "可變化已經上傳的圖像", "parameters": { "type": "object", "properties": {}, } } }) @handler.add(MessageEvent, message=TextMessage) def handle_message(event): for reply in chat('使用繁體中文的小助手', event.message.text): pass if reply.startswith('https://'): api.reply_message( event.reply_token, ImageSendMessage(original_content_url=reply, preview_image_url=reply)) else: api.reply_message(event.reply_token, TextSendMessage(text=reply)) @handler.add(MessageEvent, message=ImageMessage) def handle_image_message(event): message_id = event.message.id # 取得圖片訊息的 ID # 向 Line 請求圖片內容 message_content = api.get_message_content(message_id) content = message_content.content # 取得圖片的二進位內容 img = Image.open(BytesIO(content)) img.save('image.png', 'PNG') api.reply_message(event.reply_token, TextSendMessage(text="已儲存檔案")) if __name__ == "__main__": app.run(host='0.0.0.0', port=5000) ``` 由於這個程式只能接收**正方形**的圖片,且圖片大小**不能超過 4MB**,因此可以利用以下 gradio 製作的網頁,來調整圖片 ``` python= from rembg import remove # Import function for removing background from PIL import Image # Import pillow image manipulation module import gradio as gr def remove_bg(img, x_off, y_off, scale): x = int(x_off * img.width / 100) # Calculate horizontal shift in pixels y = int(y_off * img.height / 100) # Calculate vertical shift in pixels width = img.width - x # Calculate actual width height = img.height - y # Calculate actual height size = min(width, height) # Take the shorter dimension img = img.crop((x, y, x + size, y + size)) # Crop to square region if scale < 100: size = int(size * scale / 100) # Scale according to specified ratio img = img.resize((size, size)) img_nb = remove(img) # Remove background img.save('img.png') # Save image file img_nb.save('img_nb.png') # Save image with no background file return (img, img_nb) no_bg_if = gr.Interface( fn=remove_bg, inputs=[ gr.Image(label='Input Image', type='pil'), gr.Slider(label='Horizontal Cut Offset (%)'), gr.Slider(label='Vertical Cut Offset (%)'), gr.Slider(label='Scale Ratio', value=100, step=5) ], outputs=[gr.Gallery(label='Processed Image')] ) no_bg_if.launch() ``` ![Screenshot 2024-03-15 at 1.33.37 AM](https://hackmd.io/_uploads/SkAeB3l0p.png) ![image](https://hackmd.io/_uploads/SyZaS2gAa.png) > 這邊**變化圖片**的功能與**文字生圖**的費用是一樣的 ## 可控制變化內容的 create_edit 函式 OpenAI 的 Image API 有提供另一個 create_edit 函式,可以根據文字描述來修改圖像,主要參數為: 1. image:這必須是一張透明背景的 PNG 檔圖片,透明區域是模型根據文字修改的部分,非透明部分不會變動 2. prompt:針對透明部分生成內容的描述 ### 讓 LINE 也能變化圖像背景 > 本節的 Replit 範例在 https://replit.com/@flagtech/GPTDevLINEedit ### 設定環境變數 > 這裡跟前面一樣(省略) 完整程式碼: ``` python= from flask import Flask, request, abort from linebot import LineBotApi, WebhookHandler from linebot.exceptions import InvalidSignatureError from linebot.models import ( MessageEvent, TextMessage, TextSendMessage, ImageMessage, ImageSendMessage) from PIL import Image import numpy as np from PIL import ImageDraw from io import BytesIO from flagchat import chat, func_table import openai import os api = LineBotApi(os.getenv('LINE_TOKEN')) handler = WebhookHandler(os.getenv('LINE_SECRET')) app = Flask(__name__) @app.post("/") def callback(): # 取得 X-Line-Signature 表頭電子簽章內容 signature = request.headers['X-Line-Signature'] # 以文字形式取得請求內容 body = request.get_data(as_text=True) app.logger.info("Request body: " + body) # 比對電子簽章並處理請求內容 try: handler.handle(body, signature) except InvalidSignatureError: print("電子簽章錯誤, 請檢查密鑰是否正確?") abort(400) return 'OK' def txt_to_img_url(prompt): response = openai.Image.create(prompt=prompt, n=1, size='1024x1024') return response['data'][0]['url'] func_table.append({ "chain": False, # 生圖後不需要傳回給 API "func": txt_to_img_url, "spec": { # function calling 需要的函式規格 "name": "txt_to_img_url", "description": "可由文字生圖並傳回圖像網址", "parameters": { "type": "object", "properties": { "prompt": { "type": "string", "description": "描述要產生圖像內容的文字", } }, "required": ["prompt"], }, } }) def img_variation(): try: os.stat('image.png') except FileNotFoundError: return "你還沒上傳檔案" res = openai.Image.create_variation( image=open('image.png', 'rb'), n=1, size='1024x1024') return res['data'][0]['url'] func_table.append({ "chain": False, # 生圖後不需要傳回給 API "func": img_variation, "spec": { # function calling 需要的函式規格 "name": "img_variation", "description": "可變化已經上傳的圖像", "parameters": { "type": "object", "properties": {}, } } }) def img_edit(prompt): try: os.stat('image_nb.png') except FileNotFoundError: return "你還沒上傳檔案" res = openai.Image.create_edit( prompt=prompt, image=open('image_nb.png', 'rb'), n=1, size='1024x1024') return res['data'][0]['url'] func_table.append({ "chain": False, # 生圖後不需要傳回給 API "func": img_edit, "spec": { # function calling 需要的函式規格 "name": "img_edit", "description": "可依照文字描述修改上傳圖像並傳回圖像網址", "parameters": { "type": "object", "properties": { "prompt": { "type": "string", "description": "描述要修改圖像內容的文字", } }, "required": ["prompt"], }, } }) @handler.add(MessageEvent, message=TextMessage) def handle_message(event): for reply in chat('使用繁體中文的小助手', event.message.text): pass if reply.startswith('https://'): api.reply_message( event.reply_token, ImageSendMessage(original_content_url=reply, preview_image_url=reply)) else: api.reply_message(event.reply_token, TextSendMessage(text=reply)) def convert_transparent(img): # 轉換成有透明通道的圖像 img = img.convert("RGBA") width, height = img.size pixdata = np.array(img) # 將像素轉成 numpy 陣列 # 取得圖像最上方邊緣顏色的平均值, 如果這是去背圖在轉成 JPG 檔 # 那最邊緣應該都是被轉成同樣顏色 (通常是白色或黑色) 的背景 bg_color = tuple(np.average(pixdata[0,:,:], axis=0).astype(int)) # 建立用來執行 flood fill 演算法的遮罩 mask = Image.new('L', (width + 2, height + 2), 0) # 在遮罩上執行 flood fill ImageDraw.floodfill(mask, (0, 0), 255) # 建立新圖像資料 new_data = [] for y in range(height): for x in range(width): # 如果是遮罩範圍內的點而且是與背景同色 # 把這個點變更為透明 if (mask.getpixel((x+1, y+1)) == 255 and pixdata[y, x, :3].tolist() == list(bg_color[:3])): new_data.append((255, 255, 255, 0)) else: new_data.append(tuple(pixdata[y, x])) # 建立具有透明通道的新圖像 img_transparent = Image.new('RGBA', img.size) img_transparent.putdata(new_data) return img_transparent @handler.add(MessageEvent, message=ImageMessage) def handle_image_message(event): message_id = event.message.id # 取得圖片訊息的 ID # 向 Line 請求圖片內容 message_content = api.get_message_content(message_id) content = message_content.content # 取得圖片的二進位內容 img = Image.open(BytesIO(content)) img.save('image.png', 'PNG') img_nb = convert_transparent(img) img_nb.save('image_nb.png', 'PNG') api.reply_message(event.reply_token, TextSendMessage(text="已儲存檔案")) if __name__ == "__main__": app.run(host='0.0.0.0', port=5000) ``` ![Screenshot 2024-03-15 at 1.46.27 AM](https://hackmd.io/_uploads/S1Rlu2lC6.png) > 效果並不是很理想,嘖嘖 ## Fork 專案,填上需要的 key 以及替換 Webhook 網址,即可使用 | 功能 | 網址 | | -------- | -------- | | 基本Echo | https://replit.com/@112356044/GPTDevLINE | | LINE版 ChatGPT | https://replit.com/@112356044/GPTDevLINEbot | | 變化圖片樣式 | https://replit.com/@112356044/GPTDevLINEvar | | 變化圖片背景 | https://replit.com/@112356044/GPTDevLINEedit |