# 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/) 申請一個帳號

並且建立一個 Messagin API Channel,這邊建立一個名叫 `《GP》CHAT` 的 LINE BOT(以下省略申請步驟...)

這裡需要記得三個密鑰:
1. Channel secret

2. Your user ID

3. Channel access token

最後透過 QR Code 來加入新建立的機器人吧

### 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,因此要將這些資訊放到主機中:

### 執行程式

### 串接程式與通道
在剛剛 LINE BOT 的頁面**點擊** Messaging API,並且找到 Webhook settings,**點擊** `Edit` 按鈕,將伺服器網址貼上

變按下 `Verify` 確認是否成功

只要能成功讓 LINE BOT 復述你的話就沒問題嘍

## 升級為 AI 聊天機器人
> 本節的 Replit 範例在 https://replit.com/@flagtech/GPTDevLINEbot
### 設定環境變數
這裡需附上 `OPENAI_API_KEY`

完整程式碼:
``` 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()
```


> 這邊**變化圖片**的功能與**文字生圖**的費用是一樣的
## 可控制變化內容的 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)
```

> 效果並不是很理想,嘖嘖
## 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 |