[Golang][Notion] 如何透過 Golang 來操控 Notion DB 當成線上資料庫

Notion Databases: An advanced tutorial on Notion's game-changing feature

前提

在撰寫許多 Side Project 的時候,除了網路服務伺服器之外,最困擾的大概就是資料庫的問題。雖然之前我的文章 [學習心得][Golang] 把 Github Issue 當成資料庫來用 曾經教過透過 Github Golang API 來將簡單的一些資料放在 Github Issue 上,但是如果資料格式比較複雜的時候。可能就會需要透過類似資料庫格式的儲存體來處理。 偏偏許多線上資料庫都是算時間與用量,對於想寫一些有趣的 Side Project 卻沒有那麼友善。

本篇文章將使用 Notion Database 作為資料的儲存體,並且透過 Golang 去查詢,插入相關的資料處理。 本篇文章也會從如何設定一個 Notion Integration 開始教導,讓你透過 Golang 來操控 Notion Database 沒有任何痛苦。

本篇文章將透過: https://github.com/kkdai/linebot-smart-namecard 來說明。

關於 Notion Database

以上是一段 Notion 官方教學影片 Notion Database 裡面有提到如何建立一個 Database ,並且有稍微解釋:

  • Create -> 選擇 Database 欄位中 -> Table

使用 Notion Database 的好處:

image-20240115211147923

  • Notion Database 支援相當豐富的格式,並且有很漂亮的視覺化介面。
  • 並且 Notion Database 支援多種格式: Table, Board, Calendar, List, and Gallery
  • 除了蠻方便 coding 之外,如果有後台管理員,可以透過 Notion UI 來直接查看結果。

建立 Notion Integration

可以先到 Notion Developer 建立第一個 Notion Integration :

image-20240115233258526

  • Type: Internal 只有你可以用,其他人沒有辦法選到。
  • Name: 只要可以辨識就好。

這樣就可以建立了 Integration ,並且取的 Internal Integration Secret (Notion API Key):

image-20240115233433452

開啟 Notion Database 讓 Notion Integration 可以存取:

記得要讓 Notion 頁面取得 Integration 權限 ,參考以下圖片。

官方給的 GIF 檔案相當的清楚,這也是最重要的其中一步。要讓你的資料可以讓 Integration 存取。

取得 Notion Database ID

這也是一個相當重要的事情,要使用 Golang 去存取你的 Notion Database 就需要以下兩個資料:

  • Notion Internal Integration Secret (API Key)
  • Notion Page ID

Notion DB 的頁面網址應該是 https://www.notion.so/b764xxxxxa?v=dexxxx1 那麼 b764xxxxxa就是你的 DatabasePageId。

了解 Notion Database Data Type:

在準備要連接 Notion Database 的時候,你必須要先知道每個欄位的差別。

image-20240115234749333

以我的資料庫為例子:

  • UID: 存放 LINE OA User UID,做為辨識之用。資料格式是: Text

image-20240115234848362

  • 其他都是 Title, Address, Email, Phone Number 。
  • Name: 使用了 Title 這個資料格式,其中差別為: Title 只能有一個欄位,並且會變成新頁面的標題。

image-20240115235405588

開始撰寫 Golang Notion Database 程式碼:

這邊使用的套件是: https://github.com/jomei/notionapi

先了解資料架構

// Person 定義了 JSON 資料的結構體
type Person struct {
	Name        string `json:"name"`
	Title       string `json:"title"`
	Address     string `json:"address"`
	Email       string `json:"email"`
	PhoneNumber string `json:"phone_number"`
}

// DatabaseEntry 定義了 Notion 資料庫條目的結構體。
type NotionDB struct {
	DatabaseID string
	Token      string
}
  • Person: 來自名片掃描的 JSON 資料,也代表這裡每個欄位的資料。除了 UID 是要透過參數進來的。
  • NotionDB:啟動 Notion 需要知道的資料:
    • Token: 就是 Notion Integration Secret
    • DatabaseID: 在 URL 即可取得。 Notion DB 的頁面網址應該是 https://www.notion.so/b764xxxxxa?v=dexxxx1 那麼 b764xxxxxa就是你的 DatabasePageId。
