AI Tech Daily Agent — 完全なアーキテクチャ徹底解説&ワークフロー分析

Dev.to / 2026/4/16

💬 オピニオンDeveloper Stack & InfrastructureIdeas & Deep AnalysisTools & Practical Usage

要点

  • この記事では、「AI Tech Daily Agent」のアーキテクチャを深く掘り下げます。これは、Fetch.ai の uAgents フレームワークに基づく、日々の技術ジャーナリズムのための自律システムです。
  • ウェブ検索、ウェブスクレイピング、GitHub API のトラッキング、画像検索、そして LLM による分析/生成を組み合わせた、多サービスのパイプラインが説明されます。これにより、裏付けのある調査記事(deep-dive articles)を作成します。
  • ワークフローには、エージェントのライフサイクルやセッション/会話管理に加え、オーケストレーションおよびエンドツーエンドの信頼できる実行のためのサービス/プロトコル層が含まれます。
  • デプロイとインフラ面での考慮事項に触れ、技術的課題(例:複数ステップにまたがる協調とデータフローの信頼性)や、計画されている今後の改善点を扱います。
  • 最終的に、Dev.to への自動公開が行われます。あらかじめ定義されたフォーマット要件に従い、監視・制御のためのインタラクティブなチャットインターフェースも用意されています。

AI Tech Daily Agent — 検索とデータから、深掘り記事の公開まで

日次のテクノロジー・ジャーナリズムのための自律AIエージェントを構築するための包括的な探究

目次

  1. はじめに
  2. プロジェクト概要
  3. コア・アーキテクチャ
  4. コンポーネント分析
  5. ワークフロー・パイプライン
  6. サービス層の詳細
  7. プロトコル実装
  8. エージェントのライフサイクル
  9. データフロー&オーケストレーション
  10. コード分析:重要なパターン
  11. デプロイ&インフラ
  12. 技術的課題と解決策
  13. 今後の拡張
  14. 結論

はじめに

AI Tech Daily Agent は、自律型AIエージェントを高度に実装して、技術ジャーナリズムを自動化するためのシステムです。Fetch.ai の uAgents フレームワークに基づいており、このシステムは複数のサービスをオーケストレーションし、AI およびテクノロジー企業に関する包括的な深掘り記事を毎日調査・分析・生成します。

このプロジェクトは、エージェントベースのシステムが、人手を大きく要する複雑で多段階のワークフローを自動化する力を持つことを示しています。Web 検索、コンテンツのスクレイピング、GitHub API の統合、大規模言語モデル(LLM)、画像検索をひとつのまとまったパイプラインに統合することで、エージェントは、人の介入を最小限に抑えつつ、高品質で調査に裏打ちされた記事を生成します。

主な機能:

  • トピックのカバー範囲に基づく企業の自動選定
  • 複数ソースからのリアルタイムなニュース集約
  • オープンソースプロジェクトのための GitHub リポジトリ追跡
  • 深いコンテンツ分析のための Web スクレイピング
  • 特定のフォーマット要件を満たす LLM による記事生成
  • 自動公開のための Dev.to プラットフォーム統合
  • 対話的な制御とモニタリングのためのチャットインターフェース
  • セッション管理と会話の取り扱い

プロジェクト概要

目的&ミッション

AI Tech Daily Agent は、特定の課題を解決するために存在します。それは、急速に進化する AI およびテクノロジー企業に関する日次の深掘りテクニカルコンテンツを作るのに必要な高い労力です。従来のテクニカル・ジャーナリズムでは、記者は次のような作業を行う必要があります:

  1. 複数のニュースソースを監視する
  2. GitHub リポジトリを追跡する
  3. 企業の発表を分析する
  4. 技術的な詳細を理解する
  5. 包括的な記事を書く
  6. さまざまなプラットフォーム向けにフォーマットする
  7. 記事を公開し、配信する

