iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0
Mobile Development

Ionic結合ChatGPT - 30天打造AI英語口說導師APP系列 第 25

【Day - 25】Capacitor SQLite - 儲存歷史對話內容

  • 分享至 

  • xImage
  •  

在這24天鐵人賽中,我們經常需要測試多種對話情境。但每次重新啟動APP後,之前所有的對話都消失,必須重新開始。為了解決這一問題,今天我們打算實現歷史對話儲存功能。我們將採用Capacitor SQLite來實現。此外,我們還將設計一個聊天室架構,使我們能更組織性的管理和分類每次的對話。讓我們開始今天的挑戰吧!

Capacitor SQLite套件

SQLite 是一款輕量級的資料庫系統,它存儲的數據不需要一個獨立的伺服器或系統,而是以檔案形式存在於本地。而Capacitor SQLite套件則是為Capacitor提供了對SQLite數據庫的存取能力。

我們可以透過命令安裝Capacitor SQLite套件,它是一個插件,因此必須使用sync指令來同步Android和iOS的專案:

npm i @capacitor-community/sqlite
npx cap sync

 

新增SQLiteDB Service

我們新增一個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();
}

 

調整OpenAI Service

在【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的功能是正常的。這證明了我們成功的實現了歷史對話的儲存功能。
https://ithelp.ithome.com.tw/upload/images/20230925/20161663SWCi4UDroG.png

結語

今天我們運用了Capacitor SQLite套件,成功的完成從資料庫連線的建立、資料表的設計,到資料的操作和串接。另外對聊天室的資料結構也已經有了明確的規劃。在接下來的幾天,我們將逐步實作類似ChatGPT的聊天室和對話記錄功能,進一步豐富APP的功能和使用體驗哦!



Github專案程式碼:Ionic結合ChatGPT - Day25


上一篇
【Day - 24】Server Sent Event應用 - 結合GPT即時說明文法錯誤
下一篇
【Day - 26】Navigation導航 - 瀏覽歷史對話
系列文
Ionic結合ChatGPT - 30天打造AI英語口說導師APP30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言