// QueryDatabase 根據提供的屬性和值查詢 Notion 資料庫。
func (n *NotionDB) QueryDatabase(UId, property, value string) ([]Person, error) {
	client := notionapi.NewClient(notionapi.Token(n.Token))

	// Add UId to the filter conditions
	// 建立查詢過濾條件
	filter := &notionapi.DatabaseQueryRequest{
		Filter: notionapi.AndCompoundFilter{
			notionapi.PropertyFilter{
				Property: property,
				RichText: &notionapi.TextFilterCondition{
					Equals: value,
				},
			},
			notionapi.PropertyFilter{
				Property: "UID",
				RichText: &notionapi.TextFilterCondition{
					Equals: UId,
				},
			},
		},
	}

	// 調用 Notion API 來查詢資料庫
	result, err := client.Database.Query(context.Background(), notionapi.DatabaseID(n.DatabaseID), filter)
	if err != nil {
		return nil, err
	}

	var entries []Person

	for _, page := range result.Results {
		entry := n.createEntryFromPage(&page)
		entries = append(entries, entry)
	}
	return entries, nil
}

這一段需要注意的是:

  • 過濾條件使用的是 AndCompoundFilter ,也就是要兩個條件 A && B 。
  • 其中要注意的 PropertyFilter如果資料格式不同的時候,需要處理不同資料。
    • Text: TextFilterCondition
    • Title: TitleFilterCondition
  • 依此類推。

再來看如何新增資料

// AddPageToDatabase adds a new page with the provided field values to the specified Notion database.
func (n *NotionDB) AddPageToDatabase(Uid string, name string, title string, address string, email string, phoneNumber string) error {
	client := notionapi.NewClient(notionapi.Token(n.Token))

	// 建立 Properties 物件來設置頁面屬性
	properties := notionapi.Properties{
		"UID": notionapi.RichTextProperty{
			RichText: []notionapi.RichText{
				{
					PlainText: name,
					Text:      &notionapi.Text{Content: Uid},
				},
			},
		},
		"Name": notionapi.TitleProperty{
			Title: []notionapi.RichText{
				{
					PlainText: name,
					Text:      &notionapi.Text{Content: name},
				},
			},
		},
		// Address, Email, Phone Number....
	}

	// 創建一個新頁面的請求
	pageRequest := &notionapi.PageCreateRequest{
		Parent: notionapi.Parent{
			DatabaseID: notionapi.DatabaseID(n.DatabaseID),
		},
		Properties: properties,
	}

	// 調用 Notion API 來創建新頁面
	_, err := client.Page.Create(context.Background(), pageRequest)
	if err != nil {
		log.Println("Error creating page:", err)
		return err
	}

	log.Println("Page added successfully:", Uid, name, title, address, email, phoneNumber)
	return nil
}
  • 大部分程式碼都是類似的,但是根據欄位不同。需要調整以下內容:
"UID": notionapi.RichTextProperty{
			RichText: []notionapi.RichText{
				{
					PlainText: name,
					Text:      &notionapi.Text{Content: Uid},
				},
			},
		},
  • 其中的 name 是固定參數,不能改。
  • 只有後面的 Content: Uid 可以改。

  • 此外,根據欄位不同 RichTextProperty 也會變動。如果不正確,就無法正確地寫入資料。

最後測試範例程式碼:

func TestAddNotionDB(t *testing.T) {
	token := os.Getenv("NOTION_INTEGRATION_TOKEN")
	pageid := os.Getenv("NOTION_DB_PAGEID")

	// If not set token and pageid , skip this test
	if token == "" || pageid == "" {
		t.Skip("NOTION_INTEGRATION_TOKEN or NOTION_DB_PAGEID not set")
	}

	db := &NotionDB{
		DatabaseID: pageid,
		Token:      token,
	}

	err := db.AddPageToDatabase("uid", "name", "title", "address", "[email protected]", "phone")
	if err != nil {
		t.Fatal(err)
	}
}

參考資料:

[遊戲天國] FF15 全破

