3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS Blocks】Agentを使ってStrandsAgentsのAIエージェントを動かしてみよう (Ollamaでローカル実行まで)

3
Posted at

はじめに

今回は、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環境に本番向けのリソースを構築する

HostingPipelineは本番環境のみとなります。

テンプレートを使ってすぐに動かせる

AWS Blocksには、あらかじめプロジェクトのテンプレートが用意されています。

以下のようにコマンド引数--templateでテンプレートを指定できます。

npx @aws-blocks/create-blocks-app my-app --template react

用意されているテンプレートのラインナップは以下になります。
本記事ではreactを使用しました。

テンプレート フロントエンド Block 説明
default Vite
lit-html
AuthBasic
DistributedTable
Realtime
ログイン機能付きのTodoアプリ
react Vite
React
同上 defaultと同じTodoアプリを、Reactで実装したもの
demo Vite
素のTypeScript
AuthBasic
KVStore
DistributedTable
優先度順ソート付きのTodoアプリ
bare Vite
lit-html
なし APIの結果を表示するだけの最小構成のサンプル
backend なし なし フロントエンドを含まず、サンプルAPIのみ
nextjs Next.js
(App Router)
なし Server/Client ComponentsからBlocksを呼び出すサンプル
auth-cognito Vite
素のTypeScript
AuthCognito
KVStore
DistributedTable
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送信ができるよう許可する
todo-app/aws-blocks/index.ts
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のセカンダリインデックスを定義する
todo-app/aws-blocks/index.ts
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 通知用のメッセージの型(actiontodoId)を定義する
(actionは作成・更新・削除のどれか)
todo-app/aws-blocks/index.ts
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-architecture.png

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)に配信する

コードにすると以下のようになります。

todo-app/aws-blocks/index.ts
  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サービスの構成で動作します。

aws-architecture2.png

追加するBlock②: KnowledgeBase

KnowledgeBaseは、指定したフォルダ内のドキュメントを自然言語のクエリで検索できます。
今回は、Agentで構築したAIエージェントにツールとして渡しています。

KnowledgeBaseが提供する機能

KnowledgeBaseは、以下の機能をもつドキュメント検索を構築できます。

機能 内容
ドキュメント検索 文章の意味に基づいて、関連性の高いドキュメントを検索する
チャンク分割 検索しやすい形に整えるためのチャンク分割ができる

コードにすると以下のようになります。
sourceで指定したディレクトリ内にドキュメントを入れています。

todo-app/aws-blocks/index.ts
const kb = new KnowledgeBase(scope, 'tips', {
  source: './knowledge',
  description: '家電・道具のお手入れメモ',
});

使い道としては、以下のようにAgentのツールに渡してあげることで、AIエージェントに知識を与えることができます。

todo-app/aws-blocks/index.ts
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を開きます。
image.png

3. サインアップし、Todoを追加・完了・削除できることを確認します。

image.png

ローカル起動時のデータ保存先

AuthBasicのローカル実行時は、.bb-data/{Blockごとの識別子}/配下にデータを保存しています。

4. Todoアプリを改造してみる

せっかくなので理解を深めるためにTODOアプリに以下の改造を行います。

  • DistributedTableを改造してTODOに期限を追加
  • Realtimeを改造してTODOの完了通知を追加

改造1:TODOに期限を追加する

DistributedTableに保存するTodoのスキーマに、新しいフィールドを追加してみます。

バックエンド: todoSchemaとcreateTodoに期限を追加する

1. todoSchemadueDateを任意項目として追加します。

todo-app/aws-blocks/index.ts
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. createTododueDateを受け取れるようにします。

todo-app/aws-blocks/index.ts
- 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を追加します。

todo-app/src/App.tsx
- 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を追加します。

todo-app/src/App.tsx
  const [newTitle, setNewTitle] = useState('');
  const [newPriority, setNewPriority] = useState(2);
+ const [newDueDate, setNewDueDate] = useState('');

3. addTodoが期限を渡すように変更します。

todo-app/src/App.tsx
  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. 入力フォームのselectbuttonの間に、期限用の<input type="date">を追加します。

todo-app/src/App.tsx
  <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一覧の各項目に、期限があれば表示するようにします。

