Domänenspezifische Widgets als gleichwertige Bausteine für das Sprachmodell.
Sobald fachliche Konzepte ins Spiel kommen, reicht der Basic Catalog von A2UI selten aus: Eine Flugbuchung, eine Boardkarte oder ein Bonusprogramm wirken als generische Karten und Listen schnell beliebig. Mit Custom Catalogs sieht A2UI deshalb einen klaren Mechanismus vor, um dem Sprachmodell eigene, fachlich passende Komponenten und Funktionen zur Verfügung zu stellen – ohne den schlanken, deklarativen Charakter des Protokolls aufzugeben.
Dieser dritte und abschließende Teil der Serie zeigt, wie sich solche Custom Catalogs in Angular definieren, beim Renderer registrieren und in die im zweiten Teil vorgestellte AG-UI-Abstraktion integrieren lassen.
📂 Source Code (siehe Branch a2ui-dynamic)
Was sind Custom Catalogs in A2UI?
Ein Custom Catalog erweitert A2UI um eigene, fachlich getriebene Komponenten und Funktionen, die das Sprachmodell wie jede andere Komponente referenzieren darf. Häufig stellen Custom Catalogs eine Übermenge des Basic Catalog dar, sodass sie neben den eigenen auch die allgemein bekannten UI-Bausteine enthalten. Aus Sicht des Renderers bleibt die Verarbeitung gleich; das LLM erhält lediglich einen größeren Werkzeugkasten.
Im weiteren Verlauf ergänzen wir die im ersten Teil vorgestellte Passagier-Karte um eine eigene MilesProgress-Komponente, die den Fortschritt zur nächsten Bonusstufe visualisiert:

