0
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?

Google A2UI 入門:Angular で「喋るUI」を扱ってみる

0
Last updated at Posted at 2026-06-05

なにこれ

みんなが大好きな[要出典] Angular と、最近流行りの A2UI を組み合わせて、AI エージェント向けのリッチな UI をステップバイステップで作ります。

こんな人向け

  • 最近の AI エージェントって、UIリッチじゃね? と気づいてしまった人
  • AI エージェントアプリのフロントエンドの作り方を知りたい! と思っている人
  • A2UI ってそもそもなに? という人
  • Angular ちょっと分かる! という人(重要)

はじめに

先日、話題の新刊『Amazon Bedrock AgentCore 実践入門』が発売されました。

弊社エンジニアの福地 はるきがテクニカルレビューアとして制作に関与しています。私も発売日に購入しましたが、2026年5月時点における AgentCore の最新知識を広く吸収できる貴重な一冊だと思いました1

さて、ここ数年の AI エージェントの進化と発展は著しく、昨今は LLM の応答内容も大変インタラクティブかつリッチになりました。しかしそれゆえに、フロントエンド側の実装が追い付かないという課題も浮上しつつあります。

かつて、LLM がプレーンテキストだけを応答していた時代は、まだ原始的な UI だけで事足りたものですが、そのうち「ユーザーに選択肢を提示して選ばせたい」「承認ボタンを表示したい」など、UI のバリエーションがいたずらにややこしくなったせいで、フロントエンドの実装対応が切実なボトルネックと化しました。

かといって、エージェントが生成した HTML や任意のコードをそのまま無条件で画面に流し込むのは危険きわまりないことです。エージェントの出力を、安全に素早く UI に反映するにはどうすればよいでしょうか。

こうした諸々の課題に対応するため、AG-UI (Agent-User Interaction Protocol)A2UI (Agent-to-User Interface) などの標準規格が登場しました2。今日はこれらのうち、A2UI を取り上げていきます3

A2UI とは

A2UI (Agent-to-User Interface) 4は、AI エージェントがリッチで対話的な UI を生成し、それを Web・モバイル・デスクトップでネイティブに描画するためのオープンな仕組みです。

Google が提唱し、Apache 2.0 ライセンスで公開されています。

かなり大雑把ですが、A2UI は JSON から UI を錬成するプロトコルです。
これによって AI エージェントの可能性が大きく広がる可能性を秘めています。

なぜそんなものが必要なのか

古来、AI エージェントの出力を UI に反映する方法として、主に2つの選択肢がありました。

  1. プレーンテキストだけを返す: 安全だが、表現力がなさすぎる
  2. HTML やコードを返す: 表現力はあるが、危険すぎる

どちらも極端すぎたため、その間を取る A2UI が満を持して登場しました。エージェントは、なまの HTML や実行可能な UI コードの代わりに「何を表示したいか」を表す宣言的な JSON を返却します5

そして、その JSON をレンダラーrendererが実際の UI コンポーネント部品に変換します。 この変換処理は、あくまでクライアント(フロントエンド)側の機能です。

今までのやり方となにが違うのか

端的には、クライアント側が許可していない UI を LLM が勝手に表示してしまわないよう制御できる点がポイントです。

クライアントは、あらかじめ承認した信頼できるコンポーネント群(Button や Text、Card など)をカタログcatalogとして保持しています。

エージェントは「カタログにある部品を、こう並べてほしい」とリクエストできるだけで、カタログにない部品を勝手に持ち込むことはできません。

この設計のおかげで、以下のメリットが得られます。

  • 安全: 任意のコードが実行されない。レンダリングはクライアントの自前の UI コンポーネントで行う
  • LLM フレンドリー: フラットな JSON なので、LLM が生成しやすい
  • フレームワーク非依存: 同じ JSON に基づいて Angular でも Flutter でも Lit でも利用できる

2点目はややイメージしづらいため、少し直感的な補足をします。

UI のツリー構造は、 HTML では入れ子構造として表現するしかありませんでした。LLM の気持ちになって考えてみると、UI の階層が深くなるほど「この親タグは、いつになれば閉じるのだろう……」と延々と気にし続けなくてはなりません。

