From fc3b2220891861b6c2b05c4f9f63d32042f811a8 Mon Sep 17 00:00:00 2001 From: Atsushi Morimoto <74th.tech@gmail.com> Date: Wed, 8 Apr 2026 20:55:52 +0900 Subject: [PATCH 1/2] feat: update documentation and improve example applications for Claude Agent SDK --- README.md | 6 +- docs/create_your_claude_agent_sdk_apps.md | 80 +++++++++++++++++++ docs/run_sample_app_ja.md | 6 +- docs/server_ja.md | 4 +- docs/setup_ja.md | 20 ++--- .../{claude_agent_sdk.py => app.py} | 67 +++------------- example_apps/echo.py | 6 -- example_apps/echo_with_move.py | 40 +++++----- example_apps/gemini.py | 46 ++++++++--- 9 files changed, 157 insertions(+), 118 deletions(-) create mode 100644 docs/create_your_claude_agent_sdk_apps.md rename example_apps/claude_agent_sdk/{claude_agent_sdk.py => app.py} (54%) diff --git a/README.md b/README.md index bc1a743..56b996a 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,6 @@ async def setup(proxy: WsProxy): @app.talk_session async def talk_session(proxy: WsProxy): - # 聞くポーズ - await proxy.move_servo([(ServoMoveType.MOVE_Y, 80, 100)]) - chat = client.chats.create( model="gemini-3-flash-preview", config=types.GenerateContentConfig( @@ -55,6 +52,9 @@ async def talk_session(proxy: WsProxy): ) while True: + # 聞くポーズ + await proxy.move_servo([(ServoMoveType.MOVE_Y, 80, 100)]) + try: # 音声認識 text = await proxy.listen() diff --git a/docs/create_your_claude_agent_sdk_apps.md b/docs/create_your_claude_agent_sdk_apps.md new file mode 100644 index 0000000..f41f9d5 --- /dev/null +++ b/docs/create_your_claude_agent_sdk_apps.md @@ -0,0 +1,80 @@ +# Claude Agent SDKによるAIエージェントアプリケーション開発 + +[example_apps/claude_agent_sdk/app.py](../example_apps/claude_agent_sdk/app.py) をベースに改変を行い、Claude Agent SDKを利用したエージェントを作る手順を解説します。 + +> [!CAUTION] +> Claude Agent SDKでは、エージェントがファイルシステムの変更権限を持ちます。 +> エージェントに与える指示によっては、意図しないファイルの編集や削除などが行われる可能性があります。 +> エージェントに与える指示には十分注意したり、コンテナ環境に置くなどしてください。 + +## ディレクトリを決める + +まず、コードのディレクトリと、ワークスペースとなるディレクトリを決めてください。 + +コードのディレクトリは、エージェントのプログラムを置くディレクトリです。 +ワークスペースとは、Claude Agent SDKの作業ディレクトリです。 +ワークスペースにある .claude/ の配下に、スキルやMCPの設定などを置きます。 +ワークスペースは、Claude Codeで言うところの、起動ディレクトリに相当します。 + +## プログラムを作る + +コードのディレクトリにて、uvでプロジェクトを初期化します。 + +``` +uv init +``` + +本リポジトリをライブラリとして追加します。 + +``` +uv add https://github.com/74th/websocket-control-stackchan.git +``` + +Claude Agent SDKのライブラリも追加します。 + +``` +uv add claude-agent-sdk +``` + +[example_apps/claude_agent_sdk/app.py](../example_apps/claude_agent_sdk/app.py) を、コードのディレクトリ内にコピーします。 + +app.py を書き換えていきます。 + +WORKSPACE_DIR という変数の定義があるため、、ワークスペースのディレクトリを設定するように書き換えます。 + +```py +WORKSPACE_DIR = "/path/to/your_workspace" +``` + +## サーバの設定の.envファイルの準備 + +[./server_ja.md](./server_ja.md) で作成した .env ファイルを、コードのディレクトリにコピーしてください。 + +## サーバを起動する + +app.py と言うファイル名で作成した場合、以下のコマンドでサーバを起動します。 +ポート番号等は適宜変更してください。 + +``` +uv run uvicorn app:app.fastapi --host 0.0.0.0 --port 8000 +``` + +> [!INFORMATION] +> uvicorn.run()の第一引数は、`{Pythonモジュール名}:{モジュール内のFastAPIアプリケーションの変数名}` という形式になっています。 +> +> app.py というファイルを作った場合、Pythonモジュール名も app になります。 +> claude_agent_sdk など、既存モジュールと名前が被らないようにしてください。 +> +> app.py 内で、app = StackChanApp() というコードがあるため、モジュール内のFastAPIアプリケーションの変数名は app になります。 +> app.fastapi がFastAPIのアプリケーションオブジェクトになります。 +> +> そのため、uvicorn.run()の第一引数は、`{ファイル名(拡張子なし)}:app.fastapi` という形式になります。 + +スタックチャンを起動して、「Disconnected」から「Idle」のステータス表示になれば接続されています。 +「ハイ!スタックチャン!」と話しかけて、聞くポーズになることを確認して、話しかけてみてください。 + +## さらに作り込む + +以下のClaude Agent SDKのドキュメントを参照して、スキルやMCPの追加など、さらにエージェントを作り込んでみてください。 + +> https://platform.claude.com/docs/ja/agent-sdk/python diff --git a/docs/run_sample_app_ja.md b/docs/run_sample_app_ja.md index 0f6c20c..3e638ed 100644 --- a/docs/run_sample_app_ja.md +++ b/docs/run_sample_app_ja.md @@ -17,7 +17,7 @@ uv sync その後、以下のコマンドでPythonサーバを起動します。 ``` -uv run uvicorn app.echo_with_move:app.fastapi --host 0.0.0.0 --port 8000 +uv run uvicorn example_apps.echo_with_move:app.fastapi --host 0.0.0.0 --port 8000 ``` スタックチャンを起動して、「Disconnected」から「Idle」のステータス表示になれば接続されています。 @@ -36,7 +36,7 @@ uv sync --group example-gemini その後、以下のコマンドでPythonサーバを起動します。 ``` -uv run uvicorn app.gemini:app.fastapi --host 0.0.0.0 --port 8000 +uv run uvicorn example_apps.gemini:app.fastapi --host 0.0.0.0 --port 8000 ``` スタックチャンを起動して、「Disconnected」から「Idle」のステータス表示になれば接続されています。 @@ -86,7 +86,7 @@ Claude Agent SDKを利用するには、VertexAIを利用する場合、以下 その後、以下のコマンドでPythonサーバを起動します。 ``` -uv run uvicorn app.claude_agent_sdk:app.fastapi --host 0.0.0.0 --port 8000 +uv run uvicorn example_apps.claude_agent_sdk.app:app.fastapi --host 0.0.0.0 --port 8000 ``` スタックチャンを起動して、「Disconnected」から「Idle」のステータス表示になれば接続されています。 diff --git a/docs/server_ja.md b/docs/server_ja.md index fdd5b58..55bf1b2 100644 --- a/docs/server_ja.md +++ b/docs/server_ja.md @@ -49,7 +49,7 @@ STACKCHAN_USE_GOOGLE_CLOUD_STT=1 STACKCHAN_GOOGLE_CLOUD_STT_LANGUAGE_CODE="ja-JP" ``` -### Whisper.cppのwhisper-cliの設定 +### (オプション)Whisper.cppのwhisper-cliの設定 (WIP) @@ -59,7 +59,7 @@ STACKCHAN_WHISPER_CLI_MODEL_PATH="/path/to/whisper.cpp/ggml-small.bin" STACKCHAN_WHISPER_CLI_VAD_MODEL_PATH="/path/to/whisper.cpp/ggml-silero-v5.1.2.bin" ``` -### Whisper.cppのwhisper-serverの設定 +### (オプション)Whisper.cppのwhisper-serverの設定 (WIP) diff --git a/docs/setup_ja.md b/docs/setup_ja.md index 9b81c60..fd9a76a 100644 --- a/docs/setup_ja.md +++ b/docs/setup_ja.md @@ -115,14 +115,14 @@ docker compose run --rm --service-ports voicevox 標準ではGoogle Cloud Speech-to-Textを利用して音声認識を行います。 無料で利用できるWhisper.cppのwhisper-cliも利用できます。 -TODO +(TODO) ## (オプション)Whisper.cppのwhisper-serverのインストールと実行 標準ではGoogle Cloud Speech-to-Textを利用して音声認識を行います。 無料で利用できるWhisper.cppのwhisper-serverも利用できます。 -TODO +(TODO) ## Python開発環境の構築 @@ -137,7 +137,7 @@ Pythonの環境構築の方法は、パッケージマネージャuvのページ 以下のページを参照して、サーバの設定を行ってください。 -[./server_ja.md](./server_ja.md) +[server_ja.md](./server_ja.md) ## サンプルアプリケーションの実行 @@ -145,19 +145,13 @@ Pythonの環境構築の方法は、パッケージマネージャuvのページ 以下のページを参照して、サンプルアプリケーションの実行方法を確認してください。 -[./run_sample_app_ja.md](./run_sample_app_ja.md) - -## アプリケーションを作る - -(WIP) - -[example_apps/gemini.py](../example_apps/gemini.py) をベースに改変を行い、アプリケーションを作ってみましょう。 +[run_sample_app_ja.md](./run_sample_app_ja.md) -## Claude Agent SDKによるエージェントの構築と実行 +## Claude Agent SDKによるAIエージェントアプリケーションの開発 -(WIP) +以下のページを参照して、ユーザ独自のコードで、Claude Agent SDKを利用したAIエージェントアプリケーションを起動する方法を確認してください。 -[example_apps/claude_agent_sdk/claude_agent_sdk.py](../example_apps/claude_agent_sdk/claude_agent_sdk.py) をベースに改変を行い、Claude Agent SDKを利用したエージェントを作ってみましょう。 +[docs/create_your_claude_agent_sdk_apps.md](../docs/create_your_claude_agent_sdk_apps.md) ## Claude Agent SDKをDocker環境で実行する diff --git a/example_apps/claude_agent_sdk/claude_agent_sdk.py b/example_apps/claude_agent_sdk/app.py similarity index 54% rename from example_apps/claude_agent_sdk/claude_agent_sdk.py rename to example_apps/claude_agent_sdk/app.py index f04e0d6..db802f9 100644 --- a/example_apps/claude_agent_sdk/claude_agent_sdk.py +++ b/example_apps/claude_agent_sdk/app.py @@ -3,17 +3,13 @@ import os import pathlib from logging import StreamHandler, getLogger -from typing import Any, Literal from claude_agent_sdk import ( ClaudeAgentOptions, ClaudeSDKClient, ResultMessage, - create_sdk_mcp_server, - tool, ) from dotenv import load_dotenv -from pydantic import BaseModel from stackchan_server.app import StackChanApp from stackchan_server.ws_proxy import ( @@ -39,51 +35,17 @@ model = "claude-haiku-4-5@20251001" -# ツールの作成 -class AirConRemoteInput(BaseModel): - room: Literal["寝室", "リビング"] - state: Literal["オフ", "暖房オン", "冷房オン"] - - -@tool( - "aircon-control", - "自宅のエアコンを操作する。寝室かリビングかを指定する。", - AirConRemoteInput.model_json_schema(), -) -async def aircon_remote(dict_args: dict[str, Any]): - args = AirConRemoteInput.model_validate(dict_args) - # 実際に実装が必要 - print(f"🌳エアコンを操作します {args}") - return {"state": "success"} - - -# MCPサーバ化 -home_remote_mcp = create_sdk_mcp_server( - name="home-remote", - version="1.0.0", - tools=[aircon_remote], +option = ClaudeAgentOptions( + model=model, + system_prompt="あなたは音声AIアシスタントのスタックチャンです。ユーザの質問に対して、3文程度の言葉で答えてください。音声案内であるため、マークダウンや絵文字等は用いずに、文字列だけで回答してください", + cwd=str(WORKSPACE_DIR), + setting_sources=["project"], + permission_mode="bypassPermissions", ) - -def setup_claude_agent_sdk() -> ClaudeSDKClient: - option = ClaudeAgentOptions( - model=model, - system_prompt="あなたは音声AIアシスタントのスタックチャンです。ユーザの質問に対して、3文程度の言葉で答えてください。音声案内であるため、マークダウンや絵文字等は用いずに、文字列だけで回答してください", - cwd=str(WORKSPACE_DIR), - setting_sources=["project"], - # MCPサーバを登録 - mcp_servers={"home-remote": home_remote_mcp}, - # tools=["mcp__home-remote__aircon-control"], - # 全て許可 - permission_mode="bypassPermissions", - ) - - return ClaudeSDKClient( - options=option, - ) - - -client = setup_claude_agent_sdk() +client = ClaudeSDKClient( + options=option, +) @app.setup @@ -128,14 +90,3 @@ async def talk_session(proxy: WsProxy): logger.info("AI: %s", message.result) if message.result: await proxy.speak(message.result) - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run( - "example_apps.claude_agent_sdk.claude_agent_sdk:app.fastapi", - host="0.0.0.0", - port=8000, - reload=True, - ) diff --git a/example_apps/echo.py b/example_apps/echo.py index 84726bc..f69bff6 100644 --- a/example_apps/echo.py +++ b/example_apps/echo.py @@ -38,9 +38,3 @@ async def talk_session(proxy: WsProxy): return logger.info("Heard: %s", text) await proxy.speak(text) - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run("example_apps.echo:app.fastapi", host="0.0.0.0", port=8000, reload=True) diff --git a/example_apps/echo_with_move.py b/example_apps/echo_with_move.py index 905a6ce..fbc2b4f 100644 --- a/example_apps/echo_with_move.py +++ b/example_apps/echo_with_move.py @@ -35,31 +35,31 @@ async def setup(proxy: WsProxy): @app.talk_session async def talk_session(proxy: WsProxy): while True: - try: - await proxy.move_servo([(ServoMoveType.MOVE_Y, 80, 100)]) + # listening pose + await proxy.move_servo([(ServoMoveType.MOVE_Y, 80, 100)]) + try: + # voice recognition text = await proxy.listen() - - await proxy.move_servo( - [ - (ServoMoveType.MOVE_Y, 100, 100), - (ServoWaitType.SLEEP, 200), - (ServoMoveType.MOVE_Y, 90, 100), - (ServoWaitType.SLEEP, 200), - (ServoMoveType.MOVE_Y, 100, 100), - (ServoWaitType.SLEEP, 200), - (ServoMoveType.MOVE_Y, 90, 100), - ] - ) - except EmptyTranscriptError: + # off pose await proxy.move_servo([(ServoMoveType.MOVE_Y, 90, 100)]) return - logger.info("Heard: %s", text) - await proxy.speak(text) + # nod pose + await proxy.move_servo( + [ + (ServoMoveType.MOVE_Y, 100, 100), + (ServoWaitType.SLEEP, 200), + (ServoMoveType.MOVE_Y, 90, 100), + (ServoWaitType.SLEEP, 200), + (ServoMoveType.MOVE_Y, 100, 100), + (ServoWaitType.SLEEP, 200), + (ServoMoveType.MOVE_Y, 90, 100), + ] + ) -if __name__ == "__main__": - import uvicorn + logger.info("Heard: %s", text) - uvicorn.run("example_apps.echo:app.fastapi", host="0.0.0.0", port=8000, reload=True) + # speaking + await proxy.speak(text) diff --git a/example_apps/gemini.py b/example_apps/gemini.py index f541130..a37b870 100644 --- a/example_apps/gemini.py +++ b/example_apps/gemini.py @@ -7,7 +7,12 @@ from google.genai import types from stackchan_server.app import StackChanApp -from stackchan_server.ws_proxy import WsProxy +from stackchan_server.ws_proxy import ( + EmptyTranscriptError, + ServoMoveType, + ServoWaitType, + WsProxy, +) logger = getLogger(__name__) logger.addHandler(StreamHandler()) @@ -35,23 +40,38 @@ async def talk_session(proxy: WsProxy): ) while True: - text = await proxy.listen() - if not text: + # listening pose + await proxy.move_servo([(ServoMoveType.MOVE_Y, 80, 100)]) + + try: + # voice recognition + text = await proxy.listen() + + except EmptyTranscriptError: + # off pose + await proxy.move_servo([(ServoMoveType.MOVE_Y, 90, 100)]) return + + + # nod pose + await proxy.move_servo( + [ + (ServoMoveType.MOVE_Y, 100, 100), + (ServoWaitType.SLEEP, 200), + (ServoMoveType.MOVE_Y, 90, 100), + (ServoWaitType.SLEEP, 200), + (ServoMoveType.MOVE_Y, 100, 100), + (ServoWaitType.SLEEP, 200), + (ServoMoveType.MOVE_Y, 90, 100), + ] + ) + logger.info("Human: %s", text) - # AI応答の取得 + # generate response resp = await chat.send_message(text) - # 発話 + # speaking logger.info("AI: %s", resp.text) if resp.text: await proxy.speak(resp.text) - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run( - "example_apps.gemini:app.fastapi", host="0.0.0.0", port=8000, reload=True - ) From e424622095c3753aec28a1fba6557742fb319218 Mon Sep 17 00:00:00 2001 From: Atsushi Morimoto <74th.tech@gmail.com> Date: Wed, 8 Apr 2026 21:00:20 +0900 Subject: [PATCH 2/2] feat: update Docker command in setup documentation and add docker-compose.yml for VOICEVOX service --- docs/setup_ja.md | 2 +- docker-compose.yml => misc/voicevox/docker-compose.yml | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docker-compose.yml => misc/voicevox/docker-compose.yml (100%) diff --git a/docs/setup_ja.md b/docs/setup_ja.md index fd9a76a..b38c96e 100644 --- a/docs/setup_ja.md +++ b/docs/setup_ja.md @@ -103,7 +103,7 @@ Dockerがインストールされていない場合は以下のページを参 Dockerがインストールできたら、リポジトリのディレクトリで以下のコマンドを実行してください。 ``` -docker compose run --rm --service-ports voicevox +docker compose -f ./misc/voicevox/docker-compose.yml up -d ``` 以下のサイトにアクセスし、「VOICEVOX Engine」と表示されていれば成功です。 diff --git a/docker-compose.yml b/misc/voicevox/docker-compose.yml similarity index 100% rename from docker-compose.yml rename to misc/voicevox/docker-compose.yml