news_reminderには「AIニュース」と「金融ニュース」の2つの異なる目的のパイプラインが1つのパッケージに同居している。機能追加や修正のたびに、関係ない方に影響が出ないか気を遣う状態になっている。
AIニュース(scanner.py 974行)と金融ニュース(pipeline/steps/ 4ファイル)が同じパッケージにある。
config.py)が2つあり、どちらがどの系統用か分かりにくいscanned.json(AIニュースの処理済み記録)に上限がない。現時点で109件・83KBだが、1日最大60件のペースで年間15〜17MBに成長する見込み。
notified.json(金融ニュース)は1,000件で上限管理されている → 同じ仕組みをAI側にも必要| 場所 | モデル | 方式 | タイムアウト |
|---|---|---|---|
| scanner.py(記事評価) | haiku | 同期subprocess | 120秒 |
| summarize.py(金融要約) | 指定なし(デフォルト) | 同期subprocess | 300秒 |
| dashboard.py(地政学解釈) | sonnet | 同期subprocess | 120秒 |
呼び出し方法(引数の組み立て・環境変数のフィルタリング・JSON抽出)がファイルごとに微妙に違う。バグ修正が全箇所に行き渡らないリスクがある。
Claude CLIの出力にJSON以外の文字(Richのエスケープシーケンスなど)が混ざるため、raw.find('{') で手動抽出している。壊れやすく、ネストしたJSONで誤動作する可能性がある。
relevantと判定された記事を1件ずつ順番にClaude CLI(IF層)で処理している。記事が多い日は完了まで長時間かかる。
全体で約2,000行のコードにテストが1件もない。外部API(RSS/HN/arXiv/Slack/GitHub)への依存が多く、変更時の動作確認が手動のみ。
arXivはリトライ3回あるが、RSSフィードは429/503に未対応。Google News RSSも同様。障害時の挙動がソースによって異なる。
AIニュースと金融ニュースをどう分けるかについて、3つの案を検討した。
ops/news_reminder/ ├── shared/ ← 共通基盤(新設) │ ├── claude.py ← Claude CLI呼び出し統一 │ ├── slack.py ← Slack通知共通 │ ├── retry.py ← リトライ・レート制限 │ └── data.py ← データライフサイクル管理 ├── ai_news/ ← AIニュース(scanner.pyを分割) │ ├── fetcher.py │ ├── evaluator.py │ └── config.py ├── finance/ ← 金融ニュース(pipeline/を移動) │ ├── fetcher.py │ ├── summarizer.py │ ├── notifier.py │ └── config.py ├── dashboard/ ← 地政学ダッシュボード │ └── interpreter.py └── data/ ← データファイル(共用)
ops/ai_news/ ← AIニュース専用パッケージ(新設) ├── app/ ├── data/ └── pipeline/ ops/finance_news/ ← 金融ニュース専用パッケージ(新設) ├── app/ ├── data/ └── pipeline/ ops/shared/ ← 共通基盤(新設、ops全体で使う)
ops/news_reminder/ ├── shared/ ← 共通基盤のみ切り出し ├── app/ ← 既存構造を維持(scanner.pyのリファクタのみ) ├── pipeline/ ← 既存構造を維持 └── data/
| 観点 | 案A(サブパッケージ分離) | 案B(完全分離) | 案C(共通基盤のみ) |
|---|---|---|---|
| 変更の独立性 | 高い。AI側の変更が金融側に影響しない | 最も高い。パッケージ単位で独立 | 低い。既存の混在が残る |
| 共通コードの共有 | 同一パッケージ内で自然に共有 | 別パッケージ間のimportが必要(ops/shared/) | 同一パッケージ内で共有 |
| 移行コスト | 中。ディレクトリ再配置 + importパス変更 | 高。pyproject.toml変更、スケジューラ変更、importパス全面変更 | 低。shared/ 追加とscanner.py分割のみ |
| スケジューラへの影響 | 小。コマンド名変更のみ | 大。2つのパッケージを別々に呼び出す設定が必要 | なし |
| 将来の拡張性 | 新しいニュースソース種別をサブパッケージとして追加しやすい | パッケージごとに独立進化できる | 種類が増えるとまた混在する |
理由:
ops/news_reminder/
├── shared/ ← 共通基盤
│ ├── __init__.py
│ ├── claude_caller.py ← Claude CLI統一呼び出し
│ ├── slack_poster.py ← Slack投稿ヘルパー
│ ├── retry.py ← HTTP リトライ(exponential backoff)
│ ├── json_parser.py ← LLM出力からのJSON安全抽出
│ └── data_store.py ← データファイルのライフサイクル管理
│
├── ai_news/ ← AIニュース系統
│ ├── __init__.py
│ ├── config.py ← AIニュース用設定
│ ├── fetcher.py ← RSS / HN / arXiv 取得(ソースタイプごとに関数分割)
│ ├── evaluator.py ← Claude Haiku による記事評価
│ ├── reporter.py ← Slack投稿 + バッチサマリー生成
│ └── prompts/ ← プロンプトテンプレート(.txt)
│ ├── relevance.txt
│ ├── geopolitical.txt
│ └── batch_summary.txt
│
├── finance/ ← 金融ニュース系統
│ ├── __init__.py
│ ├── config.py ← 金融ニュース用設定(保有銘柄管理含む)
│ ├── fetcher.py ← Google News RSS取得
│ ├── summarizer.py ← Claude による要約
│ ├── notifier.py ← Slack投稿
│ └── sync.py ← OneDrive Excel同期
│
├── dashboard/ ← 地政学ダッシュボード
│ ├── __init__.py
│ └── interpreter.py ← Playwright取得 + Claude解釈
│
├── thread.py ← スレッド管理(既存を維持)
│
├── pipeline/ ← CLIエントリポイント
│ └── cli.py ← コマンド振り分けのみ
│
├── data/ ← データファイル
│ ├── sources.json
│ ├── scanned.json
│ ├── holdings.json
│ ├── fetched.json
│ └── notified.json
│
└── tests/ ← テスト(新設)
├── conftest.py
├── test_fetcher_ai.py
├── test_evaluator.py
├── test_fetcher_finance.py
├── test_summarizer.py
├── test_claude_caller.py
└── test_data_store.py
現在3箇所でバラバラに実装されているClaude CLI呼び出しを1つに統一する。
| 機能 | 現状 | 再設計後 |
|---|---|---|
| 呼び出し方法 | 3ファイルに各自のsubprocess.run | call_claude(prompt, model, timeout, output_format) 関数1つ |
| モデル指定 | haiku / sonnet / 未指定がバラバラ | 引数で統一指定。デフォルトはhaiku |
| JSON抽出 | 各ファイルで独自実装 | json_parser.py に集約 |
| 環境変数フィルタ | summarize.pyだけ実施 | 全呼び出しで統一フィルタ |
| エラーハンドリング | RuntimeError / ログのみ / 未処理が混在 | 統一例外 ClaudeCallError(リトライ可否フラグ付き) |
現在arXivだけリトライ3回、他は未対応。全HTTPフェッチに共通のリトライ方針を適用する。
| 項目 | 方針 |
|---|---|
| リトライ対象 | HTTP 429(レート制限)、503(一時障害)、接続タイムアウト |
| リトライ回数 | 最大3回 |
| 待機時間 | exponential backoff: 3秒 → 6秒 → 12秒 |
| リトライ除外 | 400, 401, 403, 404(クライアント側の問題。リトライしても無駄) |
| 実装方式 | httpxクライアントのラッパー関数 or デコレータ |
| 項目 | 値 | 理由 |
|---|---|---|
| 保持件数上限 | 1,000件 | notified.jsonと統一。重複チェックに必要な直近分のみ保持 |
| 保持期間 | 90日 | 同じ記事が90日後に再出現する可能性は極めて低い |
| 削除方式 | 件数上限到達時 or 90日超過で古い順に削除 | 2重ガード |
| 削除タイミング | スキャン完了後の保存時 | 読み込み時にファイルを書き換えない(冪等性確保) |
推定: 1日60件 × 90日 = 5,400件だが、1,000件上限で先に削られる。ファイルサイズは最大700KB程度に収まる。
| データファイル | 現状 | 再設計後 |
|---|---|---|
scanned.json |
上限なし(無限成長) | 1,000件 or 90日で自動ローテーション |
notified.json |
1,000件上限(実装済み) | 変更なし(既に適切) |
fetched.json |
毎回上書き | 変更なし(中間ファイルのため問題なし) |
sources.json |
手動編集 | 変更なし |
holdings.json |
sync.pyで上書き | 変更なし |
現在974行あるscanner.pyを3つの責務に分割する。
| 新ファイル | 責務 | 推定行数 | 元の関数 |
|---|---|---|---|
ai_news/fetcher.py |
RSSフィード・HN API・arXiv APIからの記事取得 | ~300行 | _fetch_rss(), _fetch_hn_api(), _fetch_arxiv_api(), フィード巡回ループ |
ai_news/evaluator.py |
Claude Haikuによる記事の関連性評価 | ~200行 | _call_claude(), _extract_json(), _evaluate_article() |
ai_news/reporter.py |
Slack通知ブロック組み立て + バッチサマリー生成 | ~250行 | build_summary_blocks(), _generate_batch_summary(), Slack投稿ロジック |
現在scanner.pyに埋め込まれている長いプロンプト文字列(RELEVANCE_PROMPT等)をprompts/ディレクトリに.txtファイルとして分離する。
現在の問題: relevant記事が5件あれば、IF層のClaude CLIを5回連続で呼ぶ。1回120秒かかると最大10分。
複数記事を1回のClaude CLI呼び出しでまとめて評価する。
| 項目 | 現状 | 改善後 |
|---|---|---|
| 呼び出し回数 | 記事数 × 1回 | 1回(最大20件をまとめて) |
| 所要時間(5件の場合) | 最大600秒(120秒 × 5回) | 最大120秒(1回) |
| 入力形式 | 1記事のJSON | 記事リストのJSON配列 |
| 出力形式 | 1件の判断結果 | 判断結果の配列 |
注意点: 1回のプロンプトが長くなるが、Haikuのコンテキスト長(200K)に対して20記事程度は余裕がある。
| レイヤー | エラー種別 | 対応方針 |
|---|---|---|
| HTTP取得 (RSS/HN/arXiv/Google News) |
429 レート制限 | retry.py でexponential backoff(3回まで)。超過時はそのソースをスキップ |
| 503 一時障害 | 同上 | |
| 接続タイムアウト | 同上 | |
| Claude CLI | タイムアウト(120秒超過) | ログ出力 + その記事をスキップ。リトライしない(レート制限悪化を防ぐ) |
| JSON解析失敗 | ログ出力 + その記事をスキップ。raw出力をログに残す | |
| Slack投稿 | API失敗 | ログ出力。記事はscanned.jsonに保存(次回再通知しないため) |
| GitHub Issue | API失敗 | ログ出力 + 次の記事に進む。Issue作成は「あると便利」なので全体を止めない |
| 全体 | 想定外の例外 | スケジューラが捕捉し、Slackにエラー通知。次回スケジュールは正常実行 |
外部依存が多いため、モック戦略が重要。以下の方針で進める。
| テスト対象 | 種別 | モック対象 | テスト内容 |
|---|---|---|---|
json_parser.py |
ユニット | なし(純粋関数) | 正常JSON、コードブロック付き、壊れたJSON、空文字列 |
data_store.py |
ユニット | ファイルI/O(tmp_path) | 上限ローテーション、90日超過削除、空ファイル |
claude_caller.py |
ユニット | subprocess.run |
正常応答、タイムアウト、異常終了コード、環境変数フィルタ |
ai_news/fetcher.py |
ユニット | httpx(respxライブラリ) |
RSS解析、HNフィルタ、arXivリトライ、重複除外 |
ai_news/evaluator.py |
ユニット | claude_caller |
relevance判定ロジック、スコアリング、バッチ処理 |
finance/fetcher.py |
ユニット | httpx |
Google News RSS解析、重複フィルタ、件数制限 |
finance/summarizer.py |
ユニット | claude_caller |
プロンプト組み立て、JSON出力解析 |
| パイプライン全体 | 結合 | 外部API全て | fetch→evaluate→report の一連の流れ |
--dry-runオプションで行う| ステップ | スケジューラ (bot/scheduler.py) |
取得 (ai_news/fetcher.py) |
評価 (ai_news/evaluator.py) |
報告 (ai_news/reporter.py) |
IF層 (bot/scheduler.py) |
|---|---|---|---|---|---|
| 1 | 毎朝 7:00 起動 | ||||
| 2 |
RSS / HN / arXiv 記事取得 shared/retry.py でリトライ |
||||
| 3 |
scanned.json 重複チェック shared/data_store.py |
||||
| 4 |
Claude Haiku 記事評価 shared/claude_caller.py |
||||
| 5 |
scanned.json 結果保存 1,000件上限ローテーション |
||||
| 6 |
バッチサマリー 生成 Claude Haiku |
||||
| 7 |
Slack投稿 (#news-reminder) shared/slack_poster.py |
||||
| 8 |
relevant記事を バッチ評価 skip / issue / consult |
||||
| 9 | 完了 |
| ステップ | スケジューラ (bot/scheduler.py) |
取得 (finance/fetcher.py) |
要約 (finance/summarizer.py) |
通知 (finance/notifier.py) |
|---|---|---|---|---|
| 1 | 8:00/12:00/18:00 起動 |
|||
| 2 |
Google News RSS 保有銘柄ニュース取得 shared/retry.py でリトライ |
|||
| 3 |
notified.json 重複フィルタ 1,000件上限(既存) |
|||
| 4 |
Claude ニュース要約 shared/claude_caller.py |
|||
| 5 |
Slack投稿 (#news-reminder) 影響度バッジ付き |
|||
| 6 |
notified.json 更新 |
|||
| 7 | 完了 |
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ ai_news/ │ │ finance/ │ │ dashboard/ │
│ fetcher │ │ fetcher │ │ interpreter │
│ evaluator │ │ summarizer │ │ │
│ reporter │ │ notifier │ │ │
└──────┬──────┘ └──────┬───────┘ └────────┬────────┘
│ │ │
└────────────┬────┴─────────────────────┘
│
┌─────────▼─────────┐
│ shared/ │
│ claude_caller │ ← Claude CLI統一呼び出し
│ slack_poster │ ← Slack投稿共通化
│ retry │ ← HTTPリトライ共通化
│ json_parser │ ← JSON抽出共通化
│ data_store │ ← データローテーション
└───────────────────┘
一度に全部やると壊れたときの切り分けが難しい。3フェーズに分けて段階的に移行する。
shared/ ディレクトリ作成claude_caller.py — 3箇所のClaude CLI呼び出しを統一json_parser.py — JSON抽出ロジック集約retry.py — HTTPリトライ共通化data_store.py — scanned.json上限管理追加ai_news/ — scanner.pyを3ファイルに分割finance/ — pipeline/steps/を移動dashboard/ — dashboard.pyを移動pipeline/cli.py のimportパス更新bot/scheduler.py のimportパス更新| フェーズ | 変更範囲 | リスク |
|---|---|---|
| Phase 1(共通基盤) | 新規4ファイル + 既存3ファイル修正 | 低。既存の動作を変えずに共通化するだけ |
| Phase 2(再配置) | ファイル移動 + importパス変更 | 中。importパスの漏れがあるとスケジューラが動かなくなる |
| Phase 3(IF層 + テスト) | 新規機能 + テスト追加 | 低。既存の動作には影響しない追加作業 |
data/ ディレクトリの位置は変えない(スケジューラやスクリプトからの参照パスを壊さないため)thread.py は全系統が使うので、パッケージルート直下に残すscanned.jsonの肥大化問題は解消される