一方、JSON ならば、親子を入れ子にするのではなく、すべて1次元のフラットな配列の要素として扱うことができます(詳しくは後述: 手順 5を参照)。

配下にある子要素の出力をわざわざ待つことなく親要素の定義を完結できるため、LLM にとって思考量が減るとされています。

―― 前置きはこのくらいにして、ここからは実際に手を動かしつつ A2UI の使い方を確かめていきましょう。

チュートリアル: Angular × A2UI

完成形のイメージ

今回は、旅行アシスタント風の小さなデモを作成します。 初めに、これから作るものの完成形をイメージしていただくため、全体外観を下図に示します。

なお、今回の主題は A2UI を試すことなので、本物の AI エージェントは構築せず、固定応答を返却するだけのモック API で代替することとします。

image.png

大まかな流れは以下のとおりです。

  • アプリを開くと、AI エージェントのふりをしたモック API からウェルカムカードのJSONが届いて表示される
  • 「おすすめを見る」ボタンを押すと、その操作が recommend アクションとして API に送信される
  • API がおすすめ一覧の JSON を返し、画面表示が切り替わる

先の話を少しだけすると、このチュートリアルで実装する Angular のテンプレート HTML は実にシンプルです(手順 9参照)。

<a2ui-surface surfaceId="main" />

エージェントからの応答に基づいて UI が動的にレンダリングする仕組みが裏で動いているおかげです。

実行環境

今回の記事は、以下の環境で動作確認を行いました。

  • Node.js: v24.16.0
  • npm: v11.4.2
  • Angular CLI: v22.0.0-rc.2
  • Angular: v22.0.0-rc.2

チュートリアル後のディレクトリ構成

参考までに、最終的なディレクトリ構成は、おおよそ次のようになります。
この図は折に触れて再掲しますので、全体像を軽くかんしたら先に進んでください。

travel-a2ui/
└─ src/app/
   ├─ theme.ts              ← 見た目の定義(公式サンプルから流用)
   ├─ mock-agent.service.ts ← API レスポンスを返す開発用モック(エージェント役)
   ├─ app.config.ts         ← A2UI・HttpClient・モックのセットアップ
   ├─ app.ts                ← API を叩いて描画する本体
   └─ app.html              ← サーフェスを表示する1行

手順1. プロジェクトを作る

まずは Angular のアプリを新規作成します。
ターミナルに以下のコマンドを入力しましょう。

$ ng new travel-a2ui --style=css --ssr=false
$ cd travel-a2ui

手順2. A2UI を導入する

次に、A2UI の Angular レンダラーと、その土台となる web_core、テキスト描画に使う markdown-it、それにモック用の angular-in-memory-web-api を追加します。

$ npm install @a2ui/angular @a2ui/web_core @a2ui/markdown-it
$ npm install angular-in-memory-web-api

@a2ui/markdown-it は、A2UI の Text コンポーネントが内部で使う Markdown レンダラーです(HTML 化と dompurify による無害化を行います)。A2UI のピア依存ですが、自動では入らないことがあるので明示的に入れておきます。

本記事執筆時点の最新である @a2ui/angular 0.10.0 は Angular 21 以降をピア依存に持つため、v22 でもそのまま動きます。

angular-in-memory-web-api の最新版(0.21.0)はピア依存が Angular 21 となっており、v22 では npm install がピア依存の警告でエラーになることがあります。その場合は npm install angular-in-memory-web-api --force で入れてください。ライブラリの API は安定しているため、動作上の問題はありません。

手順3. テーマの作成

A2UI のセットアップには、テーマ(見た目の定義)が必要です。中身は色や余白の指定が並ぶだけの、純粋に装飾用のオブジェクトです。チュートリアルの本筋ではないので、公式サンプルのテーマをそのまま流用します。

travel-a2ui/
└─ src/app/
   ├─ theme.ts              ← 見た目の定義(公式サンプルから流用) ★ いまここ ★
   ├─ mock-agent.service.ts ← API レスポンスを返す開発用モック(エージェント役)
   ├─ app.config.ts         ← A2UI・HttpClient・モックのセットアップ
   ├─ app.ts                ← API を叩いて描画する本体
   └─ app.html              ← サーフェスを表示する1行