todo-app/src/App.tsx
- <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が起動したままの状態で、ブラウザを再読み込みします。(日付入力が増えてます)
image.png

2. Todoを追加するときに期限を指定します。
3. 一覧に、指定した期限が表示されることを確認します。

完了通知(notifications)を追加する

完了時に通知を送るnotificationsネームスペースを、Realtimeに追加します。

バックエンド: 完了通知用のnotificationsネームスペースを追加する

1. Realtimeに、notificationsネームスペースを追加します。

todo-app/aws-blocks/index.ts
  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. apinotificationsチャンネルを購読するメソッドを追加します。

todo-app/aws-blocks/index.ts
  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へ配信します。

todo-app/aws-blocks/index.ts
  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を追加します。

todo-app/src/App.tsx
  const [newDueDate, setNewDueDate] = useState('');
  const [sortBy, setSortBy] = useState<SortBy>(undefined);
+ const [toast, setToast] = useState<string | null>(null);

2. RealtimeSubscription型をimportします。

todo-app/src/App.tsx
  import { api, authApi } from 'aws-blocks';
+ import type { RealtimeSubscription } from '@aws-blocks/blocks';

3. notificationsをサブスクライブし、メッセージが届いたらトーストを3秒間表示するuseEffectを追加します。

todo-app/src/App.tsx
  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を追加します。

todo-app/src/App.tsx
      <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を追加します。

todo-app/aws-blocks/index.ts
- import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime } from '@aws-blocks/blocks';
+ import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime, Agent } from '@aws-blocks/blocks';

2. Realtimeの下にAgentを追加します。

todo-app/aws-blocks/index.ts
  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を追加するツールを定義します。

todo-app/aws-blocks/index.ts
  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一覧を取得するツールを追加します。

todo-app/aws-blocks/index.ts
+ listMyTodos: tool({
+   description: '自分のTodo一覧を取得する',
+   parameters: z.object({}),
+   handler: async ({ context }) => {
+     return await Array.fromAsync(todos.query({ where: { userId: { equals: context.userId } } }));
+   },
+ }),

3. Todoを完了にするツールを追加します。

todo-app/aws-blocks/index.ts
+ 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を削除するツールを追加します。

todo-app/aws-blocks/index.ts
+ 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エージェントと会話を開始する関数を追加します。

todo-app/aws-blocks/index.ts
+ async createConversation() {
+   const user = await auth.requireAuth(context);
+   return { conversationId: await agent.createConversationId(user.username) };
+ },

2. メッセージを送信する関数を追加します。

todo-app/aws-blocks/index.ts
+ 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. 会話履歴を取得する関数を追加します。

todo-app/aws-blocks/index.ts
+ async getChatHistory(conversationId: string) {
+   await auth.requireAuth(context);
+   const messages = await agent.getConversation(conversationId);
+   return { messages };
+ },

4. ストリーミング用のチャンネルを取得する関数を追加します。

todo-app/aws-blocks/index.ts
+ async getAgentChannel(channelId: string) {
+   return await agent.getChannel(channelId);
+ },

フロントエンド: チャットUIを追加する

1. チャット用にuseChatをインポートします。

todo-app/src/App.tsx
  import { api, authApi } from 'aws-blocks';
+ import { useChat } from '@aws-blocks/bb-agent/client';

2. チャットUIのChatコンポーネントを追加します。

todo-app/src/App.tsx
+ 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を追加します。

todo-app/src/App.tsx
  {user && <TodoApp />}
+ {user && <Chat />}

動作確認: CannedProviderの応答を確認する

1. npm run devが起動したままの状態で、ブラウザを再読み込みします。(一番下にアシスタントが追加されてます)
image.png

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にBedrockModelsOllamaModelsを追加します。

todo-app/aws-blocks/index.ts
- 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を追加します。

todo-app/aws-blocks/index.ts
  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フォルダを指定します。

todo-app/aws-blocks/index.ts
- 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. agenttoolskb.retrieve()でお手入れ方法を検索するツールを追加します。

todo-app/aws-blocks/index.ts
+ 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は出ているので、今後の動向を見守りたいと思います。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?