追記:末尾に少しブラッシュアップしたStreamlitアプリのコードを置いてます
Serenaってのが流行ってるみたいです。
StreamliとStrands Agentsを組み合わせて、遊んでみました
コーディングエージェントを作成する
ディレクトリーを作成します。
mkdir serena-app
cd serena-app
uvでプロジェクトを初期化します。
uv init --name serena-app --python 3.12
ライブラリーを追加します。
uv add strands-agents streamlit
main.py
を実装してきます。
まずは枠を作ります。
import asyncio
import streamlit as st
from mcp import StdioServerParameters, stdio_client
from strands.agent import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
async def main():
st.title("Strands Agents with Serena")
asyncio.run(main=main())
main()関数の中を埋めていきます。
サイドバーに設定を用意します。
async def main():
st.title("Strands Agents with Serena")
with st.sidebar:
st.subheader("設定")
work_dir = st.text_input("作業ディレクトリ", value="./workspace")
model_id = st.text_input("モデルID", value="apac.amazon.nova-pro-v1:0")
region_name = st.text_input("リージョン", value="ap-northeast-1")
if not work_dir:
return
メッセージを表示するところを追加します。
Tool Useがあったりするので、text
があるものだけを出力するように工夫してます。
if "messages" not in st.session_state:
st.session_state["messages"] = []
for message in st.session_state["messages"]:
role = message["role"]
text_content = [
content["text"] for content in message["content"] if "text" in content
]
if len(text_content) > 0:
with st.chat_message(role):
for text in text_content:
st.write(text)
いよいよここから、プロンプト入力後の処理です。
まずはプロンプトをそのまま出力します。
if prompt := st.chat_input():
with st.chat_message("user"):
st.write(prompt)
ここでSerenaの登場です。SerenaはMCPサーバーとして呼び出せますのでMCPクライアントを作成します。
パラメーターが色々ありますが、公式ドキュメントを参照ください🙇
mcp_client = MCPClient(
lambda: stdio_client(
StdioServerParameters(
command="uvx",
args=[
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server",
"--project",
work_dir,
"--context",
"ide-assistant",
],
)
)
)
このMCPサーバーのツールをセットしたAgentを生成します。
messages=st.session_state["messages"]とすると、エージェント呼び出し時のユーザーメッセージから最後のアシスタントメッセージまでが、st.session_state["messages"]にいい感じにセットされることを発見しました!
with mcp_client:
tools = mcp_client.list_tools_sync()
model = BedrockModel(
model_id=model_id,
region_name=region_name,
)
agent = Agent(
model=model,
system_prompt="あなたは優秀なエージェントです。ユーザーとの会話は日本語で行います。",
messages=st.session_state[
"messages"
], # agent呼び出し後、自動でユーザーとアシスタントのメッセージがappendされる
tools=tools,
callback_handler=None,
)
ストリームで呼び出して、レスポンスからツール呼び出しとアシスタントメッセージを出力します。
agent_stream = agent.stream_async(prompt)
async for event in agent_stream:
match event:
case {"current_tool_use": current_tool_use}:
with st.expander("ツール呼び出し", expanded=False):
st.write(current_tool_use)
case {"message": message}:
if "role" in message and message["role"] == "assistant":
with st.chat_message("assistant"):
for content in message["content"]:
if "text" in content:
st.write(content["text"])
Claude Sonnet 4を使った場合に、current_tool_useブロックがストリームで返ってくるので見た目は変な感じになります。。
できました!
main.py全文(折りたたまれてます)
import asyncio
import streamlit as st
from mcp import StdioServerParameters, stdio_client
from strands.agent import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
async def main():
st.title("Strands Agents with Serena")
with st.sidebar:
st.subheader("設定")
work_dir = st.text_input("作業ディレクトリ")
model_id = st.text_input("モデルID", value="apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
region_name = st.text_input("リージョン", value="ap-northeast-1")
if not work_dir:
return
if "messages" not in st.session_state:
st.session_state["messages"] = []
for message in st.session_state["messages"]:
role = message["role"]
text_content = [
content["text"] for content in message["content"] if "text" in content
]
if len(text_content) > 0:
with st.chat_message(role):
for text in text_content:
st.write(text)
if prompt := st.chat_input():
with st.chat_message("user"):
st.write(prompt)
mcp_client = MCPClient(
lambda: stdio_client(
StdioServerParameters(
command="uvx",
args=[
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server",
"--project",
work_dir,
"--context",
"ide-assistant",
],
)
)
)
with mcp_client:
tools = mcp_client.list_tools_sync()
model = BedrockModel(
model_id=model_id,
region_name=region_name,
)
agent = Agent(
model=model,
system_prompt="あなたは優秀なエージェントです。ユーザーとの会話は日本語で行います。",
messages=st.session_state[
"messages"
], # agent呼び出し後、自動でユーザーとアシスタントのメッセージがappendされる
tools=tools,
callback_handler=None,
)
agent_stream = agent.stream_async(prompt)
async for event in agent_stream:
match event:
case {"current_tool_use": current_tool_use}:
with st.expander("ツール呼び出し", expanded=False):
st.write(current_tool_use)
case {"message": message}:
if "role" in message and message["role"] == "assistant":
with st.chat_message("assistant"):
for content in message["content"]:
if "text" in content:
st.write(content["text"])
asyncio.run(main=main())
Serenaで扱うプロジェクトのセットアップ
自分なりの解釈でやってみたのですが、合ってるかわかりません。。(なんか間違ってる気がする。。)
ディレクトリーを作成します。先ほどのserena-app
のサブディレクトリとして作成します。
mkdir workspace
cd workspace
対象のソースとして、「Generative AI Use Cases (GenU)」のソースを取得します。
git clone https://github.com/aws-samples/generative-ai-use-cases.git
Serenaのプロジェクトとして初期化します。
uvx --from git+https://github.com/oraios/serena serena project generate-yml
実行すると、.serena/project.yml
が生成されます。
コメントを除くとこんな内容です。language
は配下のソースから認識されます。
language: typescript
ignore_all_files_in_gitignore: true
ignored_paths: []
read_only: false
excluded_tools: []
initial_prompt: ""
project_name: "workspace"
インデックスを作成します。
uvx --from git+https://github.com/oraios/serena serena project index
cacheが作成されます。
.serena/
├── cache
│ └── typescript
│ └── document_symbols_cache_v23-06-25.pkl
└── project.yml
3 directories, 2 files
オレオレコーディングエージェントを起動する
serena-app
ディレクトリに移動し、Streamlitを起動します。
cd ..
uv run streamlit run main.py
ブラウザでhttp://localhost:8501/
にアクセスします。
どんなツールが使えるか聞いてみました。すごくたくさんのツールがあるようです。
コーディングっぽいことを聞いてみましょう。
Bedrockの呼び出しをしているソースの場所を特定して
search_for_pattern
ツールを使用して、関連するソースを取得しました。
Bedrockを呼び出しているコードを特定してください。
(使い方が悪いのか、Nova Proでは無限ループっぽい動きをしたのでClaude Sonnet 4で実施しました)
対応している生成AIモデルのモデルIDをすべて教えて
新しく「us.anthropic.claude-opus-5-20261231-v1:0」が追加されたので、「us.anthropic.claude-opus-4-20250514-v1:0」が使えるところに追加してほしいです。
合ってるかわからないですが、修正はガッツリ行われました!
イマイチSerenaの凄さがわからないままですが、なんかできましたw
追記
こっちのほうがスマートな気がします。
import asyncio
import streamlit as st
from mcp import StdioServerParameters, stdio_client
from strands.agent import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
def write_message(message):
with st.chat_message(message["role"]):
for content in message["content"]:
match content:
case {"text": text}:
st.write(text)
pass
case {"toolUse": toolUse}:
with st.expander(
f"toolUse: {toolUse['name']}",
expanded=False,
):
st.write(toolUse)
case {"toolResult": toolResult}:
with st.expander("toolResult", expanded=False):
st.write(toolResult)
async def main():
st.title("Strands Agents with Serena")
with st.sidebar:
st.subheader("設定")
work_dir = st.text_input("作業ディレクトリ", value="./workspace")
model_id = st.text_input(
"モデルID",
value="apac.amazon.nova-pro-v1:0",
# "モデルID", value="apac.anthropic.claude-sonnet-4-20250514-v1:0"
)
region_name = st.text_input("リージョン", value="ap-northeast-1")
if not work_dir:
return
if "messages" not in st.session_state:
st.session_state["messages"] = []
for message in st.session_state["messages"]:
write_message(message)
if prompt := st.chat_input():
with st.chat_message("user"):
st.write(prompt)
mcp_client = MCPClient(
lambda: stdio_client(
StdioServerParameters(
command="uvx",
args=[
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server",
"--project",
work_dir,
"--context",
"ide-assistant",
],
)
)
)
with mcp_client:
tools = mcp_client.list_tools_sync()
model = BedrockModel(
model_id=model_id,
region_name=region_name,
)
agent = Agent(
model=model,
system_prompt="あなたは優秀なエージェントです。ユーザーとの会話は日本語で行います。",
messages=st.session_state[
"messages"
], # agent呼び出し後、自動でユーザーとアシスタントのメッセージがappendされる
tools=tools,
callback_handler=None,
)
agent_stream = agent.stream_async(prompt)
async for event in agent_stream:
match event:
case {"message": message}:
write_message(message)
asyncio.run(main=main())