FF15 全破了, 同時有玩 FF16 (用 PS Portal) 跟 FF15 (Steam Deck) ,後來竟然覺得 FF15 比較讓我玩得下去。 很專心地把它破關了。

  • 一開始為人詬病的開車兜風那段,我覺得很悠閒。

  • FF15 的相當多樣的支線任務我還蠻喜歡的,不論陸行鳥,拍照任務,還是武器鍛鍊任務。

  • 最後的三個好友單挑歷代王,很感人啊。

  • 幾個 DLC 慢慢留著玩,應該要準備人龍8 惹。

找了一下劇情完結:

關於本傳的解釋:

image-20240115225004791

image-20240115225020138

[研討會心得] NV TW LLM Developer Day 2024

image-20240111122733576

活動資訊

NVIDIA LLM Developer Day重播)

  • 09:30 - 11:00 台灣時間 - 議程 1: 開發大型語言模型 (LLMs) 的快速途徑: slide

  • 11:10 - 12:50 台灣時間 - 議程 2a:量身客製自己的大型語言模型應用:slides

  • 15:30 台灣時間 - 議程 2b:生物科學大型語言模型和生成式人工智慧

  • 17:10 台灣時間 - 議程 3:運行自己的大型語言模型

議程 1: 開發大型語言模型 (LLMs) 的快速途徑

案例 (1) - 音樂公司的客服系統

image-20240111095051777image-20240111095054776

  • 第一個案例: 透過 LLM 來做樂器商的客服系統。
  • 收到客戶 email, 客戶 tag
  • 根據問題找出 RAG 可能的回覆。(顯示於下方),幫助客服回覆。

感想:

  • 竟然用 OpenAI 而非 LLAMA 以 NV DevDay 來說有點怪。
  • 裏面 OpenAI 版本是 < 1.0 , 但是 Openai package 在 11/07 就已經更新到 1.0。(因為當初還被雷到)

1.1 解讀 email 的 prompt

image-20240111095509845

  • 分條列出好的地方跟不好的地方。
  • 分類緊急性。
  • 判斷信件的口吻
  • 結構化輸出

1.2 分析的 Prompt

image-20240111095935738

  • 資料處理的 prompt

image-20240111100529570

  • 格式化輸出的 prompt

image-20240111100620053

  • 透過 COLANG 來定義使用者資料流。

尚未公開產品 Nemotron-3

image-20240111102836188

  • NeVA (NeMo Vision and Language Assistant)

其他:

GTC 2024 - 實體 GTC 03/18。

更多參考:

[打造自己的知識系統] Part 1: 整理資訊流 with IFTTT

image-20240109113904905

(From PlantUML)

總結

我一直以來都是一個喜歡東看西看的人(聽說很多人跟我一樣),但是一直以來資訊流的整理一直是我很痛苦的事情。接下來可能會有一系列的文章記錄著我邊打造個人 LLM KM 系統的時候的一些記錄跟想法。

資訊流

原本資訊流還蠻簡單的,就是希望所有的資訊可以透過 twitter 去當 gateway 然後開始轉到其他地方。 這裡有點麻煩得時候,自從 Twitter 再也不接受免費 API 的申請後(最便宜 100$USD) 。 只好去購買 IFTTT 的服務幫我把資訊從 Twitter 打到 Webhook 。 這邊我以前有類似的文章:

看到五年前開始打造的時候才 10 ~ 20 github issue ,現在卻有 1.3k 的數字。開始思考要如何整理相關資訊。

更多參考:

[線上課程筆記]DeepLearningAI - Advanced Retrieval for AI with Chroma

image-20240108172844892

課程簡介

Deep Learning AI 新的課程,如何優化 IR/RAG on Chroma 。 講師是 Chroma co-founder 有以下三個技術:

  • Query Expansion: 透過相關概念來擴展查詢。
  • Cross-encoder reranking: 透過不同檢索編碼來排序查詢結果。
  • Training and utilizing Embedding Adapters: 透過加入 adapter 來優化檢索結果。

課程資訊: https://learn.deeplearning.ai/advanced-retrieval-for-ai/

