はじめに
今回は、AWS Blocksの「Agent」をローカルで試してみました。
Agentを使用することで簡易なAIエージェントを構築できます。
AWS Blocksが提供するテンプレートからTODOアプリを作って、AIチャット機能を追加してみます。
AWS Blocksは2026/8/12時点(v0.2.7)でプレビューとなります。
1. AWS Blocksとは
AWS Blocksは、認証・データベース・AIエージェントといったバックエンド機能をレゴブロックのように組み合わせてアプリケーションを構築できるお手軽ツールです。
多種多様なBlockが提供されている
開発者は、AWS Blocksが提供する様々なBlockの中からアプリケーションのユースケースに応じて自由に選択できます。
| カテゴリ | Block | 説明 |
|---|---|---|
| 認証 | AuthBasic | DynamoDBによるユーザー認証 |
| AuthOIDC | 外部の認証サービスとの連携によるユーザー認証 | |
| AuthCognito | Cognitoによる多要素認証・ソーシャルサインイン・パスキーなどによるユーザー認証 | |
| データ永続化 | KVStore | DynamoDBによるシンプルなキー・バリューストア |
| DistributedTable | DynamoDBによる構造化データを扱えるキー・バリューストア | |
| Database | Aurora Serverless v2によるPostgreSQL互換のデータベース | |
| DistributedDatabase | Aurora DSQLによるデータベース | |
| FileBucket | S3によるファイルストレージ | |
| リアルタイム・非同期処理 | Realtime | API GatewayによるWebSocket API |
| AsyncJob | SQS + Lambdaによる非同期処理 | |
| CronJob | EventBridge + Lambdaによるスケジュール処理 | |
| AI | Agent | Strands AgentsによるAIエージェント |
| KnowledgeBase | Bedrock Knowledge Baseによるドキュメント検索 | |
| コミュニケーション | EmailClient | SESによるメール送信 |
| 設定 | AppSetting | ParameterStoreによる設定管理 |
| 可観測性 | Metrics | CloudWatchによるメトリクス |
| Logger | CloudWatch Logsによるログ保存 | |
| Tracer | X-Rayによるリクエストの追跡 | |
| Dashboard | CloudWatchによるダッシュボード | |
| ホスティング | Hosting | CloudFront・S3によるフロントエンドホスティング |
| CI/CD | Pipeline | CodePipelineによるCI/CDパイプライン |
ローカル環境・Sandbox環境・本番環境を切り替えて使える
AWS Blocksでは、以下3つの環境が提供されています。
開発者は、ローカル環境でテストして、次はSandbox環境で実際にAWSアカウントでテストして、といったように環境を切り替えて使えます。
特にローカル環境では、AWSアカウントをすぐに準備できない場合や、まだ作り込んでいないのでとりあえず動かしたい場合に重宝するかなと思いました。
| 環境 | 内容 |
|---|---|
| ローカル環境 | 各種AWSサービスをローカルのNode.js上でモックとして動かす (AWSアカウント不要) |
| Sandbox環境 | AWS環境に使い捨てのリソースを構築する |
| 本番環境 | AWS環境に本番向けのリソースを構築する |
HostingとPipelineは本番環境のみとなります。
テンプレートを使ってすぐに動かせる
AWS Blocksには、あらかじめプロジェクトのテンプレートが用意されています。
以下のようにコマンド引数--templateでテンプレートを指定できます。
npx @aws-blocks/create-blocks-app my-app --template react
用意されているテンプレートのラインナップは以下になります。
本記事ではreactを使用しました。
| テンプレート | フロントエンド | Block | 説明 |
|---|---|---|---|
default |
Vite lit-html |
AuthBasicDistributedTableRealtime
|
ログイン機能付きのTodoアプリ |
react |
Vite React |
同上 |
defaultと同じTodoアプリを、Reactで実装したもの |
demo |
Vite 素のTypeScript |
AuthBasicKVStoreDistributedTable
|
優先度順ソート付きのTodoアプリ |
bare |
Vite lit-html |
なし | APIの結果を表示するだけの最小構成のサンプル |
backend |
なし | なし | フロントエンドを含まず、サンプルAPIのみ |
nextjs |
Next.js (App Router) |
なし | Server/Client ComponentsからBlocksを呼び出すサンプル |
auth-cognito |
Vite 素のTypeScript |
AuthCognitoKVStoreDistributedTable
|
Cognitoの認証機能をひと通り試せるTodoアプリ |
amplify |
なし | KVStore |
Amplify Gen2にAWS Blocksを追加するサンプル |
lit-htmlとは
lit-htmlは、軽量なHTML描画ライブラリです。
ReactよりシンプルでJavaScriptの中に直接HTMLを書いて画面に表示できます。
2. 今回作るTODOアプリの構成
今回はAWS BlocksでAIアシスタント付きTODOアプリを作って、ローカル環境で動かしてみます。
Reactテンプレートを使ってTODOアプリを作成する
プロジェクトは、reactテンプレートを使って構築します。
プロジェクト構成
reactテンプレートの場合、以下のようなプロジェクト構成になります。
主に、AWS Blocksの定義が書いてあるtodo-app/aws-blocks/index.tsとフロントエンドのtodo-app/src/App.tsxを修正していくことになります。
todo-app/
├── aws-blocks/
│ ├── index.ts # 認証・データ・APIなど、実際の「ブロック」を定義
│ ├── index.cdk.ts # CDK Appを起動し、インフラ寄りの構成を追加
│ └── index.handler.ts # Lambda実行時にindex.tsを読み込んで動作
├── src/
│ ├── App.tsx # Todoアプリ本体のReactコンポーネント
│ └── main.tsx
├── test/ # E2Eテスト
├── .blocks/
│ └── config.json # プロジェクト固有のstackIdを持つCLI管理設定ファイル
└── AGENTS.md # AIコーディングエージェント向け説明ファイル
初期構築されるBlock
テンプレートには、ユーザー認証付きのTODOアプリが最初から組み込まれており、以下のBlockが使用されています。
| Block | TODOアプリでの役割 |
|---|---|
AuthBasic |
ユーザー名・パスワードによるユーザー認証 |
DistributedTable |
Todoをユーザーごとに保存 |
Realtime |
WebSocketでTODOをリアルタイム通知 |
AuthBasicによるユーザー認証
AuthBasicの初期設定としては以下が設定されています。
auth.createApi()は、フロントエンドから呼び出す用の認証APIを作っています。
| 設定 | 説明 |
|---|---|
passwordPolicy |
パスワードの最小文字数を8文字に設定する |
crossDomain |
AuthBasicはセッションをCookieで管理するため、クロスドメイン(Sandbox環境)でもCookie送信ができるよう許可する |
import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime } from '@aws-blocks/blocks';
import { z } from 'zod';
const scope = new Scope('todo-app');
const auth = new AuthBasic(scope, 'auth', {
passwordPolicy: { minLength: 8 },
crossDomain: process.env.BLOCKS_SANDBOX === 'true',
});
export const authApi = auth.createApi();
DistributedTableによるTODO保存
DistributedTableの初期設定としては以下が設定されています。
| 設定 | 説明 |
|---|---|
schema |
Todoの型をZodで定義する |
key |
DynamoDBのパーティションキー、ソートキーを定義する |
indexes |
優先度順・タイトル順で一覧を取得するためのDynamoDBのセカンダリインデックスを定義する |
const todoSchema = z.object({
userId: z.string(),
todoId: z.string(),
title: z.string(),
completed: z.boolean(),
priority: z.number(),
version: z.number(),
createdAt: z.number(),
});
const todos = new DistributedTable(scope, 'todos', {
schema: todoSchema,
key: { partitionKey: 'userId', sortKey: 'todoId' },
indexes: {
byPriority: { partitionKey: 'userId', sortKey: 'priority' },
byTitle: { partitionKey: 'userId', sortKey: 'title' },
},
});
RealtimeによるTODO変更通知
Realtimeの初期設定としては以下が設定されています。
| 設定 | 説明 |
|---|---|
namespaces |
通知用のメッセージの型(actionとtodoId)を定義する(actionは作成・更新・削除のどれか) |
const rt = new Realtime(scope, 'live', {
namespaces: {
todos: Realtime.namespace(z.object({
action: z.enum(['created', 'updated', 'deleted']),
todoId: z.string(),
})),
},
});
AWS環境へデプロイした際の裏側
今回はローカル上で動かしますが、実際にAWSへデプロイすると以下のようなAWSサービスが構築されます。
AWS Blocksの共通リソース
上記の図を見ると、どのBlockにも属さないAPI GatewayとLambdaがいると思います。
これはAWS Blocksが用意した全てのBlockで共通となるリソースです。
開発者が任意のREST APIを実装するために使用したり、各Blockが裏側でLambdaを使ったりします。
(内部的には1つのLambdaで用途に合わせて分岐してるようですね。)
追加するBlock①: Agent
今回は、このTODOアプリにAgentというBlockを使って、AIアシスタント機能を追加します。
AIアシスタントは、チャットベースでTodoを操作するAIエージェントです。
Agentが提供する機能
Agentは、以下の機能をもつAIエージェントを構築できます。
| 機能 | 内容 |
|---|---|
| ツール実行 | 既存のAPIやデータ操作を「ツール」として登録しておくと、会話の内容に応じてエージェントが自動的に呼び出す |
| 会話履歴の永続化 | やり取りした内容をデータストアに自動的に保存し、会話を再開できるようにする |
| ストリーミング配信 | 応答をチャンク単位でリアルタイム(WebSocket)に配信する |
コードにすると以下のようになります。
const agent = new Agent(scope, 'todo-assistant', {
model: {
deployed: BedrockModels.BALANCED,
local: OllamaModels.SMALL,
},
systemPrompt: 'あなたはTodoアプリのアシスタントです。ユーザーの指示に応じてTodoを追加・完了・削除します。',
toolContextSchema: z.object({ userId: z.string() }),
tools: (tool) => ({
// 省略
}),
});
modelの設定とローカル実行時のLLMモック
Agentのmodelは、環境ごとに使うモデルを分けて指定します。
-
deployed:AWSへデプロイしたときに使われるモデル -
local:ローカル実行時に使われるモデル (指定有無で動作が変わる)
localの指定 |
ローカルでの挙動 |
|---|---|
| モデルを指定 | 指定したローカルLLM(Ollama等)が呼び出される |
| 指定しない | あらかじめ決まった応答を返すモック(CannedProvider)が呼び出される |
フロントエンドではuseChatでチャット状態を管理する
フロントエンドは、入力したプロンプトをAIエージェントに渡して、その回答を受け取って画面に表示したりする必要があります。
そのため、メッセージの送受信や会話状態を管理するための便利機能としてuseChatが提供されています。
名前にuseが付いていますが、Reactのフックではなくただの関数なので環境に依存しません。
AWS環境へデプロイした際の裏側
AgentをAWS上にデプロイすると以下のようなAWSサービスの構成で動作します。
追加するBlock②: KnowledgeBase
KnowledgeBaseは、指定したフォルダ内のドキュメントを自然言語のクエリで検索できます。
今回は、Agentで構築したAIエージェントにツールとして渡しています。
KnowledgeBaseが提供する機能
KnowledgeBaseは、以下の機能をもつドキュメント検索を構築できます。
| 機能 | 内容 |
|---|---|
| ドキュメント検索 | 文章の意味に基づいて、関連性の高いドキュメントを検索する |
| チャンク分割 | 検索しやすい形に整えるためのチャンク分割ができる |
コードにすると以下のようになります。
sourceで指定したディレクトリ内にドキュメントを入れています。
const kb = new KnowledgeBase(scope, 'tips', {
source: './knowledge',
description: '家電・道具のお手入れメモ',
});
使い道としては、以下のようにAgentのツールに渡してあげることで、AIエージェントに知識を与えることができます。
const agent = new Agent(scope, 'todo-assistant', {
// 省略
tools: (tool) => ({
searchCareInstructions: tool({
description: '家電・道具のお手入れ方法を検索する',
parameters: z.object({ query: z.string() }),
handler: async ({ input }) => {
const results = await kb.retrieve(input.query, { maxResults: 3 });
return results.map(r => r.text);
},
}),
}),
});
AWSにデプロイすると、KnowledgeBaseはAmazon Bedrock Knowledge Basesとして動きます。
3. reactテンプレートでTodoアプリを作ってみる
reactテンプレートをセットアップして、素のTodoアプリをローカルで動かしてみます。
前提条件
前提条件は以下になります。
- Node.js 22以上
- npm 10以上
- AWS Blocks v0.2.7(本記事の動作確認時点のバージョン)
reactテンプレートでプロジェクトを作成する
1. 次のコマンドで新規プロジェクトを作成します。
npx @aws-blocks/create-blocks-app todo-app --template react
2. 作成されたプロジェクトに移動します。
cd todo-app
動作確認: npm run devでTodoアプリを触ってみる
1. ローカル開発サーバーを起動します。
npm run dev
2. ブラウザでhttp://localhost:3000を開きます。

