Chapter 7: 轉換 (Transformations)

歡迎來到 open-notebook 教學系列的第七章!在上一章 提示詞管理器 (Prompter) 中,我們學會了如何像使用信件模板一樣,有效地管理和生成給 AI 模型(LLM)的指令(提示詞)。Prompter 確保了我們能清晰地告訴 AI 我們希望它做什麼以及如何格式化輸出。

現在,想像一下,您剛剛用 open-notebook 添加了一篇很長的網路文章作為來源。您可能希望系統能自動幫您做一些常見的處理,例如:

如果每次添加新來源後,都要手動去聊天介面輸入指令來做這些事情,會很重複也很麻煩。我們希望能有一種方法,可以預先定義好這些常用的文字處理任務,然後在需要時輕鬆地套用它們。

這就是「轉換 (Transformations)」的設計目的!

為什麼需要轉換 (Transformations)?

我們希望 open-notebook 不僅能儲存資訊,還能主動地幫助我們處理和理解這些資訊。對於文字內容,有很多常見的 AI 處理需求,像是摘要、提取關鍵字、翻譯、改寫風格等等。

如果沒有一個統一的方式來定義和管理這些常見任務,我們可能會:

「轉換 (Transformations)」提供了一種標準化的方式來定義、管理和應用這些可重複使用的 AI 文字處理任務。

什麼是轉換 (Transformations)?

轉換 (Transformations) 是您可以定義的一系列特定的 AI 文字處理任務,可以應用於您的來源內容。您可以把它們想像成一套為您的文字內容準備的濾鏡

每個「轉換」濾鏡本身儲存了實現特定效果所需的所有資訊:

重要的是,Transformation 物件本身也繼承自 物件模型 (ObjectModel)。這意味著每個轉換都可以像筆記本或筆記一樣,被儲存到資料庫、從資料庫讀取、更新或刪除。

# open_notebook/domain/transformation.py (簡化示意)
from typing import ClassVar
from open_notebook.domain.base import ObjectModel

class Transformation(ObjectModel): # 繼承自 ObjectModel
    table_name: ClassVar[str] = "transformation" # 對應資料庫表格
    name: str         # 內部名稱
    title: str        # UI 顯示標題
    description: str  # 描述
    prompt: str       # 核心:給 AI 的提示詞模板
    apply_default: bool # 是否預設套用

這樣一來,我們可以輕鬆地管理這些可重複使用的 AI 處理任務。

如何使用轉換 (Transformations)

使用「轉換」主要涉及兩個方面:定義管理轉換,以及在流程中應用轉換。

1. 定義和管理轉換

open-notebook 提供了一個使用者介面(位於 pages/8_💱_Transformations.py),讓您可以輕鬆地建立、編輯和刪除轉換。

在這個頁面上,您可以:

# pages/8_💱_Transformations.py (簡化 UI 邏輯示意)
import streamlit as st
from open_notebook.domain.transformation import Transformation

# ... (載入所有 transformations) ...

# 顯示建立新轉換的按鈕
if st.button("Create new Transformation"):
    # 建立一個預設的 Transformation 物件
    new_trans = Transformation(
        name="New Name", title="New Title", prompt="", apply_default=False
    )
    # ... (將 new_trans 加入 session_state 列表以便顯示) ...
    
# 遍歷顯示每個 transformation 的編輯介面
# for transformation in st.session_state.transformations:
#     with st.expander(transformation.name):
#         # 使用 st.text_input, st.text_area 等讓使用者編輯屬性
#         name = st.text_input("Name", transformation.name)
#         prompt = st.text_area("Prompt", transformation.prompt, height=300)
#         apply_default = st.checkbox("Suggest by default", ...)
        
#         if st.button("Save"):
#             # 更新 transformation 物件的屬性
#             transformation.name = name
#             transformation.prompt = prompt
#             # ...
#             transformation.save() # 使用 ObjectModel 的 save() 儲存
#             st.toast("Saved!")

程式碼解釋:

2. 在流程中應用轉換

轉換通常是在處理新來源時自動應用的,或者可以在之後手動觸發。open-notebook 使用 LangGraph 狀態機 (Graph Workflows) 來編排這個過程,特別是在 open_notebook/graphs/source.py 中定義的 source_graph