RAG Pitfall

經常查詢 RAG 結果回來會是不相關的,怎麼看出來? 透過一個 umap 套件

import umap
import numpy as np
from tqdm import tqdm

embeddings = chroma_collection.get(include=['embeddings'])['embeddings']
umap_transform = umap.UMAP(random_state=0, transform_seed=0).fit(embeddings)


# 畫點出來
import matplotlib.pyplot as plt

plt.figure()
plt.scatter(projected_dataset_embeddings[:, 0], projected_dataset_embeddings[:, 1], s=10)
plt.gca().set_aspect('equal', 'datalim')
plt.title('Projected Embeddings')
plt.axis('off')

比較相近的問題(單一問題,比較容易)

image-20240108175808801

這樣看起來查詢的資訊跟我們問得蠻相近的,紅色是回答的。綠色是前面幾個相關的。

如果問句有兩個以上,或是問句本身就不太相關。

image-20240108180009118

這樣就會出現差相當多的結果,造成查詢的資料相關度過少。出來的結果當然也就很差。

解決方式就要靠接下來的三個方法。

Query Expansion

image-20240108194721737

透過延伸的假設答案,加上原來的問題。一起下去詢問:

def augment_query_generated(query, model="gpt-3.5-turbo"):
    messages = [
        {
            "role": "system",
            "content": "You are a helpful expert financial research assistant. Provide an example answer to the given question, that might be found in a document like an annual report. "
        },
        {"role": "user", "content": query}
    ] 

    response = openai_client.chat.completions.create(
        model=model,
        messages=messages,
    )
    content = response.choices[0].message.content
    return content

e.g.

  • Q: Was there significant turnover in the executive team?
  • 先用這個 Q 直接問 OpenAI 得到可能的解答 hypothetical_answer,但是因為沒有查詢特有 RAG 資料可能不會正確。
  • 透過 f"{original_query} {hypothetical_answer}" 結合再一起,再來透過 VectorDB 來尋找答案。
original_query = "Was there significant turnover in the executive team?"
hypothetical_answer = augment_query_generated(original_query)

joint_query = f"{original_query} {hypothetical_answer}"
print(word_wrap(joint_query))

Cross-encoder reranking

image-20240108173134289

透過不同問句,取得相似問句。透過該捷達來評分。這邊的作法如下:

  • 原本問句 original_query ,得到數個 generated_queries:
  • 然後把原本問句跟其他問句下去找數組的解答。
  • 將每一組解答,透過 scores = cross_encoder.predict(pairs) 評分來打分數。
  • 挑選出比較高分的幾個項目再來去 RAG
original_query = "What were the most important factors that contributed to increases in revenue?"
generated_queries = [
    "What were the major drivers of revenue growth?",
    "Were there any new product launches that contributed to the increase in revenue?",
    "Did any changes in pricing or promotions impact the revenue growth?",
    "What were the key market trends that facilitated the increase in revenue?",
    "Did any acquisitions or partnerships contribute to the revenue growth?"
]

queries = [original_query] + generated_queries

results = chroma_collection.query(query_texts=queries, n_results=10, include=['documents', 'embeddings'])
retrieved_documents = results['documents']

# Deduplicate the retrieved documents
unique_documents = set()
for documents in retrieved_documents:
    for document in documents:
        unique_documents.add(document)

unique_documents = list(unique_documents)

pairs = []
for doc in unique_documents:
    pairs.append([original_query, doc])
    
    
print("Scores:")
for score in scores:
    print(score)

Training and utilizing Embedding Adapters

image-20240108195535753

# 產生相關問句
def generate_queries(model="gpt-3.5-turbo"):
    messages = [
        {
            "role": "system",
            "content": "You are a helpful expert financial research assistant. You help users analyze financial statements to better understand companies. "
            "Suggest 10 to 15 short questions that are important to ask when analyzing an annual report. "
            "Do not output any compound questions (questions with multiple sentences or conjunctions)."
            "Output each question on a separate line divided by a newline."
        },
    ]

    response = openai_client.chat.completions.create(
        model=model,
        messages=messages,
    )
    content = response.choices[0].message.content
    content = content.split("\n")
    return content

