【Dify × Slack】話題ニュースをAIで取得して朝9時に通知してくれる機能作ってみた
はじめに
相変わらずDify熱が下がらず、日々色々なPoCをやっています。
ところでみなさんニュースは毎日見ていますか??ニュースアプリを開いてニュースを閲覧。。余計な興味ないニュースも入ってきて自分は正直疲れてしまいます。
というわけで、AIで関心があるニュースのみ取得して、勝手にSlackに送ってくれる機能を作ってみました。
作ったもの
朝9時に、Slackの自分のチャンネルにビジネス関連の最新話題ニュースを3つ取得してくる。
アーキテクチャー
現在、Slack上には、Difyと連携させる機能がありません。
なので、今回は、GCP(Google Cloud Platform)ベースでSlackAPIを連携させました。DifyとSlackAPIで作ったものを、Cloud Functionsでファンクション化し、Cloud Schedulerを使って、毎朝9時に定期通知するように設計します。
手順
1. Difyでワークフローを作成 (2024/8/15 更新)
Difyからワークフローでアプリを作ります。
- HTTPリクエストで、yahooニュースのRSSから経済に関するニュースを取得。
- 1で取得したきた結果から上から3つニュースを取得するように加工
- 取得したきた3つのニュース情報をループ処理で要約
- 3の出力内容を微調整して加工
- Slackのチャンネルに通知
こちらが作成したDSLです。参考にどうぞ。
DSL
app:
description: 最新のニュースをyahooニュースから取得するように改良。
icon: 🤖
icon_background: '#FFEAD5'
mode: workflow
name: Newアプリ改良版
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: http-request
id: 1718076250420-source-1718076269129-target
selected: false
source: '1718076250420'
sourceHandle: source
target: '1718076269129'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: http-request
targetType: code
id: 1718076269129-source-17235252683910-target
selected: false
source: '1718076269129'
sourceHandle: source
target: '17235252683910'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: code
targetType: iteration
id: 17235252683910-source-17235282227240-target
selected: false
source: '17235252683910'
sourceHandle: source
target: '17235282227240'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: true
iteration_id: '17235282227240'
sourceType: llm
targetType: tool
id: 1723529086478-source-1723530096605-target
selected: false
source: '1723529086478'
sourceHandle: source
target: '1723530096605'
targetHandle: target
type: custom
zIndex: 1002
- data:
isInIteration: true
iteration_id: '17235282227240'
sourceType: tool
targetType: llm
id: 1723530096605-source-1723530564634-target
selected: false
source: '1723530096605'
sourceHandle: source
target: '1723530564634'
targetHandle: target
type: custom
zIndex: 1002
- data:
isInIteration: true
iteration_id: '17235282227240'
sourceType: llm
targetType: llm
id: 1723530564634-source-1723532664762-target
source: '1723530564634'
sourceHandle: source
target: '1723532664762'
targetHandle: target
type: custom
zIndex: 1002
- data:
isInIteration: false
sourceType: iteration
targetType: code
id: 17235282227240-source-1723533052035-target
source: '17235282227240'
sourceHandle: source
target: '1723533052035'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: code
targetType: tool
id: 1723533052035-source-1723544419745-target
source: '1723533052035'
sourceHandle: source
target: '1723544419745'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: tool
targetType: end
id: 1723544419745-source-1723544409743-target
source: '1723544419745'
sourceHandle: source
target: '1723544409743'
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ''
selected: false
title: 開始
type: start
variables: []
height: 53
id: '1718076250420'
position:
x: 28.42253885859111
y: 328.838586091834
positionAbsolute:
x: 28.42253885859111
y: 328.838586091834
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
authorization:
config: null
type: no-auth
body:
data: ''
type: none
desc: ''
headers: ''
method: get
params: ''
selected: false
timeout:
max_connect_timeout: 0
max_read_timeout: 0
max_write_timeout: 0
title: HTTPリクエスト
type: http-request
url: https://news.yahoo.co.jp/rss/topics/business.xml
variables: []
height: 105
id: '1718076269129'
position:
x: 326.2615840057931
y: 328.838586091834
positionAbsolute:
x: 326.2615840057931
y: 328.838586091834
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
code: "import re\n\ndef main(content: str) -> dict:\n # <channel>タグ内の<item>タグを取得するための正規表現パターン\n\
\ item_pattern = r'<item>(.*?)</item>'\n items = re.findall(item_pattern,\
\ content, re.DOTALL)\n\n extracted_items = []\n for item in items[:3]:\
\ # 最初の3つの<item>タグを処理\n title_match = re.search(r'<title>(.*?)</title>',\
\ item)\n link_match = re.search(r'<link>(.*?)</link>', item)\n \
\ description_match = re.search(r'<description>(.*?)</description>',\
\ item)\n \n title = title_match.group(1) if title_match else\
\ \"No Title\"\n link = link_match.group(1) if link_match else \"\
No Link\"\n description = description_match.group(1) if description_match\
\ else \"No Description\"\n \n extracted_items.append({\n\
\ \"title\": title,\n \"link\": link,\n \
\ \"description\": description\n })\n\n return {\n \"\
items\": extracted_items\n }\n\n# テスト実行\ncontent = \"\"\"<?xml version=\"\
1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<rss version=\"2.0\">\n \
\ <channel>\n <language>ja</language>\n <copyright>© LY Corporation</copyright>\n\
\ <pubDate>Tue, 13 Aug 2024 02:57:07 GMT</pubDate>\n <title>Yahoo!ニュース・トピックス\
\ - 経済</title>\n <link>https://news.yahoo.co.jp/topics/business?source=rss</link>\n\
\ <description>Yahoo! JAPANのニュース・トピックスで取り上げている最新の見出しを提供しています。</description>\n\
\ <item>\n <title>日経平均、一時1000円超の上昇</title>\n <link>https://news.yahoo.co.jp/pickup/6510536?source=rss</link>\n\
\ <pubDate>Tue, 13 Aug 2024 01:11:04 GMT</pubDate>\n <comments>https://news.yahoo.co.jp/articles/986a9041d5f46a9453aa5d36fdb7ec58e099f985/comments</comments>\n\
\ </item>\n <item>\n <title>5日の東京株急落「恐怖」が影響か</title>\n <link>https://news.yahoo.co.jp/pickup/6510527?source=rss</link>\n\
\ <pubDate>Tue, 13 Aug 2024 00:14:34 GMT</pubDate>\n <comments>https://news.yahoo.co.jp/articles/3616b637bb7c6080fe9ac324545198a3de31c759/comments</comments>\n\
\ </item>\n <item>\n <title>NY株が3日ぶり反落 140ドル安</title>\n \
\ <link>https://news.yahoo.co.jp/pickup/6510521?source=rss</link>\n \
\ <pubDate>Mon, 12 Aug 2024 23:17:20 GMT</pubDate>\n <comments>https://news.yahoo.co.jp/articles/75e7f81e32fe32ec451f6b21adec01c347185da3/comments</comments>\n\
\ </item>\n </channel>\n</rss>\"\"\"\n\nresult = main(content)\nfor\
\ item in result['items']:\n print(item)\n"
code_language: python3
dependencies: []
desc: ''
outputs:
items:
children: null
type: array[object]
selected: false
title: XMLを変換
type: code
variables:
- value_selector:
- '1718076269129'
- body
variable: content
height: 53
id: '17235252683910'
position:
x: 660.8349467343194
y: 328.838586091834
positionAbsolute:
x: 660.8349467343194
y: 328.838586091834
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
height: 402
iterator_selector:
- '17235252683910'
- items
output_selector:
- '1723532664762'
- text
output_type: array[string]
selected: false
startNodeType: llm
start_node_id: '1723529086478'
title: 記事要約ループ処理
type: iteration
width: 1588
height: 402
id: '17235282227240'
position:
x: 1012.8625966689151
y: 328.838586091834
positionAbsolute:
x: 1012.8625966689151
y: 328.838586091834
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 1588
zIndex: 1
- data:
context:
enabled: false
variable_selector: []
desc: ''
isInIteration: true
isIterationStart: true
iteration_id: '17235282227240'
model:
completion_params:
temperature: 0.7
mode: chat
name: gpt-4
provider: openai
prompt_template:
- id: 4e081ddd-a407-4fba-a8b2-55c1f1954fce
role: system
text: '{{#17235282227240.item#}}内に含まれているlinkを出力して。'
selected: false
title: URL抽出
type: llm
variables: []
vision:
enabled: false
extent: parent
height: 97
id: '1723529086478'
parentId: '17235282227240'
position:
x: 117
y: 85
positionAbsolute:
x: 1129.862596668915
y: 413.838586091834
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
zIndex: 1001
- data:
desc: ''
isInIteration: true
iteration_id: '17235282227240'
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:
request_params:
type: mixed
value: ''
url:
type: mixed
value: '{{#1723529086478.text#}}'
type: tool
extent: parent
height: 297
id: '1723530096605'
parentId: '17235282227240'
position:
x: 420
y: 85
positionAbsolute:
x: 1432.862596668915
y: 413.838586091834
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
zIndex: 1002
- data:
context:
enabled: false
variable_selector: []
desc: ''
isInIteration: true
iteration_id: '17235282227240'
model:
completion_params:
temperature: 0.7
mode: chat
name: gemini-1.5-flash-latest
provider: google
prompt_template:
- id: 51c6fee7-4e70-4064-adbb-b3d7394f38df
role: system
text: あなたは優秀な記者です。{{#1723530096605.text#}}を50字以内に要約して。
- id: 36e43ff3-73e3-4b90-a40a-194ebab6ec8f
role: assistant
text: 日本語
selected: false
title: 記事要約
type: llm
variables: []
vision:
configs:
detail: high
enabled: true
extent: parent
height: 97
id: '1723530564634'
parentId: '17235282227240'
position:
x: 739.1100297557973
y: 86.79000330619976
positionAbsolute:
x: 1751.9726264247124
y: 415.62858939803374
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
zIndex: 1002
- data:
context:
enabled: false
variable_selector: []
desc: ''
isInIteration: true
iteration_id: '17235282227240'
model:
completion_params:
temperature: 0.7
mode: chat
name: gpt-4
provider: openai
prompt_template:
- id: 099cee5c-8286-4fc7-81c4-b7dbd6ce8334
role: system
text: "あなたは、エンジニアです。#出力形式に沿ってニュースを{{#17235282227240.index#}}分出力してください。\n\
承知しましたなどの会話は入りません。ただ以下の #出力形式と #出力条件に沿って、出力してください。\n\n\n\n#出力形式:\n\n{{変数A}\
\ . {{変数B}}\n \n要約:{{{{#1723530564634.text#}}}}\n \nURL: {{{{#1723529086478.text#}}}}\n\
\n#変数\nA = {{#17235282227240.index#}} + 1\nB = {{#17235282227240.item#}}のtitle\n\
\n#条件:\n・{{{#17235282227240.item#}}のtitle}}の部分には、クオーテーションマークは要りません。除いて出力してください。\n"
selected: false
title: 出力内容をAIで加工
type: llm
variables: []
vision:
enabled: false
extent: parent
height: 97
id: '1723532664762'
parentId: '17235282227240'
position:
x: 1095.024981036534
y: 85
positionAbsolute:
x: 2107.887577705449
y: 413.838586091834
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
zIndex: 1002
- data:
code: "def main(output: list) -> dict:\n # 最初の行に「今日の話題ニュースです!\U0001F4E3\
」を追加\n result_string = \"今日の話題ニュースです!\U0001F4E3\\n\\n\" + \"\\n\\n\"\
.join(output)\n\n return {\n \"result\": result_string\n }\n\
# def main(output: list) -> dict:\n# modified_output = []\n\n# for\
\ item in output:\n# modified_output.append(f\"今日の話題ニュースです!\U0001F4E3\
\\n{item}\")\n\n# # リストを改行で結合して1つの文字列にする\n# result_string = \"\\\
n\\n\".join(modified_output)\n\n# return {\n# \"result\": result_string\n\
# }\n\n# # テスト用データ\n# output = [\n# \"\\\"シャトレーゼ創業 斉藤寛氏が死去\\\"\\\
n \\n要約:\\\"シャトレーゼ創業者の斉藤寛氏が8月10日、心不全のため死去。享年90歳。\\\"\\n \\nURL: \\\"https://news.yahoo.co.jp/pickup/6510572?source=rss\\\
\"\",\n# \"\\\"日経平均 終値3万6000円台に戻す\\\"\\n \\n要約:日経平均株価は1207円高の3万6232円51銭で終値、7営業日ぶりに3万6000円台を回復しました。\
\ \\n \\nURL:https://news.yahoo.co.jp/pickup/6510571?source=rss\",\n# \
\ \"5日の東京株急落「恐怖」が影響か\\n \\n要約:8月5日、東京株式市場は過去最大の下げ幅を記録。日経平均株価は4451円安の3万1458円に。米経済悪化懸念が背景とみられる。\
\ \\n \\nURL:https://news.yahoo.co.jp/pickup/6510527?source=rss\"\n# ]\n\
\n# result = main(output)\n# print(result[\"result\"])\n\n"
code_language: python3
desc: ''
outputs:
result:
children: null
type: string
selected: false
title: 微調整
type: code
variables:
- value_selector:
- '17235282227240'
- output
variable: output
height: 53
id: '1723533052035'
position:
x: 2820.1861719512117
y: 373.01962648073305
positionAbsolute:
x: 2820.1861719512117
y: 373.01962648073305
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
outputs: []
selected: false
title: 終了
type: end
height: 53
id: '1723544409743'
position:
x: 3448.2130080489633
y: 373.01962648073305
positionAbsolute:
x: 3448.2130080489633
y: 373.01962648073305
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
provider_id: slack
provider_name: slack
provider_type: builtin
selected: false
title: Incoming Webhook to send message
tool_configurations:
webhook_url: https://hooks.slack.com/services/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
tool_label: Incoming Webhook to send message
tool_name: slack_webhook
tool_parameters:
content:
type: mixed
value: '{{#1723533052035.result#}}'
type: tool
height: 89
id: '1723544419745'
position:
x: 3137.383322223892
y: 373.01962648073305
positionAbsolute:
x: 3137.383322223892
y: 373.01962648073305
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: 33.051700138396654
y: 172.7325083368391
zoom: 0.6339300371652073
2. Slackと連携
Slack側で、WebhookとなるURLを発行します。ここでどのチャンネルに投稿させられるか設定できます。以下の記事が参考になりましたので、引用させていただきます。
そのあと、Difyに戻り、Slack側で発行したWebhook URLを SlackノードのSLACK INCOMING WEB HOOK URLに入れます。
あと、入力変数には、LLMの結果を出力させるようにしてください。
ここまで設定し、実際にDify上でワークフローを手動で動かしてみると以下のように指定したチャンネルに通知が来ることがわかります。
3. DifyでAPIキー発行
左の概要ページに行き、以下のAPIキーをクリックする。
「新しいシークレットキーを作成」をクリックし、APIキーを発行しましょう。キーは後ほどGCP上で必要となるのでコピーしておいてください。
4. GCPと連携
まず、GCP上でプロジェクトを作ります。アカウントがない方は、以下から作成してください。
4-1. APIキーを保存
GCP Consoleで「APIとサービス」をクリックし、ダッシュボードに移動し、「Secret Manager API」を探して、「有効にする」をクリックする。
GCPコンソールから、「セキュリティ」を選択し、そこから「Secret Manager」に移動し、「シークレットを作成」をクリックする。
以下の入力して、「シークレット作成」を押下する。
- 名前
- dify-api-key(好きな名前でOKです)
- シークレットの値
- コピーしたDifyのAPI key
- コピーしたDifyのAPI key
4-2.Cloud Fuctionを作成
GCP Consoleで「Cloud Functions」をクリックし、ダッシュボードに移動し、「ファンクションの作成」をクリックし、新規作成しましょう。
以下の設定でファンクションを作成します。
- 環境
- 第1世代
- 関数名
- 任意の関数名
- リージョン
- asia-northeast1 (場所によって変えてください)
- トリガーのタイプ
- HTTP
- 認証
- 認証が必要
- HTTPSが必須にチェック
そのあと、以下のコードを書きます。
index.js
const {SecretManagerServiceClient} = require('@google-cloud/secret-manager');
const axios = require('axios');
const secretManager = new SecretManagerServiceClient();
async function getSecret(name) {
const [version] = await secretManager.accessSecretVersion({name: name});
return version.payload.data.toString();
}
exports.executeDifyApi = async (req, res) => {
try {
// Secret Manager からDify API Keyを取得
const apiKey = await getSecret('projects/YOUR_GCP_PROJECT_NAME/secrets/dify-api-key/versions/latest');
// Dify APIを呼び出し
const response = await axios.post('https://api.dify.ai/v1/workflows/run', {
inputs: {}, // Dify APIに必要な入力パラメータ
response_mode : "blocking",
user: "test",
}, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
console.log('Dify API response:', response.data);
res.status(200).send('Dify API call successful');
} catch (error) {
console.error('Error calling Dify API:', error);
res.status(500).send('Dify API call failed');
}
};
package.js
{
"dependencies": {
"@google-cloud/secret-manager": "^4.0.0",
"axios": "^0.21.1"
}
}
書いた後、テストしてみて、問題なく呼び出せているようでしたら、そのままデプロイしてください。
4-3.アクセス権の付与
GCP Consoleから「IAMと管理」を選択し、「アクセス権を付与」をクリックする。これがないと、Cloud Function呼び出し時に、403エラーが出ます。
以下の入力し、保存をクリック。
- 新しいプリンシパル
- サービスカウントを入力する
- ロールを選択
- Secret Managerのシークレットアクセサー
サービスアカウントは、Cloud Functionsの詳細から確認できます。
また、Cloud Functionsに戻り、「権限」をクリックし、「プリンシパルを追加」をクリック。
以下の入力し、保存をクリック。
- 新しいプリンシパル
- 先ほど入力したサービスカウント
- ロールを選択
- Cloud Functions起動元
- Cloud Functions起動元
4-4.Cloud Schedulerを作成
GCP Console で Cloud Scheduler に移動し、「ジョブを作成」 をクリックする。
以下の設定を行います:
- 名前
- 任意の名前
- リージョン
- Cloud Functions設定時と同じもの
- 説明
- 入れなくてもOK!
- 頻度
- cron 形式で指定 (例: 毎日午前9時に実行する場合 0 9 * * *)。詳細はこちらを参照してみてください。
- ターゲットタイプ
- HTTP
- URL
- 作成したCloud FunctionsのトリガーとなるURL
- HTTPヘッダー
- 設定しなくてOK
- Authヘッダー
- OIDCトークン
- サービスアカウント
- 作成したCloud Functionsのサービスアカウントを選択
その後のオプションの設定はデフォルトの設定値で大丈夫です。もし気になる場合は、ご自身で好きな値にしてください!
ちなみに、トリガーURLはCloud Functionsのトリガーから確認できます。
これで設定は完了です!
実際に動かしてみると、朝9時に設定したチャンネル(今回はSlack上の自分のチャンネル)に、きちんと通知がくるようになりました!!
まとめ
今回は、GCPとSlackとDifyを組み合わせて、Slackに通知する仕組みを作りました。AIのロジックの部分をDifyでサクッと作れるのはいいですね!
ますますDifyの将来性に期待です!!
Discussion