Automatic Ruby で、複数の公開ページから記事を集め、重複を除外し、本文を 1 つのテキストにまとめたうえで、AI サービスへ 1 回だけ渡す Recipe を組みます。AI Tutorial でも、CustomFeedWeb、StorePermalink、FilterFullFeed、FilterSanitize、FilterJoin、FilterSakuraAI、PublishMarkdown を順に並べる構成を示しています[1]。
この記事では、そのチュートリアルを日本語でなぞりながら、さくらの AI Engine を使って、前回実行後に見つかったニュースを 1 本の日本語ダイジェストとして Markdown に保存します。
完成形は次の流れです。
CustomFeedWeb
↓
StorePermalink
↓
FilterFullFeed
↓
FilterSanitize
↓
FilterJoin
↓
FilterSakuraAI
↓
PublishMarkdown
以前、閲覧中の長い Web ページをさくらの AI Engine で要約する Chrome 拡張 web-digest を作りました[2]。今回はブラウザで開いている 1 ページではなく、Automatic Ruby が継続的に収集する複数の記事を対象にします。
前提環境
2026 年 8 月 22 日時点では、FilterSakuraAI、FilterJoin、CustomFeedWeb などを含む v26.08 は Release Date: TBD です[3]。そのため、この記事では RubyGems からインストールした既存リリースではなく、GitHub の master をチェックアウトして実行します。
Automatic Ruby では現在、Unix 系 OS と Ruby 3.3 から 4.0 をサポート対象としています。ソースコードから実行する場合は、利用するプラグインに応じて Bundler のオプショングループ を追加します[4]。
まず Ruby と Bundler を確認します。
ruby -v
bundle -v
Bundler がなければインストールします。
gem install bundler
Automatic Ruby をチェックアウトします。
git clone https://github.com/id774/automaticruby.git
cd automaticruby
今回の Recipe では、HTML を読むための html、SQLite へ処理済み URL を保存するための store、HTML をテキストへ落とすための sanitize が必要です。3 つをまとめて有効にしてから依存 Gem を入れます。
bundle config set --local with "html store sanitize"
bundle install
動作確認とユーザーディレクトリの作成を行います。
bundle exec bin/automatic --version
bundle exec bin/automatic scaffold
~/.automatic/config、~/.automatic/db、~/.automatic/assets などが作られれば準備完了です。
追記 (2026-08-22)
リリースされました。
automatic 26.08
したがって GitHub の最新にこだわらなければ gem install でもインストールすることができます。
gem install automatic
さくらの AI Engine のトークンを用意する
さくらの AI Engine では、コントロールパネルからアカウントトークンを発行します。トークンは <UUID>:<シークレット> の形式で、再表示されないため発行時に安全な場所へ保存します。Chat Completions の OpenAI 互換エンドポイントは https://api.ai.sakura.ad.jp/v1/chat/completions です[5]。
公式手順では、gpt-oss-120b を指定した curl の例が掲載されています。Automatic Ruby に組み込む前に、まず API 単体で疎通を確認しておくと切り分けが楽です。
curl --location 'https://api.ai.sakura.ad.jp/v1/chat/completions' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer <Token>' \
--header 'Content-Type: application/json' \
--data '{
"model": "gpt-oss-120b",
"messages": [
{
"role": "system",
"content": "こんにちは"
}
],
"temperature": 0.7,
"max_tokens": 200,
"stream": false
}'
<Token> は実際に発行したアカウントトークンへ置き換えます。choices 以下に応答が返れば、Automatic Ruby 側の設定へ進めます。
まず AI を使わずニュースを集める
最初から AI を呼ぶのではなく、ニュースの取得、重複排除、本文取得、テキスト化、Markdown 出力までを先に確認します。ここで本文が空なら、後ろに AI を足しても要約できる情報は増えません。
~/.automatic/config/ai-digest.yml を作成します。
plugins:
- module: CustomFeedWeb
config:
retry: 2
interval: 2
sites:
- name: Python Insider
url: https://blog.python.org/
link_selector: 'a[href]'
include:
- '^https://blog\.python\.org/20[0-9]{2}/[0-9]{2}/[^/]+/?$'
fetch_items: 20
- name: Rust Blog
url: https://blog.rust-lang.org/
link_selector: 'a[href]'
include:
- '^https://blog\.rust-lang\.org/20[0-9]{2}/[0-9]{2}/[0-9]{2}/[^/]+/?$'
fetch_items: 20
- name: The Go Blog
url: https://go.dev/blog/
link_selector: 'a[href]'
include:
- '^https://go\.dev/blog/[^/]+$'
fetch_items: 20
- module: StorePermalink
config:
db: ai-digest-test.db
- module: FilterFullFeed
config:
siteinfo: items_all.json
- module: FilterSanitize
config:
mode: restricted
- module: PublishMarkdown
config:
file: ~/.automatic/markdown/ai-digest-test.md
mode: append
CustomFeedWeb は各一覧ページを読み、include に一致する記事 URL だけを item にします。fetch_items: 20 は各サイトから最大 20 件を対象にする設定です。
StorePermalink は item の URL を SQLite に記録し、すでに処理した URL を後段へ流しません。ここでは段階確認用として ai-digest-test.db を使い、本番用の DB と分けます。DB ファイルの実体は ~/.automatic/db 以下に置かれます。
FilterFullFeed は URL と siteinfo の URL パターンを照合し、対応する XPath で記事本文を取得します。FilterSanitize は取得した HTML を後段で扱いやすいテキストへ落とします。最後の PublishMarkdown は残った item を Markdown ファイルへ追記します。
実行します。
cd ~/automaticruby
bundle exec bin/automatic -c ~/.automatic/config/ai-digest.yml
チェックアウト先が ~/automaticruby ではない場合は、そのディレクトリへ読み替えます。
出力を確認します。
sed -n '1,120p' ~/.automatic/markdown/ai-digest-test.md
同時にログも確認します。特に FilterFullFeed が本文を取得できているかを見ます。
FilterFullFeed が本文を取れているか確認する
ここは今回の Recipe で最も注意が必要です。
Automatic Ruby に同梱されている items_all.json は古い LDRFullFeed の siteinfo を元にしたスナップショットです。AI Tutorial では、Python Insider、Rust Blog、The Go Blog の 3 サイトについて、同梱 siteinfo が一致せず、次のようなログになることが明記されています[1]。
Fulltext SITEINFO not found: https://go.dev/blog/pkgsite-api
この状態では FilterFullFeed が item を壊すことはありませんが、一覧ページ側に description がなければ、タイトルと URL だけが残ります。タイトルと URL だけを結合して AI へ渡しても、記事本文のダイジェストにはなりません。
本文を入れる方法は 2 つあります。
- 一覧ページに要約本文が載っているサイトでは、
CustomFeedWebのitem_selectorとdescription_selectorを設定して、その場でdescriptionを作ります。 - 記事ページから全文を取る場合は、現在の対象サイトに合う
siteinfoを~/.automatic/assets/siteinfo/に用意し、FilterFullFeedのsiteinfoで指定します。
どちらを使う場合も、AI を追加する前に Markdown を開き、各 item に実際の文章が入っていることを確認します。
同じニュースを 2 回送らない
次に、同じ Recipe をもう一度実行します。
bundle exec bin/automatic -c ~/.automatic/config/ai-digest.yml
新しい記事がなければ、StorePermalink が既存 URL を除外するため、後段で処理される item は大きく減ります。ここを確認してから AI を追加します。
AI API を使う Recipe では、この順序が特に重要です。重複排除を AI より前に置けば、同じ記事を繰り返し送らずに済みます。
この段階では ai-digest-test.db を使っているため、後で完成版 Recipe を試すときは本番用の ai-digest.db が空の状態から始まります。段階確認と本番用で同じ DB を使うと、先に確認した記事が処理済みになり、AI を追加した直後の実行で何も渡らないことがあります。
また、StorePermalink は FilterJoin より前に置く必要があります。FilterJoin は複数の記事を 1 つの item にまとめるため、結合後の item は単一の記事 URL を持ちません。URL をキーにする store プラグインをその後ろへ置くと、期待した重複排除になりません[1]。
複数の記事を FilterJoin で 1 本にまとめる
ここからはプラグインを追加したときの差分を確認します。すでに ai-digest-test.db へ URL を記録しているため、完成版の実行確認は後述する新しい ai-digest.db で行います。
本文まで取得できたら、FilterSanitize と PublishMarkdown の間に FilterJoin を追加します。
- module: FilterSanitize
config:
mode: restricted
- module: FilterJoin
config:
title: Daily Digest
- module: PublishMarkdown
config:
file: ~/.automatic/markdown/ai-digest.md
mode: append
FilterJoin は複数の item を 1 つの item にまとめるだけのプラグインです。HTTP 通信も AI 呼び出しも行いません。
たとえば 2 記事あれば、結合後の description は概ね次の形になります。
ARTICLE 1
Title: Enabling Polonius on nightly
URL: https://blog.rust-lang.org/2026/08/04/enabling-polonius-alpha-on-nightly/
記事 1 の本文
ARTICLE 2
Title: Extending the pkgsite API
URL: https://go.dev/blog/pkgsite-api
記事 2 の本文
この時点でも Recipe は単独で使えます。AI を呼ばず、その日に見つかった記事を 1 本の Markdown へまとめるだけなら、ここで終了できます。
FilterSakuraAI で 1 日分をまとめて要約する
FilterJoin と PublishMarkdown の間に FilterSakuraAI を追加します。
- module: FilterJoin
config:
title: Daily Digest
- module: FilterSakuraAI
config:
token: YOUR_SAKURA_AI_TOKEN
model: gpt-oss-120b
prompt: |
以下の記事群について、個別記事の要約を羅列するのではなく、
全体を一つのダイジェストとして日本語で要約してください。
retry: 2
interval: 2
- module: PublishMarkdown
config:
file: ~/.automatic/markdown/ai-digest.md
mode: append
token はさくらの AI Engine で発行した実際のアカウントトークンへ置き換えます。
FilterSakuraAI は item の description をさくらの AI Engine へ渡し、返ってきた回答で description を置き換えます。今回の並びでは FilterJoin の出力が 1 つの item なので、複数記事があっても AI への問い合わせは結合後の 1 つの item に対して 1 回です[1]。
逆に、次の順序にすると意味が変わります。
FilterSanitize
↓
FilterSakuraAI
↓
FilterJoin
この場合は記事ごとに FilterSakuraAI が動きます。3 記事なら 3 回問い合わせ、その 3 個の要約を最後に結合します。
「記事ごとの短い要約一覧」が欲しいなら AI を前に置き、「その日のニュース全体を 1 本のダイジェストとして読みたい」なら FilterJoin を前に置きます。この記事では後者を使います。
完成した Recipe
最終的な ~/.automatic/config/ai-digest.yml は次の形です。
plugins:
- module: CustomFeedWeb
config:
retry: 2
interval: 2
sites:
- name: Python Insider
url: https://blog.python.org/
link_selector: 'a[href]'
include:
- '^https://blog\.python\.org/20[0-9]{2}/[0-9]{2}/[^/]+/?$'
fetch_items: 20
- name: Rust Blog
url: https://blog.rust-lang.org/
link_selector: 'a[href]'
include:
- '^https://blog\.rust-lang\.org/20[0-9]{2}/[0-9]{2}/[0-9]{2}/[^/]+/?$'
fetch_items: 20
- name: The Go Blog
url: https://go.dev/blog/
link_selector: 'a[href]'
include:
- '^https://go\.dev/blog/[^/]+$'
fetch_items: 20
- module: StorePermalink
config:
db: ai-digest.db
- module: FilterFullFeed
config:
siteinfo: items_all.json
- module: FilterSanitize
config:
mode: restricted
- module: FilterJoin
config:
title: Daily Digest
- module: FilterSakuraAI
config:
token: YOUR_SAKURA_AI_TOKEN
model: gpt-oss-120b
prompt: |
以下の記事群について、個別記事の要約を羅列するのではなく、
全体を一つのダイジェストとして日本語で要約してください。
retry: 2
interval: 2
- module: PublishMarkdown
config:
file: ~/.automatic/markdown/ai-digest.md
mode: append
ただし、前述のとおり、この 3 サイトは同梱の items_all.json だけでは現在の本文を取得できないことがあります。実運用では、対象サイトの本文が description に入るよう CustomFeedWeb のセレクタまたは独自 siteinfo を調整したうえで使います。
Recipe を実行します。
cd ~/automaticruby
bundle exec bin/automatic -c ~/.automatic/config/ai-digest.yml
出力を確認します。
sed -n '1,160p' ~/.automatic/markdown/ai-digest.md
Daily Digest の下に、さくらの AI Engine が返した日本語のダイジェストが入れば成功です。
トークンを含む Recipe は秘密ファイルとして扱う
FilterSakuraAI の token は Recipe に直接書くため、その YAML ファイル自体が秘密情報になります。Automatic Ruby の運用ドキュメントでも、認証情報を含む Recipe を commit せず、ファイル権限を制限するよう定めています[4]。
chmod 600 ~/.automatic/config/ai-digest.yml
~/.automatic/config をそのまま Git 管理する運用は避けます。サンプルを公開する場合は、実トークンを YOUR_SAKURA_AI_TOKEN のようなプレースホルダーへ戻します。
retry と interval は回答品質ではなく通信失敗に対する設定
FilterSakuraAI の retry と interval は、AI に「もう一度考えさせる」ための設定ではありません。
AI Tutorial では、timeout、HTTP 429、HTTP 5xx のような一時的な失敗を再試行し、設定不備、拒否されたリクエスト、不正な JSON、期待した応答形式がない場合はそのまま終了する設計になっています[1]。
retry: 2
interval: 2
この例では、再試行可能な失敗に対して最大 2 回の retry を設定し、再試行の間を 2 秒空けます。
失敗時に空文字列を Markdown へ書いて成功扱いにはしません。AI の回答を取得できなければ Recipe はそこで失敗するため、ログと終了コードで検知できます。
StorePermalink より後ろの失敗はロールバックされない
Automatic Ruby の Recipe には、後段が失敗したときに前段の副作用を巻き戻すトランザクションはありません。StorePermalink が URL を DB へ記録した後で FilterSakuraAI が失敗した場合、その URL は処理済みのまま残ります[4]。
そのため、Recipe を組み立てている間は本番 DB と確認用 DB を分けます。本番運用では終了コードとログを確認し、AI 呼び出しが失敗した日の記事を再処理する必要がある場合は、DB の状態を確認したうえで復旧します。
この制約は、StorePermalink を AI より前に置いて重複送信を防ぐこととの交換条件です。現行の「複数記事を結合して 1 回だけ AI へ渡す」構成では、URL 単位の記録と結合後の AI 呼び出しを 1 つのトランザクションにはできません。
長い入力はサイト数と fetch_items で抑える
複数の記事本文を FilterJoin でまとめると、AI へ送る 1 回の入力は大きくなります。モデルが受け付ける入力上限を超えれば、さくらの AI Engine 側のエラーで Recipe は終了します。
現行の AI Tutorial には、長文を自動的に複数チャンクへ分割し、個別処理後に再統合する専用プラグインはありません[1]。
まずは次の 3 点で入力を制御します。
-
sitesを必要な情報源だけに絞る -
fetch_itemsを小さくする -
includeを狭くして記事以外を混ぜない
たとえば動作確認専用の Recipe では fetch_items: 3 程度まで下げ、本文取得と AI 応答を確認してから本番値へ戻す方が安全です。ただし、本番で値を小さくしすぎると、更新が多い日に一覧の上位だけを取得し続け、下位の記事まで到達できない可能性があります。日次運用では、1 日の最大更新件数より十分に大きい値を設定します。
cron で 1 日 1 回実行する
Automatic Ruby 自体は常駐しません。1 回の起動で Recipe を実行して終了し、繰り返し実行は cron など外部の スケジューラ に任せます[4]。
今回のようにソースコードから実行する場合は、長い cron 行へ bundle exec を直接書くより、小さな ラッパースクリプト を用意した方が扱いやすくなります。
まずログディレクトリを作ります。
mkdir -p ~/.automatic/log
次に、たとえば ~/bin/automatic-ai-digest を作ります。
#!/bin/sh
set -eu
cd "$HOME/automaticruby"
exec /ABSOLUTE/PATH/TO/bundle exec bin/automatic \
-c "$HOME/.automatic/config/ai-digest.yml"
/ABSOLUTE/PATH/TO/bundle は、対話シェルで次を実行して確認した絶対パスへ置き換えます。
command -v bundle
実行権限を付けます。
chmod 700 ~/bin/automatic-ai-digest
毎朝 7 時に動かす場合は、crontab -e へ次を追加します。
0 7 * * * $HOME/bin/automatic-ai-digest >> $HOME/.automatic/log/ai-digest.log 2>&1
ここでいう「1 日分」は、厳密に JST の 0 時から 24 時までに公開された記事を日付条件で抽出する意味ではありません。StorePermalink にまだ記録されていない URL、つまり前回実行後に新しく検出された記事を次回の処理対象にする方式です。
そのため初回実行では、一覧ページに残っている過去記事も対象になります。2 回目以降を 1 日 1 回の間隔で動かすことで、実質的に「前回実行以降の新着ニュース」を日次でまとめる運用になります。
まとめ
Automatic Ruby で日次ニュース要約を作る場合、AI 用の大きな専用処理を書く必要はありません。
discover
↓
deduplicate
↓
fetch
↓
clean
↓
join
↓
summarize with Sakura AI Engine
↓
write Markdown
この中で FilterSakuraAI が担当するのは、受け取った 1 つの item のテキストをさくらの AI Engine へ渡し、返ってきたテキストで置き換える部分だけです。
日次ダイジェストとして使うときの判断点は 3 つです。
- AI を入れる前に、記事本文が
descriptionに入っていることを確認する -
StorePermalinkをFilterJoinより前に置き、処理済み記事を再送しない - 1 日分を 1 回で要約するなら、
FilterJoinの後ろにFilterSakuraAIを置く
この 3 点を守れば、収集元を増やす場合も、要約 prompt を変える場合も、Ruby コードを変更せず Recipe の設定だけで調整できます。
参考文献
- Automatic Ruby Developers, AI Tutorial(2026-08-17). https://github.com/id774/automaticruby/blob/master/doc/AI_TUTORIAL.md
- id774, 長い Web ページを構造に沿って要約する Chrome 拡張 web-digest を作った(2026-08-17). https://blog.id774.net/entry/2026/08/17/5517/
- Automatic Ruby Developers, automaticruby Repository Version History(2026-08-21). https://github.com/id774/automaticruby/blob/master/doc/VERSIONS
- Automatic Ruby Developers, Deployment(2026-08-17). https://github.com/id774/automaticruby/blob/master/doc/DEPLOYMENT.md
- さくらインターネット, 利用手順(2026-05-20). https://manual.sakura.ad.jp/cloud/ai-engine/02-howto.html