このエージェントは、パイプライン全体を自動化し、通常は数時間かかるであろう人手の作業を、2〜3 分の自動処理に短縮します。

テクノロジースタック

このプロジェクトでは、モダンな Python ベースのテクノロジースタックを活用しています:

コア・フレームワーク:

  • uAgents プロトコル(Fetch.ai):分散型のエージェント通信プロトコル
  • Python 3.11+:async/await をサポートする最新の Python
  • uv:高速な Python パッケージマネージャ

Web&データ:

  • Requests:API 連携のための HTTP クライアント
  • GitHub REST API:リポジトリとリリースの追跡
  • Dev.to API:コンテンツ公開プラットフォーム
  • Bing/Web Search API:ニュースおよび Web 検索機能

AI&NLP:

  • OpenAI/LLM API:コンテンツ生成と分析
  • LangChain 風のプロンプト:構造化されたプロンプトエンジニアリング

インフラ:

  • Agentverse:エージェントのホスティングとディスカバリー(発見)
  • Almanac Contracts:分散型のサービス登録
  • 環境設定(Environment Configuration):柔軟なデプロイ設定

プロジェクト構造

ai-tech-daily-agent/
├── agent.py                    # メインエージェントのエントリポイント
├── config/
│   ├── __init__.py
│   └── sources.py             # 追跡対象のリポジトリ&企業
├── protocols/
│   ├── __init__.py
│   └── chat_proto.py          # チャットプロトコルの実装
├── services/
│   ├── __init__.py
│   ├── article_service.py     # 記事生成ロジック
│   ├── company_picker.py      # 企業選定アルゴリズム
│   ├── devto_service.py       # Dev.to API 統合
│   ├── github_service.py      # GitHub API 統合
│   ├── image_search_service.py # 画像検索ロジック
│   ├── llm_service.py         # LLM 抽象化レイヤー
│   ├── publish_service.py     # 公開オーケストレーション
│   ├── web_scraper_service.py # コンテンツスクレイピング
│   └── web_search_service.py  # 検索 API のラッパー
├── tests/
│   ├── __init__.py
│   └── test_filter.py         # ユニットテスト
├── pyproject.toml             # プロジェクトの依存関係
├── uv.lock                    # 固定された依存関係のバージョン
├── .gitignore
├── README.md
├── PROJECT_DEEP_DIVE.md       # このドキュメント
└── docs/
    └── deep-dive/             # 生成されたダイアグラム画像(PNG)
        ├── architecture.png
        ├── pipeline.png
        └── data-flow.png

この構造は、関心の分離(separation of concerns)の明確な区分を備えたクリーンアーキテクチャの原則に従っています:

  • 設定は config/
  • プロトコル定義は protocols/
  • 業務ロジックは services/
  • エントリポイントはルートに配置

コア・アーキテクチャ

システム・アーキテクチャ図

AI Tech Daily Agent は、モジュール性、スケーラビリティ、保守性のために設計された、多層構造のアーキテクチャに従っています。

例示的なアーキテクチャGitHub で表示):

システムアーキテクチャ — Agentverse/uAgents から agent.py、services、外部 API までのレイヤー

アーキテクチャの原則

このアーキテクチャには、堅牢で保守しやすくするための重要な原則がいくつか組み込まれています:

1. 関心の分離(Separation of Concerns)
各サービスには、単一で明確に定義された責務があります:

  • company_picker.py - 企業の選定ロジックだけを担当
  • github_service.py - GitHub API のやり取りだけを担当
  • article_service.py - 記事生成だけを担当
  • publish_service.py - 公開(パブリッシュ)ロジックだけを担当

2. 依存性の注入(Dependency Injection)
サービスは依存関係をパラメータとして受け取るため、テストや柔軟性が高まります:

def generate_article(
    company: dict,
    search_data: dict,
    scraped_content: str,
    github_repos: list[dict],
    images: dict[str, str],
) -> tuple[str, str]:

3. Async/Await パターン
ネットワーク操作は、ブロックしないように async を使用します:

async def _run_pipeline(ctx: Context) -> str:
    result = await asyncio.to_thread(run_pipeline, dry_run)
    return result

4. エラーハンドリング & フォールバック
サービスが失敗した場合の、円滑な縮退動作:

if result:
    # LLM 生成コンテンツを使用
else:
    result = _fallback_article(...)

5. 設定の外部化
追跡対象の企業とリポジトリはすべて config/sources.py にあり、ハードコードしていません:

TRACKED_COMPANIES = [...]
TRACKED_FRAMEWORK_REPOS = [...]

通信モデル

エージェントは、エージェント間の通信に uAgents プロトコルを使用します:

チャットプロトコル:

  • 標準の uAgents チャットプロトコル仕様を実装
  • StartSessionContentEndSessionContent によるセッション管理をサポート
  • 確実な配信のためのメッセージ承認(acknowledgment)
  • ユーザーとの対話のためのテキストベースのコマンド

主要なプロトコル機能:

# セッション開始
StartSessionContent  Welcome message

# ユーザーコマンド
TextContent("generate")  Start pipeline
TextContent("status")  Show history
TextContent("help")  Show commands

# 承認
ChatAcknowledgement  Confirmation of receipt

コンポーネント分析

1. メインエージェント(agent.py)

agent.py ファイルは、システム全体のエントリポイントおよびオーケストレーターとして機能します。

主要な責務:

  1. エージェント登録:Almanac コントラクトを使用して Agentverse に登録
  2. プロトコル設定:ユーザーとのやり取りのためにチャットプロトコルを取り付け
  3. パイプラインオーケストレーション:すべてのサービスの実行を調整
  4. 環境設定:ドライランモードと API キーを扱う
  5. ログ出力:パイプライン全体にわたる包括的なログを提供

重要なコードフロー:

# エージェント登録
Agent(
    name="ai-tech-daily-agent",
    port=8000,
    seed=AGENT_SEED,
    endpoint=["http://localhost:8000/submit"],
)

# メインパイプライン
def run_pipeline(dry_run: bool = False) -> str:
    1. 履歴を確認し 企業を選択
    2. Web/検索 クエリを実行
    3. GitHub リポジトリデータを取得
    4. スクレイプ して コンテンツを読み取る
    5. LLM を使用して 記事を生成
    6. 適切な 画像を見つける
    7. 必要に応じて Dev.to公開する
    8. 履歴を更新

デザインパターン:パイプライン/責任の連鎖

run_pipeline 関数は、各ステップが前のステップの上に構築されるパイプラインパターンを実装しています:

def run_pipeline(dry_runbool = False) -> str:
    # ステップ1:会社の選択
    history = get_history()
    company = select_company(history, TRACKED_COMPANIES)

    # ステップ2:データ収集
    search_data = {
        "news": search_news(...),
        "web": search_web(...),
        "github": search_github(...),
    }

    # ステップ3:コンテンツの収集
    github_repos = get_all_repos()
    scraped_content = scrape_and_read(...)

    # ステップ4:記事の生成
    articlefilename = generate_article(...)

    # ステップ5:公開
    if not dry_run:
        devto_id = publish_to_devto(...)

    return result

各ステップはその出力を次のステップへ渡し、データ変換のパイプラインを作成します。

2. 会社ピッカーサービス(company_picker.py)

会社ピッカーは、毎日どの会社を取り上げるかを決めるための中核となる判断ロジックを実装しています。

アルゴリズム:

  1. 履歴の読み込みhistory.json を読み込み、過去の掲載状況を確認する
  2. 候補の絞り込み:直近14日間に掲載された会社を除外する
  3. ランダム選択:残った候補から選ぶ
  4. 履歴の更新:選択結果を記録する

主要コード:

def select_company(historylist[dict], companies: list[dict]) -> dict:
    cutoff = (datetime.now() - timedelta(days=14)).isoformat()
    recent_slugs = {h["slug"] for h in history if h["date"] >= cutoff}

    candidates = [c for c in companies if c["slug"] not in recent_slugs]

    if not candidates:
        log.warning("No candidates available after 14-day filter")
        return companies[0]

    return random.choice(candidates)

設計上の考慮事項:

  • 14日間のクールダウン期間:繰り返しの掲載を防止する
  • ランダム選択:掲載の多様性を確保する
  • フォールバック機構:すべての会社が直近の場合は最初の会社を選ぶ
  • スラッグの一致:比較のために単純な文字列照合を用いる

データ構造:

返却形式: {"translated": "翻訳されたHTML"}
COMPANY_TRACKING = [
    {
        "name": "OpenAI",
        "slug": "openai",
        "topics": ["llm", "generative-ai", "gpt"],
    },
    {
        "name": "Anthropic",
        "slug": "anthropic",
        "topics": ["llm", "claude", "safety"],
    },
    # ... 他の企業
]

3. Web Search Service(web_search_service.py)

このサービスは、ニュースおよび一般的なWeb検索のためのWeb検索操作を抽象化します。

API統合:

このサービスは検索API(おそらくBingまたは同様のもの)と統合し、以下を取得します:

  • ニュース記事:タイトル、URL、本文、日付
  • Web検索結果:タイトルと説明

主要な機能:

def search_news(company: str, topics: list[str]) -> list[dict]:
    """
    企業に関する最近のニュースを検索します。
    タイトル、URL、本文、日付を含むニュース項目のリストを返します。
    """
    queries = [company] + topics
    all_news = []

    for query in queries:
        results = _call_search_api(query="news:" + query)
        all_news.extend(results)

    return _deduplicate(all_news)

def search_web(company: str) -> list[dict]:
    """
    企業情報のための一般的なWeb検索を行います。
    """
    return _call_search_api(query=company)

データ変換:

生の検索結果は、標準化された形式に変換されます:

# 生のAPIレスポンス
{
    "title": "...",
    "url": "...",
    "snippet": "...",
    "date": "...",
}

# 内部形式に変換
{
    "title": "...",
    "url": "...",
    "body": "...",
    "date": "...",
}

エラーハンドリング:

このサービスには、以下に対する堅牢なエラーハンドリングが含まれています:

  • API失敗(空のリストを返す)
  • レート制限(リトライ付き)
  • ネットワークタイムアウト
  • 不正なレスポンス

ワークフローパイプライン

完全なパイプラインの概要

AI Tech Daily Agentは、シンプルなコマンドを公開された記事へと変換する包括的なパイプラインを実行します。以下に完全なワークフローを示します。

図解のパイプラインGitHubで表示):

generateコマンドから公開と履歴までのエンドツーエンドのパイプライン

パイプライン実行の詳細

フェーズ1:会社の選択(5秒)

# 履歴ファイルを読み込む
if os.path.exists(HISTORY_FILE):
    history = json.loads(Path(HISTORY_FILE).read_text())
else:
    history = []

# 時間的フィルタを適用する
cutoff = (datetime.now() - timedelta(days=14)).isoformat()
recent_slugs = {h["slug"] for h in history if h["date"] >= cutoff}

# 会社を選択する
candidates = [c for c in TRACKED_COMPANIES if c["slug"] not in recent_slugs]
company = random.choice(candidates)

フェーズ2:データ収集(30-45秒)

効率化のための同時API呼び出し:

# 異なるクエリのバリエーションで並列検索
news_queries = [
    company["name"],
    company["name"] + " news",
    company["name"] + " announcement",
    *company["topics"]
]

all_news = []
for query in news_queries:
    news = search_news(query)
    all_news.extend(news)