今回は、見栄えのする restaurant サンプルの theme.ts を使いましょう。ボタンや見出しにグラデーション、カードにガラス風の質感が入った今風のテーマです。

ファイルの中身をすべてコピーし、src/app/theme.ts として保存してください(以下のような内容のファイルです)。

theme.ts
export const theme: Types.Theme = {
  /* あまりにも長いので省略 */
}

このテーマは CSS の light-dark() を使っているので、OS のダーク/ライト設定に自動で追従します。装飾しているのは Button / Text / Card といった標準カタログの部品だけなので、そのまま貼るだけで動作します。スタイルシートを1枚読み込むのと同じ感覚で、後から差し替えできます。

手順4. モック(ダミーのエージェント)を作る

今回は、本物のエージェントの代わりに、 固定の A2UI メッセージを返すだけのモック を用意します。これに使うのが、先ほど導入した angular-in-memory-web-api です。

travel-a2ui/
└─ src/app/
   ├─ theme.ts              ← 見た目の定義(公式サンプルから流用)
   ├─ mock-agent.service.ts ← API レスポンスを返す開発用モック(エージェント役) ★ いまここ ★
   ├─ app.config.ts         ← A2UI・HttpClient・モックのセットアップ
   ├─ app.ts                ← API を叩いて描画する本体
   └─ app.html              ← サーフェスを表示する1行

src/app/mock-agent.service.ts を以下のように新規作成します。createDb() は内部のデータ置き場を返すお約束のメソッドで、今回は空で構いません。

src/app/mock-agent.service.ts
import { Injectable } from '@angular/core';
import { InMemoryDbService, RequestInfo } from 'angular-in-memory-web-api';

@Injectable({ providedIn: 'root' })
export class MockAgentService implements InMemoryDbService {
  createDb() {
    return { agent: [] };
  }
}

api/agent/... 宛てのリクエストは、この agent というまとまり宛てとして扱われます。

次に、POST を受けたときの振る舞いを post() で上書きします。リクエストボディから userAction を取り出し、応答メッセージを返します。

src/app/mock-agent.service.ts
  post(reqInfo: RequestInfo) {
    const body = (reqInfo.req as { body?: { userAction?: { name: string } | null } }).body;
    const action = body?.userAction ?? null;
    return reqInfo.utils.createResponse$(() => ({
      status: 200,
      body: { messages: reply(action) },
    }));
  }

reply(action) が、エージェントのモック実装です。

ゆくゆくは、次のように引数に基づいて応答 JSON を振り分けたいのですが、説明の都合上、さしあたってウェルカムカード用の JSON だけを固定で返却するようにしておきます。

recommend の分岐は、のちの手順 11で足します。あわせて、応答メッセージの型 ServerToClientMessage と、あとで使う部品の型 ComponentInstance も、ここで import しておきます。

src/app/mock-agent.service.ts
import type { ComponentInstance, ServerToClientMessage } from '@a2ui/web_core/types/types';

function reply(action: { name: string } | null): ServerToClientMessage[] {
  // いまは初回のウェルカム画面だけを返す。recommend の分岐は手順 11 で足す。
  return welcomeMessages();
}

手順5. ウェルカムカードのメッセージを組み立てる

引き続き mock-agent.service.ts を実装します。

travel-a2ui/
└─ src/app/
   ├─ theme.ts              ← 見た目の定義(公式サンプルから流用)
   ├─ mock-agent.service.ts ← API レスポンスを返す開発用モック(エージェント役) ★ いまここ ★
   ├─ app.config.ts         ← A2UI・HttpClient・モックのセットアップ
   ├─ app.ts                ← API を叩いて描画する本体
   └─ app.html              ← サーフェスを表示する1行

ウェルカムカードの UI 構造

ざっくり完成イメージを把握するため、これから作ろうとしている UI の構造を示しておきましょう。
HTML でマークアップするのではありません。 あくまで、AI エージェントにこの構造を喋らせる6のです。