3. サインアップし、Todoを追加・完了・削除できることを確認します。
ローカル起動時のデータ保存先
AuthBasicのローカル実行時は、.bb-data/{Blockごとの識別子}/配下にデータを保存しています。
4. Todoアプリを改造してみる
せっかくなので理解を深めるためにTODOアプリに以下の改造を行います。
- DistributedTableを改造してTODOに期限を追加
- Realtimeを改造してTODOの完了通知を追加
改造1:TODOに期限を追加する
DistributedTableに保存するTodoのスキーマに、新しいフィールドを追加してみます。
バックエンド: todoSchemaとcreateTodoに期限を追加する
1. todoSchemaにdueDateを任意項目として追加します。
const todoSchema = z.object({
userId: z.string(),
todoId: z.string(),
title: z.string(),
completed: z.boolean(),
priority: z.number(),
+ dueDate: z.number().optional(),
version: z.number(),
createdAt: z.number(),
});
2. createTodoがdueDateを受け取れるようにします。
- async createTodo(title: string, priority: number = 2) {
+ async createTodo(title: string, priority: number = 2, dueDate?: number) {
const user = await auth.requireAuth(context);
const todoId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
- const todo = { userId: user.username, todoId, title, completed: false, priority, version: 1, createdAt: Date.now() };
+ const todo = { userId: user.username, todoId, title, completed: false, priority, dueDate, version: 1, createdAt: Date.now() };
await todos.put(todo);
await rt.publish('todos', user.username, { action: 'created' as const, todoId });
return todo;
},
フロントエンド: 期限を入力・表示できるようにする
1. Todoの型にdueDateを追加します。
- type Todo = { todoId: string; title: string; completed: boolean; priority: number; version: number };
+ type Todo = { todoId: string; title: string; completed: boolean; priority: number; dueDate?: number; version: number };
2. 期限を保持ためのuseStateを追加します。
const [newTitle, setNewTitle] = useState('');
const [newPriority, setNewPriority] = useState(2);
+ const [newDueDate, setNewDueDate] = useState('');
3. addTodoが期限を渡すように変更します。
const addTodo = async () => {
if (!newTitle.trim()) return;
+ const dueDate = newDueDate ? new Date(newDueDate).getTime() : undefined;
- await api.createTodo(newTitle.trim(), newPriority);
+ await api.createTodo(newTitle.trim(), newPriority, dueDate);
setNewTitle('');
+ setNewDueDate('');
await load();
};
4. 入力フォームのselectとbuttonの間に、期限用の<input type="date">を追加します。
<select value={newPriority} onChange={e => setNewPriority(Number(e.target.value))}>
<option value={1}>🔴 High</option>
<option value={2}>🟡 Medium</option>
<option value={3}>🟢 Low</option>
</select>
+ <input
+ type="date"
+ value={newDueDate}
+ onChange={e => setNewDueDate(e.target.value)}
+ />
<button onClick={addTodo}>Add</button>
5. Todo一覧の各項目に、期限があれば表示するようにします。
- <span style={{ flex: 1 }}>{t.title}</span>
+ <span style={{ flex: 1 }}>
+ {t.title}
+ {t.dueDate && (
+ <span style={{ marginLeft: 8, fontSize: '0.8em', color: '#888' }}>
+ ({new Date(t.dueDate).toLocaleDateString()})
+ </span>
+ )}
+ </span>
動作確認: 期限を設定してTodoが表示されることを確認する
1. npm run devが起動したままの状態で、ブラウザを再読み込みします。(日付入力が増えてます)

2. Todoを追加するときに期限を指定します。
3. 一覧に、指定した期限が表示されることを確認します。
完了通知(notifications)を追加する
完了時に通知を送るnotificationsネームスペースを、Realtimeに追加します。
バックエンド: 完了通知用のnotificationsネームスペースを追加する
1. Realtimeに、notificationsネームスペースを追加します。
const rt = new Realtime(scope, 'live', {
namespaces: {
todos: Realtime.namespace(z.object({
action: z.enum(['created', 'updated', 'deleted']),
todoId: z.string(),
})),
+ notifications: Realtime.namespace(z.object({
+ message: z.string(),
+ })),
},
});
2. apiにnotificationsチャンネルを購読するメソッドを追加します。
async subscribeTodos() {
const user = await auth.requireAuth(context);
return rt.getChannel('todos', user.username);
},
+ async subscribeNotifications() {
+ const user = await auth.requireAuth(context);
+ return rt.getChannel('notifications', user.username);
+ },
3. toggleTodoでTodoを完了に変更したときだけnotificationsへ配信します。
async toggleTodo(todoId: string) {
const user = await auth.requireAuth(context);
const todo = await todos.get({ userId: user.username, todoId });
if (!todo) throw new Error('Todo not found');
+ const completed = !todo.completed;
- await todos.put({ ...todo, completed: !todo.completed, version: todo.version + 1 }, { ifFieldEquals: { version: todo.version } });
+ await todos.put({ ...todo, completed, version: todo.version + 1 }, { ifFieldEquals: { version: todo.version } });
await rt.publish('todos', user.username, { action: 'updated' as const, todoId });
+ if (completed) {
+ await rt.publish('notifications', user.username, { message: `「${todo.title}」を完了しました` });
+ }
return { success: true };
},
フロントエンド: 通知をトースト表示する
1. トースト表示用のstateを追加します。
const [newDueDate, setNewDueDate] = useState('');
const [sortBy, setSortBy] = useState<SortBy>(undefined);
+ const [toast, setToast] = useState<string | null>(null);
2. RealtimeSubscription型をimportします。
import { api, authApi } from 'aws-blocks';
+ import type { RealtimeSubscription } from '@aws-blocks/blocks';
3. notificationsをサブスクライブし、メッセージが届いたらトーストを3秒間表示するuseEffectを追加します。
useEffect(() => {
let sub: any;
(async () => {
try {
const channel = await api.subscribeTodos();
sub = channel.subscribe(() => load());
await sub.established;
} catch { /* realtime not available in local dev */ }
})();
return () => sub?.unsubscribe();
}, [load]);
+ useEffect(() => {
+ let sub: RealtimeSubscription | undefined;
+ (async () => {
+ const channel = await api.subscribeNotifications();
+ sub = channel.subscribe(({ message }) => {
+ setToast(message);
+ setTimeout(() => setToast(null), 3000);
+ });
+ })();
+ return () => sub?.unsubscribe();
+ }, []);
4. トースト用のUIを追加します。
<p style={{ color: '#888', fontSize: '0.85em' }}>{todos.filter(t => !t.completed).length} remaining</p>
+ {toast && (
+ <div style={{ position: 'fixed', bottom: 16, right: 16, background: '#333', color: '#fff', padding: '8px 16px', borderRadius: 4 }}>
+ {toast}
+ </div>
+ )}
</div>
);
}
動作確認: Todoを完了して通知が表示されることを確認する
1. npm run devが起動したままの状態で、ブラウザを再読み込みします。
2. Todoを完了(チェック)にします。
3. 画面右下にトーストで完了メッセージが表示されることを確認します。
5. チャットでTodoを操作できるAIエージェントを作ってみる
ここから、Agentを追加して、チャットでTodoを操作できるようにします。
まずはセットアップ不要なCannedProvider(モック)でチャット機能を動かしてみます。
その後、実際のローカルLLM(Ollama)に差し替えて試します。
CannedProviderでチャット機能を作る
バックエンド: Agentを追加する
1. importにAgentを追加します。
- import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime } from '@aws-blocks/blocks';
+ import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime, Agent } from '@aws-blocks/blocks';
2. Realtimeの下にAgentを追加します。
const rt = new Realtime(scope, 'live', {
namespaces: {
todos: Realtime.namespace(z.object({
action: z.enum(['created', 'updated', 'deleted']),
todoId: z.string(),
})),
notifications: Realtime.namespace(z.object({
message: z.string(),
})),
},
});
+ const agent = new Agent(scope, 'todo-assistant', {
+ systemPrompt: 'あなたはTodoアプリのアシスタントです。ユーザーの指示に応じてTodoを追加・完了・削除します。',
+ toolContextSchema: z.object({ userId: z.string() }),
+ tools: (tool) => ({
+ // 次のステップでツールを追加します
+ }),
+ });
バックエンド: 既存のTodo操作をツールとして定義する
1. toolsの中にTodoを追加するツールを定義します。
tools: (tool) => ({
- // 次のステップでツールを追加します
+ addTodo: tool({
+ description: 'Todoを新しく追加する。期限が指定されていなければdueDateは省略する',
+ parameters: z.object({
+ title: z.string().describe('Todoのタイトル'),
+ priority: z.number().default(2).describe('優先度。1が高、2が中、3が低'),
+ dueDate: z.number().optional().describe('期限。UNIXタイムスタンプ(ミリ秒)。ユーザーが期限を言っていなければ省略する'),
+ }),
+ handler: async ({ input, context }) => {
+ const todoId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
+ const todo = { userId: context.userId, todoId, title: input.title, completed: false, priority: input.priority, ...(input.dueDate !== undefined ? { dueDate: input.dueDate } : {}), version: 1, createdAt: Date.now() };
+ await todos.put(todo);
+ await rt.publish('todos', context.userId, { action: 'created' as const, todoId });
+ return todo;
+ },
+ }),
2. Todo一覧を取得するツールを追加します。
+ listMyTodos: tool({
+ description: '自分のTodo一覧を取得する',
+ parameters: z.object({}),
+ handler: async ({ context }) => {
+ return await Array.fromAsync(todos.query({ where: { userId: { equals: context.userId } } }));
+ },
+ }),
3. Todoを完了にするツールを追加します。
+ completeTodo: tool({
+ description: 'Todoを完了にする',
+ parameters: z.object({ todoId: z.string() }),
+ handler: async ({ input, context }) => {
+ const todo = await todos.get({ userId: context.userId, todoId: input.todoId });
+ if (!todo) throw new Error('Todo not found');
+ await todos.put({ ...todo, completed: true, version: todo.version + 1 }, { ifFieldEquals: { version: todo.version } });
+ await rt.publish('todos', context.userId, { action: 'updated' as const, todoId: input.todoId });
+ return { success: true };
+ },
+ }),
4. Todoを削除するツールを追加します。
+ deleteTodo: tool({
+ description: 'Todoを削除する',
+ parameters: z.object({ todoId: z.string() }),
+ handler: async ({ input, context }) => {
+ await todos.delete({ userId: context.userId, todoId: input.todoId });
+ await rt.publish('todos', context.userId, { action: 'deleted' as const, todoId: input.todoId });
+ return { success: true };
+ },
+ }),
}),
これでエージェントが4つのツールのいずれかを呼び出せるようになりました。
バックエンド: チャット用のAPIを追加する
1. 既存のapiの中にAIエージェントと会話を開始する関数を追加します。
+ async createConversation() {
+ const user = await auth.requireAuth(context);
+ return { conversationId: await agent.createConversationId(user.username) };
+ },
2. メッセージを送信する関数を追加します。
+ async sendChatMessage(conversationId: string, message: string, channelId: string) {
+ const user = await auth.requireAuth(context);
+ await agent.stream(message, { conversationId, channelId, userId: user.username, context: { userId: user.username } });
+ },
3. 会話履歴を取得する関数を追加します。
+ async getChatHistory(conversationId: string) {
+ await auth.requireAuth(context);
+ const messages = await agent.getConversation(conversationId);
+ return { messages };
+ },
4. ストリーミング用のチャンネルを取得する関数を追加します。
+ async getAgentChannel(channelId: string) {
+ return await agent.getChannel(channelId);
+ },
フロントエンド: チャットUIを追加する
1. チャット用にuseChatをインポートします。
import { api, authApi } from 'aws-blocks';
+ import { useChat } from '@aws-blocks/bb-agent/client';
2. チャットUIのChatコンポーネントを追加します。
+ function Chat() {
+ const [messages, setMessages] = useState<{ id: string; role: string; content: string }[]>([]);
+ const [input, setInput] = useState('');
+ const chatRef = useRef<ReturnType<typeof useChat> | null>(null);
+
+ if (!chatRef.current) {
+ chatRef.current = useChat({
+ api: {
+ sendMessage: (convId, msg, chId) => api.sendChatMessage(convId, msg, chId),
+ createConversation: () => api.createConversation(),
+ getConversation: (id) => api.getChatHistory(id),
+ },
+ subscribe: async (channelId, handler) => {
+ const channel = await api.getAgentChannel(channelId);
+ return channel.subscribe(handler);
+ },
+ onMessagesChange: (msgs) => setMessages(msgs),
+ });
+ }
+
+ const send = async () => {
+ if (!input.trim()) return;
+ await chatRef.current!.sendMessage(input.trim());
+ setInput('');
+ };
+
+ return (
+ <div style={{ marginTop: 24, borderTop: '1px solid #ddd', paddingTop: 16 }}>
+ <h2>Todoアシスタント</h2>
+ <ul style={{ listStyle: 'none', padding: 0 }}>
+ {messages.map((m) => (<li key={m.id}><strong>{m.role}:</strong> {m.content}</li>))}
+ </ul>
+ <input
+ value={input}
+ onChange={e => setInput(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && send()}
+ placeholder="牛乳を買うタスクを追加して"
+ style={{ width: '100%', padding: 8 }}
+ />
+ </div>
+ );
+ }
3. Appコンポーネントの中でTodoAppの下にChatを追加します。
{user && <TodoApp />}
+ {user && <Chat />}
動作確認: CannedProviderの応答を確認する
1. npm run devが起動したままの状態で、ブラウザを再読み込みします。(一番下にアシスタントが追加されてます)

2. サインイン済みの状態で、チャット欄に「牛乳を買うタスクを追加して」のように入力します。
3. 「This is a canned mock response. No real model was called. [canned]」という決まった応答が返ってくることを確認します。この時点でTodoは追加されません。
ローカルLLM(Ollama)に差し替えて試す
次はこの部分を実際のローカルLLM(Ollama)に差し替えて試してみます。
Ollamaをセットアップする
1. Ollamaをインストールします。アカウント登録は不要です。
curl -fsSL https://ollama.com/install.sh | sh
2. Ollamaを起動します。
ollama serve
3. モデルをダウンロードします。
ollama pull llama3.1:8b
バックエンド: model.localにOllamaを指定する
1. importにBedrockModels・OllamaModelsを追加します。
- import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime, Agent } from '@aws-blocks/blocks';
+ import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime, Agent, BedrockModels, OllamaModels } from '@aws-blocks/blocks';
2. Agentの宣言にmodelを追加します。
const agent = new Agent(scope, 'todo-assistant', {
+ model: {
+ deployed: BedrockModels.BALANCED,
+ local: OllamaModels.SMALL,
+ },
systemPrompt: 'あなたはTodoアプリのアシスタントです。ユーザーの指示に応じてTodoを追加・完了・削除します。',
toolContextSchema: z.object({ userId: z.string() }),
tools: (tool) => ({
動作確認: 実際のLLMでチャットしてみる
1. npm run devを再起動します。
2. チャット欄に、CannedProviderのときと同じ「牛乳を買うタスクを追加して」を入力します。
3. 今度はTodo一覧に実際にタスクが追加されることを確認します。
6. KnowledgeBaseで家電・道具のお手入れ方法を答えられるようにする
最後にKnowledgeBaseを追加して、AIエージェントに専門的な知識を与えてみます。
事前準備:ナレッジ用のドキュメントを用意する
今回は試しに家電や道具のお手入れ方法を与えてみます。
1. 次のコマンドでtodo-app/knowledgeディレクトリを新規作成します。
mkdir -p todo-app/knowledge
2. 次のコマンドでtips.mdファイルを新規作成します。
cat << 'EOF' > todo-app/knowledge/tips.md
# 家電・道具のお手入れメモ
## コーヒーメーカー「ブリューマスターX7」
去年フィルターを2ヶ月放置したら、抽出口から茶色い水が逆流してきたことがある。取扱説明書には特に指定がないが、実際は3週間ごとに替えないと詰まる。
## 加湿器「ミストピュアG2」
タンクの底に白い結晶(カルキ)が溜まりやすい機種。一度放置しすぎて本体ごと買い替える羽目になったので、それ以来月イチで重曹水につけ置きしている。
## 自転車「サイクロンZ」
後輪のブレーキだけ、他の自転車の感覚で握ると効きすぎて急停止する。慣れるまでは前輪ブレーキを多めに使うようにしている。
EOF
ここまででドキュメントの準備ができました。
バックエンド: KnowledgeBaseを追加し、検索ツールとして登録する
1. KnowledgeBaseをインポートし、knowledgeフォルダを指定します。
- import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime, Agent, BedrockModels, OllamaModels } from '@aws-blocks/blocks';
+ import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime, Agent, BedrockModels, OllamaModels, KnowledgeBase } from '@aws-blocks/blocks';
+ const kb = new KnowledgeBase(scope, 'tips', {
+ source: './knowledge',
+ description: '家電・道具のお手入れメモ',
+ });
2. agentのtoolsにkb.retrieve()でお手入れ方法を検索するツールを追加します。
+ searchCareInstructions: tool({
+ description: '家電・道具のお手入れ方法を検索する',
+ parameters: z.object({ query: z.string() }),
+ handler: async ({ input }) => {
+ const results = await kb.retrieve(input.query, { maxResults: 3 });
+ return results.map(r => r.text);
+ },
+ }),
動作確認: お手入れ方法を聞くとナレッジベースから回答することを確認する
1. npm run devが起動したままの状態で、チャット欄に次のような質問を入力します。
- 「私が使っている加湿器のお手入れ方法を教えてください」
- 「私が使っている自転車のお手入れ方法を教えてください」
- 「私が使っているコーヒーメーカーのお手入れ方法を教えてください」
2. tips.mdに書いた固有の内容そのままの回答が返ってくることを確認します。
これでローカルで動作するAIアシスタント付きTODOアプリを作ることができました。
最終コードは以下になります。
環境の後片付け
動作確認が終わったら、ローカルにダウンロードしたOllamaのモデルを削除しておきます。
1. 次のコマンドでダウンロードしたモデルを削除します。
ollama rm llama3.1:8b
まとめ
今回は、AWS BlocksのAgentを使って、AIアシスタント付きTODOアプリを作ってみました。
Agentを使ってみた感想としては、StrandsAgents周りなどAIエージェント固有部分がうまく抽象化されており、誰でも簡単にAIエージェントが構築できそうでした。
ただ以下の記事にもある通り、Agentの裏側がAgentCore Runtimeではない点は気になりました。
AgentをAgentCoreで置き換えるPRは出ているので、今後の動向を見守りたいと思います。


