Claude Agent SDK

Claude Agent SDKを使って、ユーザの回答に応じて、質問を分岐させるデモをやってみました。

Claude Code CLIがインストールされていれば、APIキーなどの設定なしに動かすことができます。(loginができていればOK)

"""対話型デシジョンツリーのデ

ClaudeSDKClient の複数ターン会話を使い
ユーザーの回答に応じて質問を分岐させ最適な結論へ導く例

テーマ: プロジェクトに最適な技術スタックの推薦
"""

import asyncio
import functools
from typing import Any

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    ClaudeSDKClient,
    ResultMessage,
    TextBlock,
)

SYSTEM_PROMPT = """\
あなたは技術スタック選定のエキスパートです
ユーザーの回答に基づき以下のデシジョンツリーに沿って質問を進め
最適な技術スタックを推薦してください

## ルール
- 1回のメッセージにつき必ず1つだけ質問してください
- 各質問には番号付きの選択肢(2〜4を提示してください
- ユーザーが番号または選択肢名で回答したら次の分岐に進んでください
- 最終ノードに到達したら推薦する技術スタックとその理由を詳しく説明してください
- 回答が選択肢にない場合はもう一度同じ質問を丁寧に繰り返してください

## デシジョンツリー

Q1: プロジェクトの種類は
├─ 1. WebアプリQ2へ
├─ 2. モバイルアプリQ5へ
├─ 3. データ分析/MLQ7へ
└─ 4. CLI/自動化ツールQ9へ

Q2: チームの規模は?(Web
├─ 1. 個人少人数(1-3) → Q3へ
└─ 2. 大規模チーム(4人以上) → Q4へ

Q3: 重視するポイントは?(Web/少人数
├─ 1. 開発スピード → 【結論ANext.js + Supabase
├─ 2. パフォーマンス → 【結論BSvelteKit + SQLite (Turso)
└─ 3. フルスタック一体型 → 【結論CRails / Laravel

Q4: フロントエンドの複雑さは?(Web/大規模
├─ 1. シンプル管理画面等) → 【結論DReact + Express + PostgreSQL
├─ 2. リッチUISaaS等) → 【結論ENext.js + tRPC + Prisma + PostgreSQL
└─ 3. リアルタイム重視 → 【結論FReact + Elixir/Phoenix + PostgreSQL

Q5: 対象プラットフォームは?(モバイル
├─ 1. iOS のみ → 【結論GSwift + SwiftUI
├─ 2. Android のみ → 【結論HKotlin + Jetpack Compose
└─ 3. 両方Q6へ

Q6: ネイティブ性能は必要?(クロスプラットフォーム
├─ 1. 高性能が必要 → 【結論IReact Native / Flutter
└─ 2. Webベースで十分 → 【結論JCapacitor + Next.js (PWA)

Q7: 主な用途は?(データ/ML
├─ 1. データ分析可視化 → 【結論KPython + Pandas + Streamlit
├─ 2. 機械学習モデル開発Q8へ
└─ 3. データパイプライン → 【結論LPython + dbt + Airflow

Q8: モデルの規模は?(ML
├─ 1. 中規模 → 【結論MPython + scikit-learn + MLflow
└─ 2. 大規模 / ディープラーニング → 【結論NPython + PyTorch + Weights & Biases

Q9: 言語の好みは?(CLI/自動化
├─ 1. Python → 【結論OPython + Typer + Rich
├─ 2. 高速さ重視 → 【結論PRust + clap
└─ 3. 汎用性重視 → 【結論QGo + Cobra

## 結論テンプレート
最終ノードに到達したら以下の形式で推薦してください:

---
🎯 推薦スタック: [スタック名]

選定理由:
- [理由1]
- [理由2]
- [理由3]

始め方:
1. [最初のステップ]
2. [次のステップ]
3. [その次のステップ]
---

それではQ1 から始めてください
"""


async def read_input(prompt: str) -> str:
    """イベントループをブロックせずにユーザー入力を読み取る"""
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(
        None, functools.partial(input, prompt),
    )


async def main() -> None:
    options = ClaudeAgentOptions(
        system_prompt=SYSTEM_PROMPT,
        max_turns=1,
    )

    print("=== Claude Agent SDK: デシジョンツリー デモ ===\n")
    print("質問に番号で回答すると、次の質問へ分岐します。")
    print("最終的にプロジェクトに最適な技術スタックを推薦します。")
    print("'exit' で終了します。\n")

    async with ClaudeSDKClient(options=options) as client:
        # 最初の質問を Claude に出させる
        await client.query("始めてください。")

        async for message in client.receive_response():
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(f"\n{block.text}")

        # 対話ループ
        while True:
            user_input = await read_input("\n回答> ")
            user_input = user_input.strip()

            if not user_input:
                continue
            if user_input.lower() == "exit":
                print("終了します。")
                break

            await client.query(user_input)

            is_final = False
            async for message in client.receive_response():
                if isinstance(message, AssistantMessage):
                    for block in message.content:
                        if isinstance(block, TextBlock):
                            print(f"\n{block.text}")
                            if "推薦スタック" in block.text:
                                is_final = True

            if is_final:
                print("\n--- 推薦完了 ---")
                another = await read_input("\nもう一度選定しますか? [y/n] > ")
                if another.strip().lower() == "y":
                    await client.query("最初からやり直してください。Q1 から始めてください。")
                    async for message in client.receive_response():
                        if isinstance(message, AssistantMessage):
                            for block in message.content:
                                if isinstance(block, TextBlock):
                                    print(f"\n{block.text}")
                else:
                    print("終了します。")
                    break


if __name__ == "__main__":
    asyncio.run(main())

制御をシステムプロンプトに頼ってしまう点が、プログラマ的に抵抗があるのですが、これが今のスタイルなのでしょう。Webアプリ版もつくりましたが、Claudeのインスタンスをつくる機能がこのSDKにはあるようです。