AI SDK Angular
Angular UI components for the AI SDK v5.
Overview
The @ai-sdk/angular
package provides Angular-specific implementations using Angular signals for reactive state management:
- Chat - Multi-turn conversations with streaming responses
- Completion - Single-turn text generation
- StructuredObject - Type-safe object generation with Zod schemas
Installation
npm install @ai-sdk/angular ai
Peer Dependencies
- Angular 16+ (
@angular/core
)
- Zod v3+ (optional, for structured objects)
Chat
Real-time conversation interface with streaming support.
Basic Usage
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Chat } from '@ai-sdk/angular';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-chat',
imports: [CommonModule, ReactiveFormsModule],
template: `
<div class="chat-container">
<div class="messages">
@for (message of chat.messages; track message.id) {
<div class="message" [ngClass]="message.role">
@for (part of message.parts; track $index) {
@switch (part.type) {
@case ('text') {
<div style="white-space: pre-wrap">
{{ part.text }}
@if (part.state === 'streaming') {
<span class="cursor">▮</span>
}
</div>
}
@case ('reasoning') {
<details>
<summary>Reasoning</summary>
<div style="white-space: pre-wrap; opacity: 80%">
{{ part.text }}
</div>
</details>
}
@default {
<code>{{ part | json }}</code>
}
}
}
</div>
}
@if (chat.status === 'submitted') {
<div><em>Waiting...</em></div>
}
</div>
<form [formGroup]="chatForm" (ngSubmit)="sendMessage()">
<input formControlName="userInput" placeholder="Type your message..." />
@if (chat.status === 'ready') {
<button type="submit" [disabled]="!chatForm.valid">Send</button>
} @else {
<button [disabled]="chat.status === 'error'" (click)="chat.stop()">
Stop
</button>
}
</form>
</div>
`,
})
export class ChatComponent {
private fb = inject(FormBuilder);
public chat = new Chat({});
chatForm = this.fb.group({
userInput: ['', Validators.required],
});
sendMessage() {
if (this.chatForm.invalid) return;
const userInput = this.chatForm.value.userInput;
this.chatForm.reset();
this.chat.sendMessage(
{ text: userInput },
{
body: {
selectedModel: 'gpt-4o',
},
},
);
}
}
Constructor Options
interface ChatInit<UI_MESSAGE extends UIMessage = UIMessage> {
messages?: UI_MESSAGE[];
generateId?: () => string;
maxSteps?: number;
onToolCall?: (params: { toolCall: ToolCall }) => Promise<string>;
onFinish?: (params: { message: UI_MESSAGE }) => void;
onError?: (error: Error) => void;
transport?: ChatTransport;
}
Properties (Reactive)
messages: UIMessage[]
- Array of conversation messages
status: 'ready' | 'submitted' | 'streaming' | 'error'
- Current status
error: Error | undefined
- Current error state
Methods
await chat.sendMessage(
message: UIMessageInput,
options?: {
body?: Record<string, any>;
headers?: Record<string, string>;
}
);
await chat.regenerate(options?: {
body?: Record<string, any>;
headers?: Record<string, string>;
});
chat.addToolResult({
toolCallId: string;
output: string;
});
chat.stop();
File Attachments
<input type="file" multiple (change)="onFileSelect($event)" />
onFileSelect(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (files) {
this.chat.sendMessage({
text: "Analyze these files",
files: files
});
}
}
Client-side Tool Calls
const chat = new Chat({
async onToolCall({ toolCall }) {
switch (toolCall.toolName) {
case 'get_weather':
return await getWeather(toolCall.input.location);
case 'search':
return await search(toolCall.input.query);
default:
throw new Error(`Unknown tool: ${toolCall.toolName}`);
}
},
});
Completion
Single-turn text generation with streaming.
Basic Usage
import { Component } from '@angular/core';
import { Completion } from '@ai-sdk/angular';
@Component({
selector: 'app-completion',
template: `
<div>
<textarea
[(ngModel)]="completion.input"
placeholder="Enter your prompt..."
rows="4"
>
</textarea>
<button
(click)="completion.complete(completion.input)"
[disabled]="completion.loading"
>
{{ completion.loading ? 'Generating...' : 'Generate' }}
</button>
@if (completion.loading) {
<button (click)="completion.stop()">Stop</button>
}
<div class="result">
<h3>Result:</h3>
<pre>{{ completion.completion }}</pre>
</div>
@if (completion.error) {
<div class="error">{{ completion.error.message }}</div>
}
</div>
`,
})
export class CompletionComponent {
completion = new Completion({
api: '/api/completion',
onFinish: (prompt, completion) => {
console.log('Completed:', { prompt, completion });
},
});
}
Constructor Options
interface CompletionOptions {
api?: string;
id?: string;
initialCompletion?: string;
initialInput?: string;
streamProtocol?: 'data' | 'text';
onFinish?: (prompt: string, completion: string) => void;
onError?: (error: Error) => void;
fetch?: FetchFunction;
headers?: Record<string, string>;
body?: Record<string, any>;
credentials?: RequestCredentials;
}
Properties (Reactive)
completion: string
- Generated text (writable)
input: string
- Current input (writable)
loading: boolean
- Generation state
error: Error | undefined
- Error state
id: string
- Completion ID
api: string
- API endpoint
streamProtocol: 'data' | 'text'
- Stream type
Methods
await completion.complete(
prompt: string,
options?: {
headers?: Record<string, string>;
body?: Record<string, any>;
}
);
await completion.handleSubmit(event?: { preventDefault?: () => void });
completion.stop();
StructuredObject
Generate structured data with Zod schemas and streaming.
Basic Usage
import { Component } from '@angular/core';
import { StructuredObject } from '@ai-sdk/angular';
import { z } from 'zod';
const schema = z.object({
title: z.string(),
summary: z.string(),
tags: z.array(z.string()),
sentiment: z.enum(['positive', 'negative', 'neutral']),
});
@Component({
selector: 'app-structured-object',
template: `
<div>
<textarea
[(ngModel)]="input"
placeholder="Enter content to analyze..."
rows="4"
>
</textarea>
<button (click)="analyze()" [disabled]="structuredObject.loading">
{{ structuredObject.loading ? 'Analyzing...' : 'Analyze' }}
</button>
@if (structuredObject.object) {
<div class="result">
<h3>Analysis:</h3>
<div><strong>Title:</strong> {{ structuredObject.object.title }}</div>
<div>
<strong>Summary:</strong> {{ structuredObject.object.summary }}
</div>
<div>
<strong>Tags:</strong>
{{ structuredObject.object.tags?.join(', ') }}
</div>
<div>
<strong>Sentiment:</strong> {{ structuredObject.object.sentiment }}
</div>
</div>
}
@if (structuredObject.error) {
<div class="error">{{ structuredObject.error.message }}</div>
}
</div>
`,
})
export class StructuredObjectComponent {
input = '';
structuredObject = new StructuredObject({
api: '/api/analyze',
schema,
onFinish: ({ object, error }) => {
if (error) {
console.error('Schema validation failed:', error);
} else {
console.log('Generated object:', object);
}
},
});
async analyze() {
if (!this.input.trim()) return;
await this.structuredObject.submit(this.input);
}
}
Constructor Options
interface StructuredObjectOptions<SCHEMA, RESULT> {
api: string;
schema: SCHEMA;
id?: string;
initialValue?: DeepPartial<RESULT>;
onFinish?: (event: {
object: RESULT | undefined;
error: Error | undefined;
}) => void;
onError?: (error: Error) => void;
fetch?: FetchFunction;
headers?: Record<string, string>;
credentials?: RequestCredentials;
}
Properties (Reactive)
object: DeepPartial<RESULT> | undefined
- Generated object
loading: boolean
- Generation state
error: Error | undefined
- Error state
Methods
await structuredObject.submit(input: unknown);
structuredObject.stop();
Server Implementation
Express.js Chat Endpoint
import { openai } from '@ai-sdk/openai';
import { convertToModelMessages, streamText } from 'ai';
import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/chat', async (req, res) => {
const { messages, selectedModel } = req.body;
const result = streamText({
model: openai(selectedModel || 'gpt-4o'),
messages: convertToModelMessages(messages),
});
result.pipeUIMessageStreamToResponse(res);
});
Express.js Completion Endpoint
app.post('/api/completion', async (req, res) => {
const { prompt } = req.body;
const result = streamText({
model: openai('gpt-4o'),
prompt,
});
result.pipeTextStreamToResponse(res);
});
Express.js Structured Object Endpoint
import { streamObject } from 'ai';
import { z } from 'zod';
app.post('/api/analyze', async (req, res) => {
const input = req.body;
const result = streamObject({
model: openai('gpt-4o'),
schema: z.object({
title: z.string(),
summary: z.string(),
tags: z.array(z.string()),
sentiment: z.enum(['positive', 'negative', 'neutral']),
}),
prompt: `Analyze this content: ${JSON.stringify(input)}`,
});
result.pipeTextStreamToResponse(res);
});
Development Setup
Building the Library
pnpm install
pnpm build
pnpm build:watch
pnpm test
pnpm test:watch
Running the Example
cd examples/angular-chat
echo "OPENAI_API_KEY=your_key_here" > .env
pnpm start
Starts:
- Angular dev server:
http://localhost:4200
- Express API server:
http://localhost:3000
- Proxy routes
/api/*
to Express
Testing
Running Tests
pnpm test
pnpm test:watch
pnpm test:update
TypeScript Support
Full type safety with automatic type inference:
import { Chat, UIMessage, StructuredObject } from '@ai-sdk/angular';
import { z } from 'zod';
interface CustomMessage extends UIMessage {
customData?: string;
}
const chat = new Chat<CustomMessage>({});
const schema = z.object({
name: z.string(),
age: z.number(),
});
const obj = new StructuredObject({
api: '/api/object',
schema,
});
Error Handling
All components provide reactive error states:
const chat = new Chat({
onError: (error) => {
console.error('Chat error:', error);
}
});
@if (chat.error) {
<div class="error">{{ chat.error.message }}</div>
}
Performance
Stop on-going requests
chat.stop();
completion.stop();
structuredObject.stop();
Change Detection
Uses Angular signals for efficient reactivity:
chat.messages;
chat.status;
chat.error;
License
Apache-2.0