產生相關答案

generated_queries = generate_queries()
for query in generated_queries:
    print(query)

透過 10 ~ 15 個問題,產生衍生的答案。約有 150 個。 這個就變成是新的資料集。(RAG)

透過新的相似度比較方式 (mse_loss):

def mse_loss(query_embedding, document_embedding, adaptor_matrix, label):
    return torch.nn.MSELoss()(model(query_embedding, document_embedding, adaptor_matrix), label)

透過這個方式,再來找出最好的解答。

image-20240108202350286

這張圖可以看出 adapted query 結果不容易產生不相關的答案。這邊也有建議,如果可以拿到使用者的資料來作為 adapted 問句,可能可以讓答案變得更好。

衍生思考:

  • 發現結果過於不相關的時候。
  • 透過多問幾題,然後找出相關答案。
  • 變成新的資料集,作為查詢。就可以優化整個 RAG 的資料集,進而得到更好的解答。

課程總結:

  • 先解釋 RAG 經常會遇到的陷阱。過於分散的問句,造成相似的解答不相關,回覆就會無法準確。
  • Expanding Query: 請 OpenAI 幫你多問幾題,然後把問題跟答案都放進去詢問。
  • Cross Re-Rank: 算是上面的進化版,透過產生問句的答案。透過一個評分機制。找到比較好的答案再下去 RAG 。
  • Embedding Adapter: 產生更多問句,透過問句產生的解答。當作是新的 dataset ,並且下來 RAG 。

更多參考:

[Golang][LINE Bot SDK] 如何更新 Golang LINE Bot SDK v8 OpenAPI(Swagger)

image-20240105183407936

前情提要:

2023 年 LINE Bot SDK 積極推動 OpenAPI (a.k.a. swagger) 的標準介面。透過與 OpenAPI 的整合, LINE Bot SDK 有了許多好處(文章後半段補充)。本篇文章將稍微解釋一下,OpenAPI 導入後的優點,並且帶著大家一起來將有使用 LINE Bot Go SDK v7 版本的升級到 v8 的版本。

本次的程式碼,會用前幾篇文章提到的: https://github.com/kkdai/linebot-gemini-pro 作為範例。

支援 OpenAPI 的好處

LINE Bot Go SDK 近期也在 2023/11 月也將版號更新到了 v8,並且正式支援 OpenAPI。 那麼簡單的來說,一個 SDK 套件支援 OpenAPI 有哪些好處呢?

1. 標準化的 API 設計

  • 許多 API 呼叫的方式,變得更加的直觀。也比較容易了解。後面也會稍微提到。

2. 程式碼自動生成

image-20240105200853423

  • 以往要開發新的 API Entry 的時候,都要透過發送 issue -> 發送 PR -> 審核 -> 發布後才能發送到開發者手中。
  • 但是導入後, LINE Bot SDK 使用的方式,是透過 Github Action 來自動去將最新的 LINE OPENAPI Repo抓下來後,根據新的變動產生相關的對應程式碼。 (參考上圖)(參考 Code Generator 產生的commit)

更多好處:

  • 自動化文件生成:有許多相關文件可以幫忙自動化產生 OpenAPI 的文件。

  • 客戶端與服務端驗證:透過 OpenAPI 的導入可以達成自動化測試與 Consumer-Driven Contract 的驗證。
  • API 生態系統工具的整合等等

開始 Migrate LINE Bot Go SDK from v7 to v8

先修改套件版本 v7 -> v8

先加入以下三個套件,接下來會解釋一下:

	"github.com/line/line-bot-sdk-go/v8/linebot"
	"github.com/line/line-bot-sdk-go/v8/linebot/messaging_api"
	"github.com/line/line-bot-sdk-go/v8/linebot/webhook"
  • linebot: 主要處理 v7 相關的內容,儘量不要使用。會逐步 deprecated 。
  • messaging_api : 發送訊息用。
  • webhook: 接受訊息與處理相關 event 用的。

接下來將慢慢整合,並且開始說明相關套件使用到的部分。

