News Reminder(金融ニュース)とメールダイジェストの Slack 投稿を以下の観点でリニューアルする。
親メッセージ(日付ヘッダ)をチャンネルに投稿し、そのスレッドに詳細を返信する。同日に複数回実行された場合は既存スレッドに追記する。
| 項目 | News Reminder | メールダイジェスト |
|---|---|---|
| 親メッセージ | 📊 2026-03-16 金融ニュースダイジェスト | 📬 2026-03-16 メールダイジェスト |
| 投稿先 | #news-reminder | #mail-yuki, #mail-info |
| スレッド内容 | 銘柄ごとのニュース要約(Block Kit) | カテゴリ別メール要約(Block Kit) |
| 同日再実行 | 既存スレッドに追記 | 既存スレッドに追記 |
各チャンネル・日付ごとの親メッセージ ts を永続化し、同日再実行時に再利用する。
| パイプライン | ファイル |
|---|---|
| News Reminder | ops/news_reminder/data/thread_ts.json |
| メールダイジェスト | ops/mail_filing/data/digest_thread_ts.json |
{
"2026-03-16": {
"channel_id": "C0123ABC",
"ts": "1710561234.000100"
}
}
古いエントリは30日超で自動削除(冪等性維持 + ファイル膨張防止)。
1. 今日の日付キーで thread_ts.json を検索 2. 見つかった → そのスレッドに返信(thread_ts 指定) 3. 見つからない → a. 親メッセージを投稿(Block Kit header) b. 返された ts を thread_ts.json に保存 c. スレッドに詳細を返信
Block Kit 構成:
[
{ "type": "header", "text": "📊 金融ニュースダイジェスト" },
{ "type": "section", "text": "2026-03-16(月)| 3銘柄 / 7件のニュース" }
]
Block Kit 構成(1銘柄 = 1返信メッセージ):
[
{ "type": "header", "text": "{銘柄名}({ティッカー})" },
{ "type": "divider" },
// ニュース1件ごとに section
{
"type": "section",
"text": "⚠️ *<{url}|{要約}>* — {出典}\n→ 🔴 ネガティブ: {影響分析}"
},
// ... 最大3件
{ "type": "divider" },
{ "type": "context", "elements": [{ "type": "mrkdwn", "text": "💡 {銘柄別コメント}" }] }
]
Block Kit 構成(カテゴリごと = 1返信メッセージ。カテゴリ: 🔴 要対応 / 🟡 情報共有 / ⚪ 無視可能):
[
{ "type": "header", "text": "🔴 要対応({N}件)" },
{ "type": "divider" },
// メール1件ごとに section
{
"type": "section",
"text": "• *{送信者}* — {件名}\n {要点}"
},
// ... 件数分
]
summarize.py と digest.py の要約プロンプトを変更し、JSON 形式で出力させる。Block Kit 組み立てはPython側で行う。
{
"holdings": [
{
"ticker": "VOO",
"name": "S&P 500",
"articles": [
{
"summary": "FRBが利下げ見送りを示唆、市場に慎重ムード",
"source": "Reuters",
"url": "https://...",
"impact": "negative",
"impact_detail": "利下げ期待後退でインデックスに下押し圧力",
"alert": true
}
],
"comment": "銘柄別の所感"
}
],
"overall_comment": "全体の総合コメント"
}
{
"categories": [
{
"level": "action_required",
"emoji": "🔴",
"label": "要対応",
"items": [
{
"sender": "田中太郎",
"subject": "契約書の確認依頼",
"summary": "3月末までに署名が必要。添付PDFを確認のこと"
}
]
}
]
}
| ファイル | 変更 | 内容 |
|---|---|---|
ops/news_reminder/pipeline/steps/summarize.py |
変更 |
|
ops/news_reminder/pipeline/steps/notify.py |
変更 |
|
ops/mail_filing/pipeline/steps/digest.py |
変更 |
|
bot/scheduler.py |
変更 |
|
ops/news_reminder/app/config.py |
変更 | slack_channel のデフォルト値確認(変更不要の可能性あり) |
| 案 | 頻度 | メリット | デメリット |
|---|---|---|---|
| A. 平日のみ(推奨) | 月〜金 7:00 | 金融市場は平日のみ。土日のノイズ除去。週5→5回で変わらないが休日の通知がなくなる | 土日に重大ニュースがあった場合、月曜まで遅れる |
| B. 週3回 | 月・水・金 7:00 | 通知頻度が半減 | 火・木のニュースが翌日まで遅れる |
| C. 週1回 | 月 7:00 | 週次レビュー的な使い方 | タイムリー性が大幅に低下 |
推奨: 案A(平日のみ)。金融ニュースという性質上、市場が動く平日のみで十分。土日は市場が休場で新着が少なく、月曜に週末分もまとめて配信される。
# scheduler.py の news_scan 判定に曜日チェックを追加
import jpholiday # 既にops依存にあり
def _is_business_day(d: date) -> bool:
"""平日(月〜金)かつ祝日でないか判定"""
return d.weekday() < 5 and not jpholiday.is_holiday(d)
メールは毎日届くため、頻度調整は不要。現在手動実行のみだが、スケジューラに追加して毎朝 8:00 に自動実行する。
既に実装済み: scheduler.py の _run_mail_filing が 8:00 に mail_filing.pipeline run を実行しているが、これは添付ファイル振り分け用。ダイジェストは別コマンドのため追加が必要。
# scheduler.py に追加
MAIL_DIGEST_HOUR = 8 # mail_filing と同時刻でOK(非同期で順次実行)
# run() ループ内に追加
if (
now.hour == MAIL_DIGEST_HOUR
and self._last_runs.get("mail_digest") != today
):
self._last_runs["mail_digest"] = today
self._save_last_runs()
await self._run_mail_digest()
async def _run_mail_digest(self) -> None:
"""メールダイジェストを subprocess で実行する。"""
proc = await asyncio.create_subprocess_exec(
sys.executable, "-m", "mail_filing.pipeline", "digest",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=str(PROJECT_ROOT),
)
# ... タイムアウト・エラーハンドリングは _run_mail_filing と同様
現状3つの方式が混在している。
| ファイル | 現状 | 変更後 |
|---|---|---|
| notify.py | slack_sdk.WebClient(同期) | slack_sdk.WebClient(維持) |
| digest.py | httpx 直書き | slack_sdk.WebClient(統一) |
| scheduler.py | slack_sdk.AsyncWebClient | 維持(Bot本体から受け取るため) |
理由: Block Kit の blocks パラメータを正しく送信するには slack_sdk が確実。httpx 直書きは blocks の JSON シリアライズでミスが起きやすい。
from __future__ import annotations
from typing import TypedDict
class ArticleSummary(TypedDict):
summary: str
source: str
url: str
impact: str # "positive" | "negative" | "neutral"
impact_detail: str
alert: bool # ⚠️ マーク対象
class HoldingSummary(TypedDict):
ticker: str
name: str
articles: list[ArticleSummary]
comment: str
class NewsSummaryResult(TypedDict):
holdings: list[HoldingSummary]
overall_comment: str
# ts管理
def _load_thread_ts(data_dir: Path) -> dict[str, dict]:
"""thread_ts.json を読み込む。30日超のエントリは削除。"""
def _save_thread_ts(data_dir: Path, ts_data: dict[str, dict]) -> None:
"""thread_ts.json を保存する。"""
def _get_or_create_thread(
client: WebClient,
channel: str,
date_str: str,
data_dir: Path,
) -> str:
"""日付スレッドの ts を取得。なければ親メッセージを投稿して作成。"""
# メイン
def run(cfg: PipelineConfig, summary: NewsSummaryResult) -> None:
"""要約結果を日付スレッド方式・Block Kit で Slack に投稿する。"""
# 返り値の変更
async def run(cfg: PipelineConfig) -> NewsSummaryResult | None:
"""取得済みニュースを Claude CLI で要約する。構造化された辞書を返す。"""
# プロンプトにJSON出力指示を追加
# Claude CLI の出力を json.loads() でパース
# パース失敗時は最大1回リトライ(プロンプトに「JSONのみ出力せよ」を強調)
from __future__ import annotations
from typing import TypedDict
class DigestItem(TypedDict):
sender: str
subject: str
summary: str
class DigestCategory(TypedDict):
level: str # "action_required" | "info" | "ignorable"
emoji: str # "🔴" | "🟡" | "⚪"
label: str # "要対応" | "情報共有" | "無視可能"
items: list[DigestItem]
class DigestResult(TypedDict):
categories: list[DigestCategory]
# ts管理(notify.py と同じパターン)
def _load_thread_ts() -> dict[str, dict]: ...
def _save_thread_ts(ts_data: dict[str, dict]) -> None: ...
def _get_or_create_thread(
client: WebClient,
channel: str,
date_str: str,
mailbox_name: str,
) -> str: ...
# 要約結果のパース
def _parse_summary(raw: str) -> DigestResult: ...
# Block Kit 組み立て
def _build_category_blocks(cat: DigestCategory) -> list[dict]: ...
# Slack投稿(httpx → slack_sdk.WebClient に変更)
def _post_to_slack(
client: WebClient,
channel: str,
thread_ts: str,
blocks: list[dict],
text: str,
) -> None: ...
fetch (既存)
→ fetched.json(中間データ)
→ summarize.py(Claude CLI)
→ NewsSummaryResult(構造化dict)
→ notify.py
→ 1. thread_ts.json 確認/作成
→ 2. 親メッセージ投稿(or 既存スレッド取得)
→ 3. 銘柄ごとにスレッド返信(Block Kit)
→ 4. 総合コメントをスレッド返信
→ 5. 通知済み記録
Graph API(未読メール取得)
→ _build_mail_text()
→ Claude CLI(要約)
→ DigestResult(構造化dict)
→ 1. digest_thread_ts.json 確認/作成
→ 2. 親メッセージ投稿(or 既存スレッド取得)
→ 3. カテゴリごとにスレッド返信(Block Kit)
→ 4. 処理済み記録
| エラーケース | 対処 |
|---|---|
| Claude CLI の出力が JSON パース不可 | 1回リトライ(プロンプトに「JSONのみ」を強調)。2回目も失敗 → 従来のプレーンテキスト投稿にフォールバック |
| Slack API エラー(親メッセージ投稿失敗) | ログ出力して終了。スレッド返信は行わない |
| Slack API エラー(スレッド返信失敗) | ログ出力して次の返信に続行(1件の失敗で全体を止めない) |
| thread_ts.json 破損 | 空の状態として扱う(新規スレッド作成) |
| 同日のスレッドtsが無効(チャンネル削除等) | Slack APIが thread_not_found を返す → tsを削除して新規作成 |
# run() ループ内の news_scan 条件を変更
if (
now.hour == self.scan_hour
and self._last_runs.get("news_scan") != today
and _is_business_day(today) # ← 追加
):
...
# 定数追加
MAIL_DIGEST_HOUR = 8
# run() ループ内に追加
if (
now.hour == MAIL_DIGEST_HOUR
and self._last_runs.get("mail_digest") != today
):
self._last_runs["mail_digest"] = today
self._save_last_runs()
await self._run_mail_digest()
_run_mail_digest() は _run_mail_filing() と同じパターンで python -m mail_filing.pipeline digest を subprocess 実行する。
# 起動ログにmail_digestも追加
log.info(
"DailyScheduler 起動: news_scan=%02d:00(平日), mail_filing=%02d:00, mail_digest=%02d:00",
self.scan_hour, MAIL_FILING_HOUR, MAIL_DIGEST_HOUR,
)
scheduler.py の _post_summary() と _ask_if_to_evaluate() は AI News スキャン結果(#ai-ops チャンネル)用であり、今回の金融ニュース(#news-reminder)とは別系統。変更不要。
#news-reminder: 金融ニュース(News Reminder パイプライン)← 今回のリニューアル対象#ai-ops: AIニュース(scheduler.py の _run_daily)← 変更なし
jpholiday パッケージが利用可能であること(既存依存に含まれる)__main__.py 等)で summary の型変更に追従が必要| 案 | 採用 | 理由 |
|---|---|---|
| JSON出力 + Pythonで Block Kit 組み立て | ✅ 採用 | Block Kit のブロック構造を正確に制御可能。レイアウト変更がPythonコード側で完結 |
| Claude CLI に直接 Block Kit JSON を生成させる | ❌ | Block Kit の仕様は複雑で、LLM が正しい JSON を出力する保証がない。デバッグも困難 |
| 従来のプレーンテキスト維持 + Block Kit ラッパー | ❌ | プレーンテキストからの構造化パースは不安定。最初から JSON で出力させる方が確実 |
| 案 | 採用 | 理由 |
|---|---|---|
| JSONファイル(data/thread_ts.json) | ✅ 採用 | 既存の data/ パターンと一貫。シンプルで冪等 |
| Slack API で当日の投稿を検索 | ❌ | API呼び出し増加。conversations.history の権限追加が必要になる可能性 |
| 案 | 採用 | 理由 |
|---|---|---|
| digest.py の httpx → slack_sdk に変更のみ | ✅ 採用 | 最小変更。notify.py は既に slack_sdk 使用中 |
| 共通 Slack ヘルパーモジュールを作成 | ❌ | 2箇所の使用では過剰な共通化。パイプラインが独立している設計を壊す |