當一個新來源(例如,一篇網路文章)被加入並透過 source_graph 處理時:

  1. 內容提取: 首先,內容處理流程 (Content Processing Graph) 會提取來源的純文字內容。
  2. 儲存來源: 包含純文字的 Source 物件被儲存到資料庫。
  3. 觸發轉換 (條件邊): source_graph 中的 trigger_transformations 節點會檢查需要應用哪些轉換(可能是預設勾選的,或是使用者特別選擇的)。
  4. 執行轉換 (扇出): 對於每一個需要應用的轉換,trigger_transformations 會發送一個任務給 transform_content 節點。LangGraph 會並行處理這些任務。
  5. 調用轉換圖: transform_content 節點會接收來源物件 (source) 和單個轉換物件 (transformation)。然後,它會呼叫另一個更小的、專門執行單一轉換的 LangGraph 圖 (transform_graph)。
  6. 儲存結果: transform_graph 返回轉換後的文字(例如摘要)。transform_content 節點會將這個結果附加到 Source 物件上(通常作為一個「洞見」(insight))。
# open_notebook/graphs/source.py (簡化 - 觸發和執行轉換的部分)
from open_notebook.domain.transformation import Transformation
from open_notebook.graphs.transformation import graph as transform_graph
from langgraph.types import Send

# 條件邊函式:決定要觸發哪些轉換任務
def trigger_transformations(state: SourceState, config: RunnableConfig) -> List[Send]:
    # state["apply_transformations"] 包含要應用的 Transformation 物件列表
    to_apply = state["apply_transformations"] 
    if not to_apply: return [] # 如果沒有要應用的,就返回空列表

    # 為每個要應用的轉換建立一個 Send 指令
    return [
        Send( # 指示 LangGraph 呼叫 "transform_content" 節點
            "transform_content", 
            { # 傳遞給該節點的資料
                "source": state["source"], # 來源物件
                "transformation": t,       # 當前這個 Transformation 物件
            },
        )
        for t in to_apply # 遍歷所有要應用的轉換
    ]

# 執行單一轉換的節點函式
async def transform_content(state: TransformationState) -> Optional[dict]:
    source = state["source"]
    transformation: Transformation = state["transformation"]
    content_to_transform = source.full_text # 取得來源的完整文字

    # 呼叫專門的轉換圖 (transform_graph)
    result = await transform_graph.ainvoke(
        { # 傳遞輸入文字和轉換物件給 transform_graph
            "input_text": content_to_transform, 
            "transformation": transformation
        } 
    )
    
    # 將轉換結果 (例如摘要) 儲存回來源物件
    source.add_insight(transformation.title, result["output"]) 
    # ... (返回狀態更新) ...
    return {"transformation": [{"output": result["output"], ...}]}

# 在 SourceStateGraph 中定義邊
# workflow.add_conditional_edges("save_source", trigger_transformations, ["transform_content"])
# workflow.add_edge("transform_content", END)

程式碼解釋:

這個流程展示了如何將預先定義好的「轉換」無縫整合到自動化的內容處理工作流程中。

深入探索:「轉換圖」的內部機制

那麼,那個被呼叫的 transform_graph(定義在 open_notebook/graphs/transformation.py)內部又是如何運作的呢?這個圖相對簡單,基本上只有一個核心節點 run_transformation

執行流程概覽 (transform_graph.ainvoke)

transform_content 節點呼叫 transform_graph.ainvoke({"input_text": ..., "transformation": ...}) 時:

  1. 接收輸入: transform_graph 接收到包含 input_text(原始文字)和 transformation(要應用的 Transformation 物件)的狀態。
  2. 執行節點 (run_transformation): 圖中唯一的節點 run_transformation 開始執行。
  3. 準備提示詞:
    • 節點從傳入的 transformation 物件中取得核心的提示詞模板 (transformation.prompt)。
    • (可選)它可能會從 DefaultPrompts 物件讀取一些通用的前置指令,加在特定提示詞模板的前面。
    • 它使用 提示詞管理器 (Prompter) 來渲染最終的提示詞。它會將 input_text 等資料傳遞給 Prompter,讓 Jinja2 引擎將這些資料填入提示詞模板。
  4. 呼叫 AI 模型:
    • 節點使用 provision_langchain_model 這個輔助函式(內部會用到 模型管理器 (ModelManager))來獲取一個設定好的語言模型 (LLM)。
    • 它將渲染好的最終提示詞和原始的 input_text (通常作為 HumanMessage) 一起發送給 LLM。
  5. 獲取結果: LLM 處理請求,並返回生成的文字(例如摘要、翻譯結果等)。
  6. 返回輸出: run_transformation 節點將 LLM 返回的結果放入狀態的 output 欄位。
  7. 圖結束: 流程到達 ENDtransform_graph 將包含 output 的最終狀態返回給呼叫者(即 source_graph 中的 transform_content 節點)。

以下是一個簡化的序列圖,展示了 transform_graph 的執行過程:

sequenceDiagram participant SourceGraph as 來源圖 (Source Graph) participant TransformGraph as 轉換圖 (Transformation Graph) participant TransformationNode as 轉換節點 (run_transformation) participant Prompter as 提示詞管理器 participant LLM as 語言模型 SourceGraph->>TransformGraph: ainvoke({"input_text": ..., "transformation": ...}) TransformGraph->>TransformationNode: 執行 (傳入狀態) TransformationNode->>Prompter: render(模板=轉換.prompt, 資料={"input_text": ...}) Prompter-->>TransformationNode: 返回最終提示詞 TransformationNode->>LLM: invoke(最終提示詞 + 輸入文字) LLM-->>TransformationNode: 返回 AI 生成結果 TransformationNode-->>TransformGraph: 返回更新 {"output": AI結果} TransformGraph-->>SourceGraph: 返回最終狀態

transform_graph 的核心程式碼片段

讓我們看看 open_notebook/graphs/transformation.py 中定義這個簡單圖形的關鍵程式碼:

# open_notebook/graphs/transformation.py (簡化)
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict
from open_notebook.domain.transformation import Transformation # 匯入 Transformation 模型
from open_notebook.prompter import Prompter # 匯入 Prompter
# 匯入模型配置和訊息相關工具
from open_notebook.graphs.utils import provision_langchain_model 
from langchain_core.messages import HumanMessage, SystemMessage

# 定義轉換圖的狀態
class TransformationState(TypedDict):
    input_text: str           # 輸入的原始文字
    transformation: Transformation # 要應用的轉換物件
    output: str               # AI 輸出的結果

# 執行轉換的核心節點函式
def run_transformation(state: dict, config: RunnableConfig) -> dict:
    # 從狀態中獲取輸入文字和轉換物件
    content = state.get("input_text")
    transformation: Transformation = state["transformation"]
    
    # 獲取轉換的核心提示詞模板
    transformation_prompt_text = transformation.prompt 
    # ... (這裡可能加入 DefaultPrompts 的通用指令) ...
    
    # 使用 Prompter 準備最終的系統提示詞
    # Prompter 會自動處理模板渲染
    system_prompt = Prompter(prompt_text=transformation_prompt_text).render(data=state)
    
    # 準備發送給 LLM 的訊息 (包含系統提示詞和原始輸入)
    payload = [
        SystemMessage(content=system_prompt), 
        HumanMessage(content=content)
    ]
    
    # 獲取並設定 LLM (使用輔助函式,內部可能用到 ModelManager)
    chain = provision_langchain_model(
        str(payload), # 可能用於選擇模型
        config.get("configurable", {}).get("model_id"), # 允許外部傳入模型 ID
        "transformation", # 任務類型標籤
        # ... 其他模型設定 ...
    )

    # 呼叫 LLM 執行轉換
    response = chain.invoke(payload)
    
    # 返回包含輸出結果的字典,更新狀態
    return {
        "output": response.content,
    }

# 建立 LangGraph 狀態圖
agent_state = StateGraph(TransformationState)
# 新增唯一的節點 "agent"
agent_state.add_node("agent", run_transformation)
# 設定流程:從 START -> agent -> END
agent_state.add_edge(START, "agent")
agent_state.add_edge("agent", END)
# 編譯圖
graph = agent_state.compile() 

程式碼解釋:

總結

在本章中,我們深入了解了「轉換 (Transformations)」,這是 open-notebook 中定義和重用 AI 文字處理任務的機制。

透過「轉換」,open-notebook 提供了一種強大且靈活的方式,讓使用者可以自訂和自動化各種 AI 文字處理任務,豐富筆記內容並從中提取更多價值。

到目前為止,我們已經探討了 open-notebook 的許多核心概念:資料結構 (物件模型 (ObjectModel))、AI 模型管理 (模型管理器 (ModelManager))、使用者互動 (使用者介面 (Streamlit UI))、自動化流程 (內容處理流程, LangGraph 狀態機)、AI 指令生成 (提示詞管理器 (Prompter)) 以及可重用的 AI 任務 (轉換)。

所有這些元件都需要一個可靠的底層機制來儲存和讀取資料——筆記本、筆記、來源、模型設定、轉換定義等等。這就需要一個與資料庫溝通的橋樑。

在下一章,也是我們教學系列的最後一章,我們將深入了解 資料庫儲存庫 (Database Repository),看看 open-notebook 是如何與資料庫(特別是 SurrealDB)互動,實現資料的持久化儲存和檢索。