開始整合,並且說明相關變動之處:

“github.com/line/line-bot-sdk-go/v8/linebot/webhook”

  • 負責接受 Webhook 相關資料,裡面包括兩大系列:
  • MessageEvent 要處理相關訊息 Webhook 包括:
    • TextMessageContent: 文字訊息。
    • StickerMessageContent: 貼圖訊息 ..
    • ImageMessageContent: 圖片訊息,由使用者上傳的相片或是圖片訊息。
  • 其他相關 Event 處理,包括:
    • FollowEvent: 有新的好友。
    • PostbackEvent: 這個比較常用,就是透過 postback 將使用者選擇作為輸入。
    • BeaconEvent: 可以透過這個訊息來接受來自有登記過的 Beacon HWID 的訊息。
  • Webhook 處理方式也有點不同,細節稍微寫一下:
// 需要透過不同 Channel Secret 才能 Parse request 。
cb, err := webhook.ParseRequest(os.Getenv("ChannelSecret"), r)
	if err != nil {
		if err == linebot.ErrInvalidSignature {
			w.WriteHeader(400)
		} else {
			w.WriteHeader(500)
		}
		return
	}

	// 這邊比較類似以前方式,透過 callback 的 event 來 switch 個別 event type 。
	for _, event := range cb.Events {
		log.Printf("Got event %v", event)
		switch e := event.(type) {
		case webhook.MessageEvent:
			switch message := e.Message.(type) {
			// Handle only on text message
			case webhook.TextMessageContent:
			.......
  • 關於 UserId 與 RoomId 的取得方式:

    • 以前都是透過 source.RoomID 來取得,但是現在透過另外一種方式來處理。但是得要有特殊處理:
    if source.RoomID != "" //不是群組,可能是 1 on 1
    	....
    
    • 現在則是透過不同的方式,要透過 webhook.Source.(type) 來處理:
    // 取得用戶 ID ,個人覺得更加容易了解。
    var uID string
    switch source := e.Source.(type) {
    case webhook.UserSource:
      uID = source.UserId
    case webhook.GroupSource:
      uID = source.UserId
    case webhook.RoomSource:
      uID = source.UserId
    }
    

“github.com/line/line-bot-sdk-go/v8/linebot/messaging_api”

  • 這個是負責發送相關訊息或是 Event 給使用者的。也就是要發送任何訊息都要透過過這個套件。 這個套件將資料格式有切開來了:

    • messaging_api.MessagingApiAPI: 負責處理小量訊息,包括 text message, sticker message … 等等。
    • messaging_api.MessagingApiBlobAPI: Blob 負責處理比較大量的資料,比如說你需要抓取使用這上傳的圖片。
    • 這兩個都需要分開初始化,參考以下:
    	if err != nil {
    		log.Fatal(err)
    	}
      
    	blob, err = messaging_api.NewMessagingApiBlobAPI(channelToken)
    	if err != nil {
    		log.Fatal(err)
    	}
      
    
    • 如果要回覆一個訊息,變得更加容易了解:
    // v7: 需要先初始化之後,還要透過 Do() 才會執行
      
    if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(outStickerResult)).Do(); err != nil {
    	log.Print(err)
    }
      
    // v8: 可以直接呼叫
    if _, err := bot.ReplyMessage(
    		&messaging_api.ReplyMessageRequest{
    			ReplyToken: replyToken,
    			Messages: []messaging_api.MessageInterface{
    				&messaging_api.TextMessage{
    					Text: text,
    				},
    			},
    		},
    	); err != nil {
    		return err
    	}
    

總結:

Golang LINE Bot SDK 套件這一次除了升級到 OpenAPI 的版本之外,也對於許多呼叫方式與變數處理方式做了一個通盤的整理。在處理許多訊息上,變得更加直覺與異動。雖然要變動可能會比較多,並且還有一些 API 持續搬遷之中。 不過由於套件也還保持著 v7 的相關套件可以繼續使用 legacy API ,所以歡迎大家可以儘快開始整合到 v8 的版本。 這樣一來可以看到許多更新的 API 在第一時間就會釋放出來。

參考資料: