在這24天鐵人賽中,我們經常需要測試多種對話情境。但每次重新啟動APP後,之前所有的對話都消失,必須重新開始。為了解決這一問題,今天我們打算實現歷史對話儲存功能。我們將採用Capacitor SQLite來實現。此外,我們還將設計一個聊天室架構,使我們能更組織性的管理和分類每次的對話。讓我們開始今天的挑戰吧!
SQLite 是一款輕量級的資料庫系統,它存儲的數據不需要一個獨立的伺服器或系統,而是以檔案形式存在於本地。而Capacitor SQLite套件則是為Capacitor提供了對SQLite數據庫的存取能力。
我們可以透過命令安裝Capacitor SQLite套件,它是一個插件,因此必須使用sync指令來同步Android和iOS的專案:
npm i @capacitor-community/sqlite
npx cap sync
我們新增一個SQLiteDB Service,在sqlitedb.service.ts
檔案中,準備以下物件:
//SQLite連線
private sqlite: SQLiteConnection = new SQLiteConnection(CapacitorSQLite);
//數據庫連接物件
private db!: SQLiteDBConnection;
//聊天室清單的觀察者
private chatSubject$: BehaviorSubject<ChatRoomSQLiteModel[]> = new BehaviorSubject<ChatRoomSQLiteModel[]>([]);
//歷史對話的觀察者
private chatHistorySubject$: BehaviorSubject<ChatHistorySQLiteModel[]> = new BehaviorSubject<ChatHistorySQLiteModel[]>([]);
get chat$() {
return this.chatSubject$.asObservable();
}
get chatHistory$() {
return this.chatHistorySubject$.asObservable();
}
我們將定義資料庫的名稱以及聊天室和歷史對話的資料表結構。在chatroomdb
資料表中,有一個selecting
的欄位,其用途是確定是否已選中該聊天室。但由於SQLite本身不支持boolean型別,因此改用整數0(代表未選中)和1(代表已選中)來表示。另外,chathistorydb
資料表中有一個chatroomid
欄位,其用途是連結到相對應的chatroomdb
資料表:
//定義DB名稱
const DB_NAME = 'aiconversationdb'
//定義聊天室資料表結構
const chatRoomSchema = `
CREATE TABLE IF NOT EXISTS chatroomdb (
chatroomid INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
selecting INTEGER DEFAULT 0,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
`;
//定義歷史對話資料表結構
const chatHistorySchema = `
CREATE TABLE IF NOT EXISTS chathistorydb (
chathistoryid INTEGER PRIMARY KEY AUTOINCREMENT,
chatroomid INTEGER NOT NULL,
role TEXT NOT NULL,
content TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(chatroomid) REFERENCES chatroomdb(chatroomid)
);
`;
準備一個sqlite.model.ts
檔案,並依照DB結構定義聊天室和歷史對話資料表的Model:
export interface ChatRoomSQLiteModel {
chatroomid: number;
name: string;
selecting: boolean;
timestamp: Date;
}
export interface ChatHistorySQLiteModel {
chathistoryid: number;
chatroomid: number;
role: string;
content: string;
timestamp: Date;
}
回到sqlitedb.service.ts
檔案中,建立initializePlugin()
初始化方法。該方法會建立資料庫連線,並設置資料表、檢查和新增一些初始資料。最後,將資料載入我們先前定義的chat$
和chatHistory$
中:
public async initializePlugin() {
try {
//創建和打開資料庫連接
this.db = await this.sqlite.createConnection(DB_NAME, false, 'no-encryption', 1, false);
await this.db.open();
//執行數據表建立
await this.db.execute(chatRoomSchema);
await this.db.execute(chatHistorySchema);
//確保至少存在一個聊天室
await this.ensureAtLeastOneChatRoom();
//讀取聊天室和歷史對話資料
await this.loadChatRoomData();
await this.loadChatHistoryData();
} catch (error) {
console.error("Error initializing plugin:", error);
}
}
private async ensureAtLeastOneChatRoom() {
try {
//確定聊天室資料表中是否至少有一筆資料
const chatCount = await this.db.query('SELECT COUNT(*) as count FROM chatroomdb');
//若沒有任何聊天室資料,則建立一個初始的聊天室
if (chatCount.values && chatCount.values[0].count === 0) {
await this.addInitialChatRoom();
}
} catch (error) {
console.error("Error ensuring at least one chat room:", error);
}
}
private async addInitialChatRoom() {
try {
//建立一個初始的聊天室資料
const query = 'INSERT INTO chatroomdb (name, selecting) VALUES (?, ?)';
const values = ['英文口說聊天室', 1];
await this.db.query(query, values);
} catch (error) {
console.error("Error adding initial chat room:", error);
}
}
private async loadChatRoomData() {
try {
//讀取所有聊天室清單資料
const chatData = await this.db.query('SELECT * FROM chatroomdb ORDER BY timestamp');
this.chatSubject$.next(chatData.values || []);
} catch (error) {
console.error("Error loading chat data:", error);
}
}
private async loadChatHistoryData() {
try {
//只讀取當前選中的聊天室的歷史對話資料
const chatHistoryData = await this.db.query('SELECT * FROM chathistorydb JOIN chatroomdb ON chathistorydb.chatroomid = chatroomdb.chatroomid WHERE chatroomdb.selecting = 1 ORDER BY chathistorydb.timestamp');
this.chatHistorySubject$.next(chatHistoryData.values || []);
} catch (error) {
console.error("Error loading chat history data:", error);
}
}
再來新增一個addChatHistory()
方法,其主要功能是添加每次的歷史對話。該方法會找到當前選中的chatroomid
,再將其新增至歷史對話資料表中。完成後,會再次刷新存儲歷史對話的chatHistory$
:
public async addChatHistory(roleData: ChatRole, contentData: string) {
try {
console.log(`新增聊天資料:${roleData}, ${contentData}`);
//查詢當前選中的聊天室ID
const chatIdQuery = 'SELECT chatroomid FROM chatroomdb WHERE selecting = 1';
const chatIdResult = await this.db.query(chatIdQuery);
if (!chatIdResult.values || chatIdResult.values.length === 0) {
console.error('No selecting chat found');
return;
}
const chatRoomId = chatIdResult.values[0].chatroomid;
//新增對話內容
const query = 'INSERT INTO chathistorydb (chatroomid, role, content) VALUES (?, ?, ?)';
const values = [chatRoomId, roleData, contentData];
const result = await this.db.query(query, values);
await this.loadChatHistoryData();
console.log('新增完成');
return result;
} catch (error) {
console.error("Error adding chat history:", error);
return;
}
}
最後在app.component.ts
檔案中的constructor()
,執行initializePlugin()
來初始化SQLite的相關設定:
constructor(private sqlitedbService: SqlitedbService) {
this.sqlitedbService.initializePlugin();
}
在【Day - 14】的設計,所有的歷史對話都是儲存在chatMessages
變數中。但現在需要將所有的資料來源切換為SQLiteDB Service。因此,我們需要對OpenAI Service的相關程式碼進行一些調整。
首先,在openai.service.ts
檔案中,我們調整【Day - 15】時所建立的tokenizerCalcuation()
方法。此次的調整主要是將新的對話和資料源都轉換為從方法參數中獲取,並重新調整計算的方式:
private tokenizerCalcuation(newContent: string, chatHistorySQLiteData: ChatHistorySQLiteModel[]): ChatMessageModel[] {
//最大大小取決於你選用的模型
const MAX_TOKENS = 8192;
let totalTokenizer = 0;
let newChatMessage: ChatMessageModel[] = [];
//系統訊息Token數量計算
const systemPromptToken = encode(SYSTEMPROMPT).length;
totalTokenizer += systemPromptToken;
//新的使用者對話Token數量計算
const newContentToken = encode(newContent).length;
totalTokenizer += newContentToken;
for (let i = chatHistorySQLiteData.length - 1; i >= 0; i--) {
const conversationPromptToken = encode(chatHistorySQLiteData[i].content).length;
if (totalTokenizer + conversationPromptToken > MAX_TOKENS) {
//如果下一個message超過Token限制,就結束添加
break;
} else {
totalTokenizer += conversationPromptToken;
newChatMessage.unshift({
role: chatHistorySQLiteData[i].role as ChatRole,
content: chatHistorySQLiteData[i].content
});
}
}
//添加系統訊息在第0筆
newChatMessage.unshift({
role: 'system',
content: SYSTEMPROMPT
});
//添加使用者對話在最後一筆
newChatMessage.push({
role: 'user',
content: newContent
});
console.log(newChatMessage);
console.log(`Token: ${totalTokenizer}`);
return newChatMessage;
}
接著是【Day - 20】的getConversationRequestData()
方法,一樣將來源改成參數中獲取:
private getConversationRequestData(chatMessageData: ChatMessageModel[]): ChatRequestModel {
return {
model: 'gpt-3.5-turbo',
messages: chatMessageData,
temperature: 0.7,
top_p: 1,
functions: [
.
.
.
.
],
function_call: {
name: 'getEnglishTurtorConverstaionData'
}
}
}
最後,我們調整【Day - 14】中的chatAPI()
方法,將其資料源更換為SQLiteDB Service。再透過map
Operator進行資料的轉換,而在整個管道的尾端加入tap
Operator將資料儲存回SQLite內:
public chatAPI(contentData: string) {
return this.sqlitedbService.chatHistory$.pipe(
take(1),
map(chatHistoryResult => this.tokenizerCalcuation(contentData, chatHistoryResult)),
map(chatMessageResult => this.getConversationRequestData(chatMessageResult)),
switchMap(conversationRequestResult => this.http.post<ChatResponseModel>('https://api.openai.com/v1/chat/completions', conversationRequestResult, { headers: this.headers })),
map(chatAPIResult => JSON.parse(chatAPIResult.choices[0].message.function_call?.arguments!) as ConversationDataModel),
tap(conversationDataResult => {
console.log('儲存資料');
this.sqlitedbService.addChatHistory('user', contentData);
this.sqlitedbService.addChatHistory('assistant', conversationDataResult.gptResponseText);
})
);
}
經過在實體機上的測試,當我先和AI進行了一輪對話後再重新啟動APP,從Log紀錄中可以明確的看到我之前的歷史對話被保存了下來。而從SQLiteDB Service中的addChatHistory()
方法的Log也可以確認SQLite DB的功能是正常的。這證明了我們成功的實現了歷史對話的儲存功能。
今天我們運用了Capacitor SQLite套件,成功的完成從資料庫連線的建立、資料表的設計,到資料的操作和串接。另外對聊天室的資料結構也已經有了明確的規劃。在接下來的幾天,我們將逐步實作類似ChatGPT的聊天室和對話記錄功能,進一步豐富APP的功能和使用體驗哦!
Github專案程式碼:Ionic結合ChatGPT - Day25