# 結果の重複を除去する
seen_urls = set()
unique_news = [n for n in all_news if n["url"] not in seen_urls]

フェーズ3:GitHubデータ(20-30秒)

GitHubデータ収集の2種類:

# 1. 追跡対象のフレームワーク(既知のリポジトリ)
frameworks = []
for repo in TRACKED_FRAMEWORK_REPOS:
    data = fetch_github_repo(repo["owner"], repo["repo"])
    release = get_latest_release(repo["owner"], repo["repo"])
    frameworks.append({...})

返却形式: {"translated": "翻訳されたHTML"}# 2. トレンドの新規リポジトリ(ディスカバリー)
trending = []
for query in SEARCH_QUERIES:
    repos = github_search_repository(query, 
                                     sort="stars",
                                     created=">7 days ago")
    trending.extend(repos)

フェーズ4:コンテンツのスクレイピング(30〜60秒)

# 検索結果から上位URLを取得
top_urls = [item["url"] for item in search_results[:10]]

# スクレイピングして内容を読み取る
scraped_text = " "
for url in top_urls:
    try:
        html = requests.get(url, timeout=15).text
        text = extract_text_from_html(html)
        scraped_text += text
        if len(scraped_text) > 10000:  # コンテンツの上限を設ける
            break
    except Exception as e:
        log.warning(f"{url} のスクレイピングに失敗しました: {e}")

フェーズ5:記事の生成(30〜45秒)

# 包括的なプロンプトを構築
system_prompt = f"""
あなたはシニアのテック・ジャーナリストです...
TODAY'S FOCUS: {company_name}
ルール:
- 記事は必ず300行以上であること
- stars、funding、users といった具体的な数値を含めること
- 2〜3個のコードスニペットを含めること
- 出典へのリンクを含めること
"""

user_prompt = f"""
会社のトピック: {topics}

=== リアルタイムニュース ===
{formatted_news}

=== Web検索結果 ===
{formatted_web}

=== GITHUB 検索 ===
{formatted_github}

=== トラッキングしているリポジトリ ===
{formatted_repos}

=== スクレイピングしたコンテンツ ===
{scraped_content[:8000]}
"""

# 記事を生成
article = call_llm(system_prompt, user_prompt, 
                   temperature=0.7, 
                   max_tokens=8000)

フェーズ6:画像の強化(15〜20秒)

images = {}

# ロゴを検索
logo_url = search_images(f"{company} logo official website")
if logo_url:
    images["logo"] = logo_url

# ヒーロー画像を検索
hero_url = search_images(f"{company} technology platform")
if hero_url:
    images["hero"] = hero_url
# テック画像を検索する
banner_url = search_images(f"{company} アーキテクチャ テクノロジー")
if banner_url:
    images["banner"] = banner_url

フェーズ7:公開(10〜15秒)

# ローカルに保存
filename = f"{slug}-{date}.md"
article_path = Path("articles") / filename
article_path.write_text(article)

# Dev.toに公開
if not dry_run and devto_api_key:
    devto_id = create_devto_article(
        title=f"{company} — 詳細調査",
        body_markdown=article,
        tags=company["topics"] + ["ai", "technology"],
        published=True
    )
    url = f"https://dev.to/{devto_username}/{slug}"
else:
    url = f"ローカル: {article_path}"

フェーズ8:履歴の更新(2秒)

history.append({
    "name": company["name"],
    "slug": company["slug"],
    "date": datetime.now().isoformat(),
    "article_url": url,
    "devto_id": devto_id
})

# ファイルに永続化
Path(HISTORY_FILE).write_text(json.dumps(history, indent=2))

トータルのパイプライン時間:約2〜3分

サービス層の詳細調査

GitHubサービス(github_service.py)

GitHubサービスは、既知のリポジトリの追跡と、新しいトレンドプロジェクトの発見の両方を提供する重要なコンポーネントです。

認証:

def _headers() -> dict:
    h = {
        "Accept": "application/vnd.github+json",
        "User-Agent": "AI-Tech-Daily-Agent/1.0"
    }
    token = os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN")
    if token:
        h["Authorization"] = f"token {token.strip()}"
    return h

主な機能:

  1. Framework Tracking: 既知のAIエージェント・フレームワークを監視
  2. Trending Discovery: 過去7日間に作成された新しいリポジトリを見つける
  3. Release Tracking: バージョン情報のために最新リリースを追跡
  4. Metadata Collection: スター数、言語、説明、活動状況を抽出

Framework Tracking ロジック:

def get_framework_updates() -> list[dict]:
    results = []

    for repo_info in TRACKED_FRAMEWORK_REPOS:
        # リポジトリのメタデータを取得
        resp = requests.get(
            f"https://api.github.com/repos/{owner}/{repo}",
            headers=headers,
            timeout=10
        )
        data = resp.json()

        # 最新リリースを取得
        release_info = _get_latest_release(owner, repo, headers)

        # 詳細なレコードを構築
        results.append({
            "name": f"{owner}/{repo}",
            "label": repo_info["label"],
            "url": data["html_url"],
            "description": data["description"],
            "stars": data["stargazers_count"],
            "language": data.get("language"),
            "updated_at": data.get("pushed_at"),
            "latest_release": release_info,
            "type": "tracked"
        })

    # 最近の活動でソート
    results.sort(key=lambda x: x.get("updated_at", ""), reverse=True)
    return results

Trending Search ロジック:

def search_trending_repos() -> list[dict]:
    one_week_ago = (datetime.utcnow() - timedelta(days=7)).strftime("%Y-%m-%d")

    queries = [
        "ai agent",
        "llm agent framework",
        "mcp server",
        "agentic ai",
        "autonomous agent",
        # ... さらにクエリ
    ]

    all_repos = []

返却形式: {"translated": "翻訳されたHTML"}for query in queries:
        resp = requests.get(
            "https://api.github.com/search/repositories",
            params={
                "q": f"{query} created:>{one_week_ago}",
                "sort": "stars",
                "order": "desc",
                "per_page": 5
            },
            headers=headers
        )

        for repo in resp.json().get("items", []):
            all_repos.append({
                "name": repo["full_name"],
                "url": repo["html_url"],
                "description": repo["description"],
                "stars": repo["stargazers_count"],
                "language": repo["language"],
                "type": "trending"
            })

    # 重複を除去し、starsでソートする
    unique = list({r["name"]: r for r in all_repos}.values())
    unique.sort(key=lambda x: x["stars"], reverse=True)
    return unique[:10]

レート制限に関する考慮:

  • レート制限があるGitHub REST APIを使用します
  • タイムアウト処理を実装しています(リクエストあたり10〜15秒)
  • クラッシュせずに失敗を捕捉してログに記録します
  • 明示的なレート制限のコードはなく、GitHubのデフォルト上限に依存します

記事サービス(article_service.py)

記事サービスは、LLMベースの記事作成を統括する中核となるコンテンツ生成コンポーネントです。

主な生成関数:

def generate_article(
    company: dict,
    search_data: dict,
    scraped_content: str,
    github_repos: list[dict],
    images: dict[str, str],
) -> tuple[str, str]:

プロンプトエンジニアリング戦略:

このサービスは、高品質な出力を保証するために高度なプロンプトエンジニアリングを使用します:

1. システムプロンプト - ペルソナとルールを設定:

system = f"""あなたはシニアのテックジャーナリストであり、デベロッパーアドボケイトとして "AI & Tech Daily" のための、深掘りした毎日の記事を書いてください。

TODAY'S FOCUS: {name}

Write a COMPREHENSIVE deep-dive about {name} — 今この瞬間に起きていることをすべて網羅しながら。

返却形式: {"translated": "翻訳されたHTML"}RULES:
- 記事は300行以上のマークダウンであること
- すべての内容は、提供されたリアルタイム検索データに基づくこと — 架空の事実を作らないこと
- 具体的な数値を入れること: 星の数、調達額、ユーザー数、バージョン番号
- コードスニペットを2〜3個含めること: それらのツール/プロダクトの使い方
- 出典へのリンクを含めること: [text](url)
- 指示がある場合は画像を入れること(ロゴ、ヒーロー、テック画像)
- 論拠のある意見(オピニオン)を入れること — それが開発者にとってどういう意味かを述べる
- 各セクションには、実際の十分に具体的な内容(実質的な中身)を入れること

必須セクション(## 見出し、すべて必須):

# {name} — ディープダイブ | {human_date}

## 会社概要
## 最新ニュース & お知らせ
## プロダクト & テクノロジーディープダイブ
## GitHub & オープンソース
## はじめに — コード例
## 市場でのポジション & 競合
## 開発者への影響
## その' 次
## 要点
## リソース & リンク
"""

2. ユーザープロンプト — すべての文脈を提供:

user = f"""{nameについてのディープダイブ記事を{human_date}のために書いてください。

会社のトピック: {topics}
{image_instructions}

=== リアルタイムニュース(今日検索) ===
{news_text}

=== WEB検索結果 ===
{web_text}

=== GITHUB検索 ===
{github_text}

=== 追跡レポジトリデータ ===
{repo_text}

=== スクレイピング済み記事内容(上位ソースから) ===
{scraped_content[:8000]}

重要: 記事を全文で書いてください。最低300行以上。指示された通りに画像を含め、コードスニペットを含めてください。上記のデータのみを使ってください。"""

データフォーマット関数:

def _format_news(news: list[dict]) -> str:
    """プロンプト用にニュース検索結果を整形します。"""
    lines = []
    for n in news[:15]:  # 上位15件に制限
        lines.append(f"- [{n['title']}]({n['url']})")
        if n.get("body"):
            lines.append(f"  {n['body'][:300]}")
        if n.get("date"):
            lines.append(f"  日付: {n['date']}")
        lines.append("")
    return "
".join(lines)

def _format_github(github: list[dict]) -> str:
    """プロンプト用にGitHub検索結果を整形します。"""
    lines = []
    for g in github[:8]:  # 上位8件に制限
        lines.append(f"- [{g['title']}]({g['url']})")
        lines.append(f"  {g.get('body', '')[:200]}")
    return "
".join(lines)

def _format_tracked_repos(repos: list[dict]) -> str:
    """リリース情報付きで追跡中のリポジトリを整形します。"""
    lines = []
    for r in repos:
        release = r.get("latest_release")
        rel = f" — 最新: {release['tag']}" if release else ""
        lines.append(
            f"- {r['label']} (⭐{r['stars']}:,}){rel}"
            f"{r['description'][:150]} [{r['url']}]"
        )
    return "
".join(lines)

フォールバック・メカニズム:

LLMの生成が失敗するか、空のコンテンツが返された場合、サービスはテンプレート化された記事にフォールバックします:

def _fallback_article(company, search_data, repos, images, 
                      human_date, date_str):
    """LLMが失敗した場合の基本テンプレート記事を生成します。"""

    name = company["name"]
    topics = ", ".join(company["topics"])

    # 利用可能なデータを整形する
    news_bullets = "
".join(
        f"- **{n['title']}** — {n.get('body', '')[:200]} [source]({n['url']})"
        for n in search_data.get("news", [])[:10]
    )

返却形式: {"translated": "翻訳されたHTML"}web_bullets = "
".join(
        f"- [{w['title']}]({w['url']})"
        for w in search_data.get("web", [])[:8]
    )

    repo_bullets = "
".join(
        f"- **[{r['label']}]({r['url']})** ⭐ {r['stars']}:,}"
        for r in repos[:10]
    )

    # テンプレートを構築する
    return f"""# {name} — ディープダイブ | {human_date}
{logo_img}
> {name} への毎日のディープダイブ — {topics} をカバーします。

---

{hero_img}

## 最新ニュース & お知らせ

{news_bullets}

---

## Web リソース

{web_bullets}

---

## GitHub & オープンソース

{repo_bullets}

---

## 重要なポイント

1. {name} は AI/テクノロジーの領域で進化を続けています
2. 進捗の更新のために、オープンソースのプロジェクトを監視してください
3. 最新のお知らせは公式チャネルで確認してください

---

* {date_str} に生成:[AI Tech Daily Agent](https://github.com/gautammanak1/ai-tech-daily-agent)*
"""

これにより、LLM サービスが利用できない場合や失敗した場合でも、システムが常に出力を生成することが保証されます。

LLM サービス(llm_service.py)

LLM サービスは、LLM API の上にクリーンな抽象化レイヤーを提供します。

インターフェース:

def call_llm(
    system: str,
    user: str,
    temperature: float = 0.7,
    max_tokens: int = 4000,
) -> str | None:
    """
    システムメッセージとユーザーメッセージを使用して LLM API を呼び出します。
    生成されたテキスト、または失敗時は None を返します。
    """

実装:

def call_llm(
    system: str,
    user: str,
    temperature: float = 0.7,
    max_tokens: int = 4000,
) -> str | None:
    try:
        # 環境変数から API キーを取得する
        api_key = os.getenv("OPENAI_API_KEY") or os.getenv("LLM_API_KEY")

        if not api_key:
            log.warning("LLM API キーが見つかりません
")
            return None

返却形式: {"translated": "翻訳されたHTML"}# API 呼び出しを行う
        response = requests.post(
            "https://api.openai.com/v1/chat/completions",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json"
            },
            json={
                "model": "gpt-4-turbo-preview",
                "messages": [
                    {"role": "system", "content": system},
                    {"role": "user", "content": user}
                ],
                "temperature": temperature,
                "max_tokens": max_tokens
            },
            timeout=60
        )

        response.raise_for_status()
        data = response.json()

        # 生成されたコンテンツを抽出する
        return data["choices"][0]["message"]["content"]

    except requests.RequestException as e:
        log.error(f"LLM API リクエストの失敗: {e}")
        return None
    except (KeyError, IndexError) as e:
        log.error(f"LLM API レスポンスの解析に失敗: {e}")
        return None

設定:

設定用の環境変数:

  • OPENAI_API_KEY または LLM_API_KEY: LLM サービスの API キー
  • LLM_MODEL: モデル名(デフォルト: gpt-4-turbo-preview)
  • LLM_TIMEOUT: リクエストのタイムアウト(秒)(デフォルト: 60)

エラーハンドリング:

サービスはさまざまなエラーシナリオを処理します:

  • API キーがない場合: None を返す
  • ネットワークエラー: ログを出力して None を返す
  • タイムアウトエラー: ログを出力して None を返す
  • 不正なレスポンス: ログを出力して None を返す
  • レート制限: リトライのロジックとともに追加する必要がある

Dev.to サービス(devto_service.py)

このサービスは、Dev.to プラットフォームへの記事の公開を処理します。

記事の作成:

def create_devto_article(
    title: str,
    body_markdown: str,
    tags: list[str],
    published: bool = True,
) -> str | None:
    """
    Dev.to 上に記事を作成します。
    成功時は dev.to の記事 ID、失敗時は None を返します。
    """

    api_key = os.getenv("DEVTO_API_KEY")
    if not api_key:
        log.warning("Dev.to の API キーがありません")
        return Nonetry:
        response = requests.post(
            "https://dev.to/api/articles",
            headers={
                "api-key": api_key,
                "Content-Type": "application/json"
            },
            json={
                "article": {
                    "title": title,<