image.png

カード

ここからは、表示する部品をフラットな配列で定義していきます。

先ほど示した図のとおり、カードの中にColumn、その中に部品を並べる構造です。

src/app/mock-agent.service.ts
function welcomeMessages(): ServerToClientMessage[] {
  const components: ComponentInstance[] = [
    { id: 'root', component: { Card: { child: 'col' } } },
    { id: 'col', component: { Column: { children: { explicitList: ['title', 'lead', 'btn'] } } } },
  ];
  // 続く

Columnchildren{ explicitList: [...] } の形で、子の ID を並べます。

見出しと本文

次に、その列から参照されている見出しと本文を追加します。

Text の見出しには usageHint: 'h2' を付けて大きさを強調できます。

src/app/mock-agent.service.ts
  components.push(
    { id: 'title', component: { Text: { text: { literalString: 'ようこそ、旅行アシスタントへ' }, usageHint: 'h2' } } },
    { id: 'lead', component: { Text: { text: { literalString: '行き先選びをお手伝いします。' } } } },
  );

ボタン

ボタンの実装は少し注意が必要です。

ボタンそのものとは別に、ボタン上に表示するテキストを別の Text として用意する必要があります。
ボタンの child 要素にラベルの ID を指定することで、ボタンとテキストを紐づけます。

また、ボタンの要素として、押されたときに発火する action も指定します。

src/app/mock-agent.service.ts
  components.push(
    { id: 'btn', component: { Button: { child: 'btnLabel', action: { name: 'recommend' } } } },
    { id: 'btnLabel', component: { Text: { text: { literalString: 'おすすめを見る' } } } },
  );

ここで指定した action.name'recommend')が、ボタンが押されたときフロントエンドから送られ、reply() の分岐の判断に使用されます。

戻り値の組み立て

最後に、戻り値として surfaceUpdatebeginRendering を返却します。

このうち surfaceUpdate は描画したい部品の一覧、beginRendering はそれをどこから表示開始すればよいかを表しています。

src/app/mock-agent.service.ts
  return [
    { surfaceUpdate: { surfaceId: 'main', components } },
    { beginRendering: { surfaceId: 'main', root: 'root' } },
  ];
}

ここで、surfaceId は画面上の描画領域を一意に区別するための ID で、Angular のテンプレートと紐づきます(手順 9参照)。たとえば、main だけでなく sidebarmodal のように、更新対象が複数領域に及ぶ複雑なアプリケーションであっても、応用が利きます。

surfaceUpdate のデータ構造

ここまでの手順で作成した JSON は、データ構造としては一見フラットな配列ですが、child / children の ID 参照をたどると以下のような UI 階層を再現できます。

beginRendering で指定した root が UI の頂点です。

上図の階層がそのまま DOM の親子関係になります。

これで、初回アクセスに対してウェルカムカードの JSON を返せるようになりました。

手順6. provideA2UI / HttpClient / モックを登録する

travel-a2ui/
└─ src/app/
   ├─ theme.ts              ← 見た目の定義(公式サンプルから流用)
   ├─ mock-agent.service.ts ← API レスポンスを返す開発用モック(エージェント役)
   ├─ app.config.ts         ← A2UI・HttpClient・モックのセットアップ ★ いまここ ★
   ├─ app.ts                ← API を叩いて描画する本体
   └─ app.html              ← サーフェスを表示する1行

src/app/app.config.ts を開き、必要なモジュールの import を追加します。

src/app/app.config.ts
import { importProvidersFrom } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { provideA2UI, DEFAULT_CATALOG, provideMarkdownRenderer } from '@a2ui/angular';
import { MockAgentService } from './mock-agent.service';
import { theme } from './theme';

DEFAULT_CATALOG には、Text / Button / Card / Column / Row / Image / TextField といった標準コンポーネントがひととおり入っています。これが、先ほど「カタログ」と呼んだクライアントの信頼できる部品の実体です。

続いて、既存の providers 配列を以下のように修正します。HttpClient を用意し、そのリクエストをモックに横取りさせ、A2UI をセットアップする、という仕掛けになっています。

src/app/app.config.ts
  providers: [
    // ...既存のプロバイダー
    provideHttpClient(),
    importProvidersFrom(HttpClientInMemoryWebApiModule.forRoot(MockAgentService, { delay: 300 })),
    provideMarkdownRenderer(),
    provideA2UI({ catalog: DEFAULT_CATALOG, theme }),
  ],

delay: 300 は、応答に少し間を持たせて本物の API らしさを出すための演出です。

provideMarkdownRenderer() は、Text コンポーネントが内部で使う Markdown レンダラーの登録です7

provideA2UI に部品 (catalog) と見た目 (theme) を渡したことで、カタログの部品とテーマの紐づけもできました。

手順7. API を実行し、結果をレンダラーに流す

いよいよフロントエンド本体の実装です。

travel-a2ui/
└─ src/app/
   ├─ theme.ts              ← 見た目の定義(公式サンプルから流用)
   ├─ mock-agent.service.ts ← API レスポンスを返す開発用モック(エージェント役)
   ├─ app.config.ts         ← A2UI・HttpClient・モックのセットアップ
   ├─ app.ts                ← API を叩いて描画する本体 ★ いまここ ★
   └─ app.html              ← サーフェスを表示する1行

src/app/app.ts をエディタで開き、API を呼び出す HttpClient と、A2UI のメッセージを処理する MessageProcessor を import に追加します。

src/app/app.ts
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Surface, MessageProcessor } from '@a2ui/angular';
import type { ServerToClientMessage } from '@a2ui/web_core/types/types';

MessageProcessor は、届いた A2UI メッセージを解釈して内部のサーフェスsurfaceを更新する働きをします。

サーフェスとは、大雑把に言うと AI エージェントの応答を描画する UI の領域のことです。 たとえば、チャットボットアプリで AI とのやり取りを表示する領域がその具体例です。

次に、API レスポンスの型を表すインタフェースを用意します。

src/app/app.ts
interface TurnResponse {
  messages: ServerToClientMessage[];
}

@Componentデコレータの imports に描画用の Surface を追加し、さらに、API にリクエストを送信して応答メッセージを処理する共通メソッド sendTurn を作成します。

API の URL は api/agent/turn です。

src/app/app.ts
@Component({ selector: 'app-root', imports: [Surface], templateUrl: './app.html' })
export class App {
  private readonly http = inject(HttpClient);
  private readonly processor = inject(MessageProcessor);

  private sendTurn(userAction: { name: string } | null) {
    this.http.post<TurnResponse>('api/agent/turn', { userAction })
      .subscribe(res => this.processor.processMessages(res.messages));
  }
}

processMessages を呼ぶと、レンダラー内部のサーフェスが更新され、Signal を通じて画面が自動的に反応します。

手順8. 初回ロードでウェルカムカードをリクエストする

travel-a2ui/
└─ src/app/
   ├─ theme.ts              ← 見た目の定義(公式サンプルから流用)
   ├─ mock-agent.service.ts ← API レスポンスを返す開発用モック(エージェント役)
   ├─ app.config.ts         ← A2UI・HttpClient・モックのセットアップ
   ├─ app.ts                ← API を叩いて描画する本体 ★ いまここ ★
   └─ app.html              ← サーフェスを表示する1行

コンストラクタで、操作なし(null)のリクエストを送信します。
アプリを開いた瞬間にエージェントへ挨拶をしに行きます。

src/app/app.ts
  constructor() {
    this.sendTurn(null);
  }

手順9. サーフェスを表示して起動する

travel-a2ui/
└─ src/app/
   ├─ theme.ts              ← 見た目の定義(公式サンプルから流用)
   ├─ mock-agent.service.ts ← API レスポンスを返す開発用モック(エージェント役)
   ├─ app.config.ts         ← A2UI・HttpClient・モックのセットアップ
   ├─ app.ts                ← API を叩いて描画する本体
   └─ app.html              ← サーフェスを表示する1行 ★ いまここ ★

テンプレート src/app/app.html のコードは、たった1行だけで済みます。

この surfaceId="main" が、先ほどの手順 5でエージェントの返す surfaceId と一致している点に注目しましょう。

src/app/app.html
<a2ui-surface surfaceId="main" />

これで、A2UI レンダラーの MessageProcessor が、自動的に main サーフェスを探して UI を更新します。

それでは動作確認をしましょう。 以下のコマンドで開発サーバーを起動します。

$ ng serve

ブラウザで http://localhost:4200 を開くと、モックから届いたウェルカムカードが表示されるはずです。

手順10. ボタンのアクションを受け取る

では、続きを作っていきましょう。

travel-a2ui/
└─ src/app/
   ├─ theme.ts              ← 見た目の定義(公式サンプルから流用)
   ├─ mock-agent.service.ts ← API レスポンスを返す開発用モック(エージェント役)
   ├─ app.config.ts         ← A2UI・HttpClient・モックのセットアップ
   ├─ app.ts                ← API を叩いて描画する本体 ★ いまここ ★
   └─ app.html              ← サーフェスを表示する1行

ウェルカムカード上の Button が押されると、A2UI レンダラーは MessageProcessorevents というストリームに「ユーザーがこれを操作した」というイベントを流します。App のコンストラクタでこれを購読subscribeします。

src/app/app.ts
  constructor() {
    this.sendTurn(null);
    this.processor.events.subscribe(({ message }) => {
      const action = message.userAction;
      if (action) this.sendTurn({ name: action.name });
    });
  }

message.userAction には、押されたボタンの name(今回は 手順 5で設定した 'recommend')が入っています。それをそのまま sendTurn の引数に渡しています。

手順11. おすすめ一覧のメッセージを返却する

最後に mock-agent.service.ts へ戻り、recommend に対する応答を作ります。

travel-a2ui/
└─ src/app/
   ├─ theme.ts              ← 見た目の定義(公式サンプルから流用)
   ├─ mock-agent.service.ts ← API レスポンスを返す開発用モック(エージェント役) ★ いまここ ★
   ├─ app.config.ts         ← A2UI・HttpClient・モックのセットアップ
   ├─ app.ts                ← API を叩いて描画する本体
   └─ app.html              ← サーフェスを表示する1行

おすすめ一覧の UI 構造

以降の手順で作成する UI の構造をはじめに確認しておきましょう。

先ほどより少し複雑ですが、情報のまとまりごとにグループ化を行っていることが解ればひとまず先へ進んで問題ありません。

image.png

列と見出し

先ほど図で確認したとおり、Columnの中にカードを複数並べる構成を作ります。
まず親の列と見出しを定義します。

src/app/mock-agent.service.ts
function recommendMessages(): ServerToClientMessage[] {
  const components: ComponentInstance[] = [
    { id: 'root', component: { Column: { children: { explicitList: ['head', 'c1', 'c2'] } } } },
    { id: 'head', component: { Text: { text: { literalString: '今おすすめの行き先' }, usageHint: 'h2' } } },
  ];
  // 続く

1つめのカード

1件目のカードを、画像とテキストの組で作ります。Imageurl も文字列値なので、Text と同じく { literalString: '...' } で渡します。

src/app/mock-agent.service.ts
  components.push(
    { id: 'c1', component: { Card: { child: 'c1body' } } },
    { id: 'c1body', component: { Column: { children: { explicitList: ['c1img', 'c1txt'] } } } },
    { id: 'c1img', component: { Image: { url: { literalString: 'https://picsum.photos/id/1036/600/300' } } } },
    { id: 'c1txt', component: { Text: { text: { literalString: '北海道:澄んだ空気と大自然' } } } },
  );

2つめのカード

2件目も同じ要領で追加します(先ほどの1件目の内容から値を変えただけです)。

src/app/mock-agent.service.ts
  components.push(
    { id: 'c2', component: { Card: { child: 'c2body' } } },
    { id: 'c2body', component: { Column: { children: { explicitList: ['c2img', 'c2txt'] } } } },
    { id: 'c2img', component: { Image: { url: { literalString: 'https://picsum.photos/id/1018/600/300' } } } },
    { id: 'c2txt', component: { Text: { text: { literalString: '沖縄:どこまでも続く青い海' } } } },
  );

戻り値の組み立て

最後に、同じ main サーフェスを新しい起点('root' = おすすめ一覧の列)で描き直すメッセージを返します。

src/app/mock-agent.service.ts
  return [
    { surfaceUpdate: { surfaceId: 'main', components } },
    { beginRendering: { surfaceId: 'main', root: 'root' } },
  ];
}

仕上げに、手順 4 でウェルカムカードだけを返していた reply() 関数を修正します。

recommendMessages() 関数がようやく完成したので、action.name === 'recommend' のときはおすすめ一覧を返す分岐を追加しています。

src/app/mock-agent.service.ts
function reply(action: { name: string } | null): ServerToClientMessage[] {
  if (action?.name === 'recommend') return recommendMessages();
  return welcomeMessages();
}

以上で完成です。

再度 ng serve でアプリケーションを起動し、挙動を確認してみましょう。
ウェルカムカードの「おすすめを見る」を押すと、モックがおすすめ一覧を返し、画面が切り替わります。

ここまでの流れを図にまとめると、以下のようになります。

まとめ

本記事では、A2UI を使って 「エージェントが返す JSON を、安全に UI へと錬成する」 流れを、Angular 製の小さな旅行アシスタントとして組み立ててみました。

あらためて振り返ると、重点ポイントは次の3点です。

  • エージェントは、クライアントがカタログとして許可した部品しか並べられない(安全
  • UI を入れ子ではなくフラットな JSON で表現するので、LLM が生成しやすい
  • 同じ JSON を、Angular でも Flutter でも描画できる(フレームワーク非依存

フロントエンド側がやったことは、テンプレートに <a2ui-surface> を1行置き、届いたメッセージをレンダラーに流すことでした。

エージェントの応答が変わるたびに、画面はそれに合わせて描き変わります。手元で動かしてみると、JSON から UI が錬成される感覚が思いのほか心地よいはずです。

補遺: バックエンドの責務

先ほど、しれっと「エージェントが A2UI 形式の JSON を返却する」と書きましたが、これは、勝手にそうなるわけではありません。

エージェント側(たとえば Strands Agents や Bedrock AgentCore で組んだバックエンド)に対して、「あなたの出力は、必ず A2UI のカタログに沿った JSON にしなさい」という制御を、開発者が作り込む必要があります8

つまり、エージェントが UI を喋れる6ようにするのは、バックエンド側の仕事なのです。

この記事の主題はフロントエンド(Angular での描画)なので、エージェント側のこの作り込みは扱いませんでした。

いずれ機会があれば、この辺についてもご紹介したいと思います。

  1. 個人の感想であり、所属組織の見解ではありません。

  2. 厳密には、AG-UI と A2UI は競合するというより補完関係にあります。AG-UI が「エージェントとフロントエンドの通信のしかた」を定める規格であるのに対し、A2UI は「UI をどう表現するか」を定めるデータ形式です。実際、AG-UI の通信路に A2UI の JSON を乗せて併用することもできます。本記事ではシンプルさを優先し、A2UI 単体に絞って扱います。

  3. AG-UI、A2UIともに、『Amazon Bedrock AgentCore 実践入門』 §2.4.3 「その他の標準規格」および §2.4.4「標準規格の全体像」に言及があります。

  4. https://developers.googleblog.com/introducing-a2ui-an-open-project-for-agent-driven-interfaces/

  5. きちんと、A2UI の規約に沿った応答を返すようにエージェントを実装することが前提です。 妥当な JSON を返却するのはエージェント側の責務です。

  6. 喋る UI」は、いかにもAIくさい言い回しですが、これは公式ドキュメントにある「A2UI is an open standard and set of libraries that allows agents to "speak UI."」という表現の直訳です。 2

  7. これを忘れると Text の描画時に markdownRenderer.render is not a function というエラーになります。

  8. バックエンド側の実装については『Amazon Bedrock AgentCore 実践入門』 §3.5.4 「構造化出力 (Structured Output)」が参考になります。

0
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
0
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?