Eigene Komponenten für den Custom Catalog erstellen
Eine A2UI-Komponente in Angular ist im Kern eine ganz normale Angular-Komponente. Sie erhält jedoch über ein definiertes Context-Objekt die Inputs, die der Agent über A2UI übermittelt. Das folgende Listing zeigt die Definition eines solchen Kontextes für unsere MilesProgress-Komponente. Die Eigenschaft passenger ist dabei als BoundProperty deklariert: Sie kann sowohl konkrete Daten aufnehmen als auch eine Bindung an das Datenmodell repräsentieren:
export interface MilesProgressContext {
passenger: BoundProperty<Passenger>;
}
Die zugehörige Komponente empfängt diesen Kontext über das InputSignal props:
@Component({
selector: 'app-miles-progress',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [DecimalPipe],
template: `
<section class="miles-progress">
<h3>Miles Progress</h3>
<p class="current">{{ passenger().bonusMiles | number }}</p>
<p class="remaining">
{{ remainingMiles() | number }} miles to {{ nextThreshold() | number }}
</p>
<div aria-hidden="true" class="track">
<div class="fill" [style.width.%]="progressPercent()"></div>
</div>
</section>
`,
styleUrl: './miles-progress.css',
})
export class MilesProgress {
readonly props = input<MilesProgressContext>(initialContext);
readonly surfaceId = input.required<string>();
readonly componentId = input.required<string>();
readonly dataContextPath = input('/');
protected readonly passenger = computed(() => this.props().passenger.value());
protected readonly nextThreshold = computed(() =>
calcNextThreshold(this.passenger().bonusMiles),
);
protected readonly remainingMiles = computed(() =>
calcRemainingMiles(this.nextThreshold(), this.passenger().bonusMiles),
);
protected readonly progressPercent = computed(() =>
calcProgressPercent(this.nextThreshold(), this.passenger().bonusMiles),
);
}
Die gezeigte MilesProgress-Komponente liest die aktuellen Bonusmeilen aus dem Kontext und berechnet mithilfe eines computed Signals die Anzahl der Meilen, die noch bis zur Erreichung der nächsten Bonusstufe fehlen. Außerdem zeigt sie eine Fortschrittsanzeige für dieses Ziel an.
Damit der Renderer die MilesProgress-Komponente kennt und korrekt instanziieren kann, ist außerdem eine Beschreibung ihrer Inputs in Form eines Schemas erforderlich. Der Renderer vom A2UI-Team nutzt hierfür die populäre Bibliothek Zod:
import {
AngularComponentImplementation,
} from '@a2ui/angular/v0_9';
import { z } from 'zod/v3';
[...]
const passengerSchema = z
.object({
id: z.number(),
firstName: z.string(),
lastName: z.string(),
bonusMiles: z.number(),
})
.strict();
const pathBindingSchema = z.object({ path: z.string() }).strict();
const passengerOrPathSchema = z.union([passengerSchema, pathBindingSchema]);
const milesProgressSchema = z
.object({
passenger: passengerOrPathSchema.optional(),
})
.strict();
const milesProgressEntry = {
name: 'MilesProgress',
component: MilesProgress,
schema: milesProgressSchema,
} as unknown as AngularComponentImplementation;
// ^^^ unknown prevents typing issue in current version
Das Schema legt fest, dass die Eigenschaft passenger entweder als Objekt (passengerSchema) oder als Datenbindung (pathBindingSchema) mit der Eigenschaft path übergeben werden darf. Die Konstante milesProgressEntry fasst den Namen und die Implementierung der Komponente sowie das Schema zu einer Einheit zusammen, die der Katalog später aufnehmen kann.
Neu: Agentic UI with Angular
Wenn du A2UI nicht nur integrieren, sondern sauber in größere Architekturen einbetten willst:
In meinem Buch Agentic UI mit Angular gehe ich genau auf diese Patterns und Trade-offs im Detail ein.
Eigene Funktionen für den Custom Catalog
Neben Komponenten kann ein Custom Catalog auch eigene Funktionen bereitstellen. Diese ergänzen die im Basic Catalog enthaltenen Standardfunktionen wie formatNumber und formatDate. Das folgende Listing zeigt eine kleine Hilfsfunktion formatId, die eine numerische ID in eine sprechende Zeichenkette wie P-0042 umwandelt. Um die erwarteten Argumente zu beschreiben, kommt erneut Zod zum Einsatz:
import type { FunctionImplementation } from '@a2ui/web_core/v0_9';
import { z, type ZodTypeAny } from 'zod/v3';
const formatIdSchema = z
.object({
value: z.number(),
})
.strict();
export const formatIdImplementation = {
name: 'formatId',
returnType: 'string',
schema: formatIdSchema as unknown as ZodTypeAny,
execute: (args: Record<string, unknown>) => {
const { value } = formatIdSchema.parse(args);
const normalizedValue = Math.max(0, Math.trunc(value));
return `P-${String(normalizedValue).padStart(4, '0')}`;
},
} as unknown as FunctionImplementation;
// ^^^ unknown prevents typing issue in current version
Mit Komponenten und Funktionen sind die beiden zentralen Bausteine eines Custom Catalogs definiert. Im nächsten Schritt müssen wir den Katalog für den Renderer verfügbar machen.
Custom Catalog beim A2UI-Renderer registrieren
Ein Custom Catalog erbt von BasicCatalogBase und übergibt im Konstruktor eine eindeutige ID, die Liste der zusätzlichen Komponenten sowie eine Liste mit Funktionen:
@Injectable({ providedIn: 'root' })
export class CustomCatalog extends BasicCatalogBase {
constructor() {
super({
id: 'https://example.com/catalogs/flights42-a2ui-demo',
extraComponents: [milesProgress],
functions: [...BASIC_FUNCTIONS, formatIdImplementation],
});
}
}
Analog zum BasicCatalog des Angular-Adapters stellen wir auch unseren CustomCatalog als Angular-Service mit providedIn: 'root' bereit. Damit der Renderer den neuen Katalog nutzt, muss er nur noch in der Konfiguration referenziert werden:
import {
A2UI_RENDERER_CONFIG,
A2uiRendererService,
provideMarkdownRenderer,
} from '@a2ui/angular/v0_9';
import {
ApplicationConfig,
inject,
provideBrowserGlobalErrorListeners,
} from '@angular/core';
import { marked } from 'marked';
import { CustomCatalog } from './custom-catalog/custom-catalog';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
{
provide: A2UI_RENDERER_CONFIG,
useFactory: () => ({
catalogs: [inject(CustomCatalog)],
}),
},
provideMarkdownRenderer(async (markdown) =>
marked.parse(String(markdown ?? '')),
),
A2uiRendererService,
],
};
Sobald der Katalog registriert ist, kann die Beispielanwendung die MilesProgress-Komponente wie jede andere Komponente in einer A2UI-Nachricht verwenden. Das folgende Listing zeigt einen Ausschnitt einer updateComponents-Nachricht, in der die MilesProgress-Komponente neben der bestehenden Passagier-Karte angezeigt wird:
updateComponents: {
surfaceId,
components: [
{
id: 'root',
component: 'Column',
children: ['passenger-card', 'miles-progress'],
},
[...]
{
id: 'miles-progress',
component: 'MilesProgress',
passenger: { path: '/passenger' },
},
],
}
Custom Components in der AG-UI-Abstraktion einbinden
Bisher haben wir Custom Components für den eigenständigen A2UI-Renderer registriert. In Kombination mit der im zweiten Teil dieser Serie vorgestellten AG-UI-Abstraktion sieht das Vorgehen ähnlich, aber etwas komfortabler aus: Die Abstraktion bietet eigene Hilfsfunktionen, die sowohl die Komponenten-Beschreibung als auch deren Bereitstellung kapseln.
Damit der A2UI-Renderer im Sidecar nicht nur die Komponenten des Basic Catalogs, sondern auch eigene Widgets anzeigen kann, lassen sich Custom Components ergänzen. Im hier verwendeten Beispielprojekt findet sich ein TicketWidget, das eine Boardkarte darstellt:

Die dabei genutzte Hilfsfunktion createCustomComponent nimmt den Namen, die Beschreibung, die Komponenten-Implementierung sowie ein Zod-Schema entgegen, das die Eigenschaften der Komponente beschreibt:
import {
A2uiCustomCatalogComponent,
binding,
createCustomComponent,
} from '@internal/ag-ui-client';
import { z } from 'zod';
import { TicketWidget } from './ticket/ticket-widget';
export const ticketWidgetEntry = createCustomComponent({
name: 'TicketWidget',
description: 'A boarding-pass-style widget ...',
component: TicketWidget,
schema: z
.object({
ticketId: binding(z.union([z.string(), z.number()])),
from: binding(z.string()),
to: binding(z.string()),
date: binding(z.string()),
delay: binding(z.number()).optional(),
})
.strict(),
});
export const ticketingExtraComponents: A2uiCustomCatalogComponent[] = [
ticketWidgetEntry,
];
Das Schema binding repräsentiert jene Felder, die das LLM entweder direkt setzen oder über ein path-Binding an Werte aus dem Datenmodell knüpfen darf.
Damit ein Custom Catalog, der den Basic Catalog um diese Komponente erweitert, zum Einsatz kommt, packen wir die Beschreibung unseres TicketWidgets in ein Array und übergeben es an provideA2uiCatalog:
import { provideMarkdownRenderer } from '@a2ui/angular/v0_9';
import { provideA2uiCatalog } from '@internal/ag-ui-client';
import { marked } from 'marked';
import { ticketingExtraComponents } from './domains/ticketing/ai/custom-catalog/ticketing-extra-components';
[...]
export const appConfig: ApplicationConfig = {
providers: [
[...]
provideA2uiCatalog({
id: 'https://angulararchitects.io/custom_catalog.json',
components: ticketingExtraComponents,
sendCatalogDescription: true,
}),
provideMarkdownRenderer(async (markdown) =>
marked.parse(String(markdown ?? '')),
),
],
};
Für Custom Functions sieht provideA2uiCatalog eine hier nicht genutzte Eigenschaft functions vor. Damit die angeführte Id eindeutig ist, kommt in der Regel eine URL aus dem eigenen Hoheitsgebiet zum Einsatz.
Sicherheitsaspekt: sendCatalogDescription und Prompt-Injection
Der Flag sendCatalogDescription sorgt dafür, dass die textuellen Beschreibungen der Komponenten an den Agent übermittelt werden, sodass dieser den Komponentenkatalog im Prompt für das LLM berücksichtigen kann. Die im zweiten Teil vorgestellte Abstraktion im Ordner libs leitet aus dem Schema einfache Beispiele ab und hängt sie zusammen mit den Beschreibungen an den Systemprompt an.
Diese Vorgehensweise ist für die Entwicklung sehr komfortabel, kann jedoch in der Produktion zur Prompt-Injection missbraucht werden. Dabei schleust ein Angreifer schädliche Anweisungen in den Systemprompt ein und verleitet so das Sprachmodell zu ungewünschten Aktionen.
Deswegen bietet es sich im Produktionsbetrieb an, den Flag sendCatalogDescription auf false zu setzen. Das hat zur Folge, dass der Client lediglich die Katalog-Id überträgt. Diese validiert der Agent anhand einer Liste freigegebener Kataloge und ermittelt das dazugehörige Schema über eine vertrauenswürdige Registry – zum Beispiel über eine systeminterne API oder Datenbank.
Zusammenfassung
Custom Catalogs erweitern A2UI gezielt um eigene Komponenten und Funktionen, die das Sprachmodell wie jeden anderen Baustein nutzen darf. Damit lassen sich generische Antworten in fachlich passende Oberflächen übersetzen, ohne den Charakter eines schlanken, deklarativen Protokolls zu verlieren. Schema-Validierung über Zod sorgt dabei für saubere Verträge zwischen Agent und Client, während die Aufteilung in Komponenten und Funktionen den Katalog flexibel hält.
In Kombination mit der AG-UI-Abstraktion lassen sich Custom Components mit createCustomComponent kompakt beschreiben und über einen einzigen Provider-Aufruf einbinden. Wer den Schritt in den Produktionsbetrieb plant, sollte dabei den Sicherheitsaspekt rund um sendCatalogDescription mitdenken und Katalog-Schemata aus einer vertrauenswürdigen Quelle beziehen.
Wer Protokoll, Renderer, AG-UI-Anbindung und Custom Catalogs kombiniert, hat eine solide Grundlage, um Sprachmodelle nicht nur Text, sondern echte UI-Antworten liefern zu lassen – und das auf eine Weise, die zur eigenen Anwendung passt.
FAQ
Was ist ein Custom Catalog in A2UI?
Ein Custom Catalog erweitert A2UI um eigene, fachlich getriebene Komponenten und Funktionen. Er bringt häufig den Basic Catalog als Übermenge mit und stellt dem Sprachmodell zusätzlich domänenspezifische Bausteine zur Verfügung, die der Renderer wie jede andere A2UI-Komponente verarbeitet.
Wie beschreibt man eine eigene A2UI-Komponente?
Die Implementierung ist eine reguläre Angular-Komponente, die ihre Inputs über ein Context-Objekt erhält. Zusätzlich zum Bauteil selbst wird ein Zod-Schema definiert, das die erwarteten Eigenschaften beschreibt. Komponente, Name und Schema werden gemeinsam als Eintrag in den Custom Catalog aufgenommen.
Wie registriert man einen Custom Catalog in Angular?
Der Custom Catalog wird als Service mit providedIn: 'root' bereitgestellt und im Token A2UI_RENDERER_CONFIG referenziert. In der AG-UI-Abstraktion übernimmt dies die Funktion provideA2uiCatalog, an die sich Komponenten und optionale Funktionen direkt übergeben lassen.
Wofür ist sendCatalogDescription da und wann sollte man den Flag deaktivieren?
sendCatalogDescription überträgt die textuellen Komponenten-Beschreibungen an den Agent, der sie in den Systemprompt einbettet. Das ist sehr komfortabel, kann aber zu Prompt-Injection führen. Im Produktionsbetrieb empfiehlt es sich daher, den Flag auf false zu setzen und Schemata serverseitig aus einer vertrauenswürdigen Registry zu laden.
