最近は LLM を使って色々実験しており、その中でも Dify をよく使っています。今回は Dify を使った小ネタを紹介します。
Dify は、カスタマイズ可能な AI ワークフロー・チャットボット・エージェントをノーコードで作成できるプラットフォームです。作成したワークフローはブラウザや Chrome 拡張機能、API 経由で実行できます。一方、Obsidian は拡張性の高い markdown ベースのノートアプリケーションとして知られています。
今回はこれらのツールを連携させ、以下の機能を実現しました。
- Dify を利用して、指定した URL の記事を自動的に要約する
- Obsidian から Dify の要約機能を API 経由で呼び出し、結果をノートとして保存する
私は Obsidian にさまざまなメモやログを蓄積しており、日々気になった Web 上の記事もメモと共に Obsidian に保存し、ナレッジベースとして活用しています。
1 年ほど前から ChatGPT でも Web 記事の内容を読んで返答できるようになったので、普段でも要約をまとめてもらってノートにコピペしていましたが、その作業を自動化して一発で Obsidian のノートに保存できないかと考えていました。それが Dify と Obsidian の組み合わせで実現できました。
Dify と Obsidian の組み合わせを紹介する記事となるとかなりニッチな領域なんですが、両者とも拡張性が高いため、さまざまな用途に活用できると思います。例えば Dify の API アクセスを活用すると、Dify だけでは機能が足りない時に Zapier から API アクセスする形で補うなども可能です。
以下のセクションでは、Dify でのワークフロー作成から Obsidian での実装まで、具体的な手順を解説していきます。
動作イメージ
実際の動作イメージは以下の通りです:
- コマンドパレットから「Templater: Create new note from template」を選択
- テンプレート「url-summary」を選択
- 要約・保存したい記事の URL を入力
- 数秒待つと、要約されたノートが自動的に保存される
ノートとして保存した後は、LLM の要約結果を必要に応じて調整できます。私は Evergreen Notes の思想を取り入れて情報整理をしており、ここで既存の他のノートへのリンクを貼るようにしています。
この仕組みはスマホ版の Obsidian でも問題なく動作するため、出先などでも記事を簡単に保存できるのが便利です。
Dify での要約ワークフロー作成
Dify を使って、URL から記事を要約するワークフローを作成します。以下がワークフローの概要です。
このワークフローは以下のノードで構成されています:
- 開始ノード:URL を入力として受け取ります。
- JinaReader ツール:入力された URL の内容を読み取ります。
- JSON Parse ツール:JSON 文字列から任意のフィールドを取り出すツールです。ここでは JinaReader の出力から記事のタイトルを取り出します。
- LLM ノード(GPT-4o):記事の内容を要約します。要約は日本語で、主な議論点と結論を含む 3 行以内の箇条書きで生成されます。
- 終了ノード:要約内容(content)とタイトル(title)を出力します。
このワークフローを自分の Dify アカウントで再現したい方は、以下の DSL をコピーの上インポートしてください。
ワークフローの DSL
app:
description: ""
icon: 🤖
icon_background: "#FFEAD5"
mode: workflow
name: 要約
kind: app
version: 0.1.1
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
image:
enabled: false
number_limits: 3
transfer_methods:
- local_file
- remote_url
opening_statement: ""
retriever_resource:
enabled: false
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
language: ""
voice: ""
graph:
edges:
- data:
isInIteration: false
sourceType: start
targetType: tool
id: 1722151586902-source-1722300182734-target
source: "1722151586902"
sourceHandle: source
target: "1722300182734"
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: tool
targetType: tool
id: 1722300182734-source-1723634891190-target
source: "1722300182734"
sourceHandle: source
target: "1723634891190"
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: tool
targetType: llm
id: 1723634891190-source-1722299744292-target
source: "1723634891190"
sourceHandle: source
target: "1722299744292"
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: llm
targetType: end
id: 1722299744292-source-1722300260176-target
source: "1722299744292"
sourceHandle: source
target: "1722300260176"
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ""
selected: false
title: 開始
type: start
variables:
- label: URL
max_length: 256
options: []
required: true
type: text-input
variable: url
height: 90
id: "1722151586902"
position:
x: 30
y: 349
positionAbsolute:
x: 30
y: 349
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
context:
enabled: true
variable_selector:
- "1722300182734"
- text
desc: ""
model:
completion_params:
temperature: 0.7
mode: chat
name: gpt-4o-mini
provider: openai
prompt_template:
- id: b2b30596-9568-474b-8c30-75da1cfc104b
role: system
text:
"以下の記事を要約してください。要約は、日本語で、主な議論点と結論を含め、箇条書きで、3行以内でお願いします。
{{#context#}}"
selected: false
title: Generate summary
type: llm
variables: []
vision:
configs:
detail: high
enabled: true
height: 98
id: "1722299744292"
position:
x: 942
y: 349
positionAbsolute:
x: 942
y: 349
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ""
provider_id: jina
provider_name: jina
provider_type: builtin
selected: false
title: JinaReader
tool_configurations:
gather_all_images_at_the_end: 0
gather_all_links_at_the_end: 0
image_caption: 0
max_retries: 3
no_cache: 0
proxy_server: null
summary: 0
target_selector: null
wait_for_selector: null
tool_label: JinaReader
tool_name: jina_reader
tool_parameters:
url:
type: mixed
value: "{{#1722151586902.url#}}"
type: tool
height: 298
id: "1722300182734"
position:
x: 334
y: 349
positionAbsolute:
x: 334
y: 349
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ""
outputs:
- value_selector:
- "1722299744292"
- text
variable: content
- value_selector:
- "1723634891190"
- text
variable: title
selected: false
title: 終了
type: end
height: 116
id: "1722300260176"
position:
x: 1246
y: 349
positionAbsolute:
x: 1246
y: 349
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ""
provider_id: json_process
provider_name: json_process
provider_type: builtin
selected: false
title: Extract title
tool_configurations:
ensure_ascii: 1
tool_label: JSON Parse
tool_name: parse
tool_parameters:
content:
type: mixed
value: "{{#1722300182734.text#}}"
json_filter:
type: mixed
value: data.title
type: tool
height: 90
id: "1723634891190"
position:
x: 638
y: 349
positionAbsolute:
x: 638
y: 349
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: 68.00119294975991
y: 34.74871751854829
zoom: 0.6686980554677073
ワークフローを作成したら、API キーを取得します。この API キーは後で Obsidian から要約機能を呼び出す際に使用します。
次のセクションでは、この Dify ワークフローを Obsidian から呼び出す方法について説明します。
Obsidian から API 呼び出しとノート保存の実装
Obsidian から Dify の API を呼び出し、生成された要約をノートとして保存する機能を実装します。この実装には Obsidian の Templater プラグイン を使用します。Templater プラグインをインストールし、有効化してください。
1. API 呼び出し用 JavaScript ファイルの作成
Dify の API を呼び出すための JavaScript 関数を記述したファイルを作成します。以下の内容で getSummaryFromUrl.js
というファイルを作成し、Obsidian の vault の適切な場所(自分は templater-scripts
フォルダにしています)に保存します。
/**
*
* @param {string} url - The URL of the article to summarize.
* @param {function(string): Promise<void>} onTitleUpdate - Callback function to handle title updates as they are streamed.
* @param {function(string): void} onContentUpdate - Callback function to handle content updates as they are streamed.
*/
async function getSummaryFromUrl(url, onTitleUpdate, onContentUpdate) {
const response = await fetch("https://api.dify.ai/v1/workflows/run", {
method: "POST",
headers: {
Authorization: "Bearer [DifyのAPIキーを貼り付け]",
"Content-Type": "application/json",
},
body: JSON.stringify({
inputs: { url: url }, // 「開始」ノードで設定した入力フィールド
response_mode: "streaming", // Server-Sent Eventsで返却される
user: "obsidian", // APIアクセスしたユーザーを識別するための情報。適当な文字列で大丈夫
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
let bufferObj;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
for (const line of lines) {
// レスポンスには先頭に `data: ` という文字列とJSON文字列が含まれている
if (line.startsWith("data: ")) {
try {
// 不要な文字列を取り除いてパース
bufferObj = JSON.parse(line.substring(6));
} catch {
continue;
}
// タイトル抽出のノードが終了したらその時点でタイトルを保存
if (
bufferObj.event === "node_finished" &&
bufferObj.data.title === "Extract title" &&
bufferObj.data?.outputs?.text
) {
await onTitleUpdate(bufferObj.data.outputs.text);
}
// LLMのテキストチャンクを都度保存
if (bufferObj.event === "text_chunk" && bufferObj.data?.text) {
onContentUpdate(bufferObj.data.text);
}
}
buffer = lines[lines.length - 1];
}
}
}
module.exports = getSummaryFromUrl;
[DifyのAPIキーを貼り付け]
の部分を、先ほど取得した Dify の API キーに置き換えてください。
response_mode: "streaming"
を指定することで、Server-Sent Events(SSE)形式でレスポンスが返却されます。これにより、要約の生成過程をリアルタイムで取得し、ノートに反映することが可能になります。
今回は 「text_chunk
イベントであれば要約の生成結果だと見なして保存」としていますが、ワークフローで利用する LLM が複数の場合は困るかもしれません。 text_chunk
イベントのレスポンスにはどのノードで実行されたかという情報が含まれていないためです。
もし困った場合は無理にリアルタイムで更新せずに、 node_finished
イベントで最終結果を取り出す形でも実現できます。
2. テンプレートファイルの作成
次に、ノートの作成を行うテンプレートファイルを作成します。以下の内容で url-summary.md
というファイルを作成し、テンプレートフォルダに保存します。(自分は templater
フォルダにしています。)
<%*
// 実行時に URL を入力するプロンプトを出す
const url = await tp.system.prompt("Please enter a URL");
// わかりやすさのために、自分は記事のノートのプレフィックスとして「📰」をつけています
// この辺りのフォーマットはお好みで変更してください。
const handleUpdateTitle = async (newTitle) => {
await tp.file.rename(`📰${newTitle}`);
tR += `[${newTitle}](${url})\n`;
}
const handleUpdateContent = (newContent) => {
tR += newContent;
}
await tp.user.getSummaryFromUrl(url, handleUpdateTitle, handleUpdateContent);
%>
3. Templater の設定
Templater の設定で、テンプレートフォルダと getSummaryFromUrl.js
を保存したスクリプトフォルダのパスを指定してください。
これで設定は完了です。コマンドパレットを開き「Templater: Create new note from template」を実行すると、記事要約を含めてノートに保存されました!
おわりに
今回は Dify の API アクセスの実践例として、Obsidian からワークフローを呼び出して結果をノートに保存する方法を紹介しました。 今回はある程度シンプルなワークフローでしたが、カスタマイズすることで単なる記事要約に限らず様々な用途に使えるかと思います。
余談ですが、最近はノーコードで実現可能なことは可能な限りそちらに寄せる方が構築スピードや試行錯誤のしやすさから良いと考え、Dify や Zapier を試している面もあります。しかしプログラミングはどうしても楽しいので、結局この記事の例でもコーディングしてしまいました。うまくバランスを取って両方利活用していきたいですね。