클라이언트 빌드
클라이언트 빌드 (Build clients)
AG-UI와 Mastra를 사용해 처음부터 대화형 CLI 에이전트를 만들어 보는 쇼케이스예요.
출처: 문서
본문
소개 (Introduction)
클라이언트 구현을 통해 AG-UI의 이벤트 기반 프로토콜을 활용하는 대화형 애플리케이션을 빌드할 수 있어요. 이 접근 방식은 사용자와 AI 에이전트 사이에 직접적인 인터페이스를 만들어 AG-UI 프로토콜에 대한 직접 접근을 보여줍니다.
클라이언트가 반드시 웹 애플리케이션일 필요는 없어요. AG-UI는 렌더링 대상이 아닌 이벤트 스트림을 기술하므로, 그 이벤트를 소비해 사용자에게 보여줄 수 있는 모든 것이 클라이언트가 됩니다: 웹 앱, 아래에서 만드는 터미널 클라이언트, 모바일 앱, Slack이나 Microsoft Teams 같은 채팅 플랫폼. Channels SDK가 채팅 플랫폼 구현 중 하나이고, OpenTag가 실제 작업 예시입니다.
언제 클라이언트 구현을 사용할까 (When to use a client implementation)
AG-UI 프로토콜을 탐구하거나 해킹하고 싶다면 직접 클라이언트를 빌드하는 것이 유용해요. 프로덕션용으로는 CopilotKit 같은 풀 기능 클라이언트를 사용하세요.
무엇을 만들까 (What you'll build)
이 가이드에서 우리는 다음을 하는 CLI 클라이언트를 만들게요:
@ag-ui/mastra의MastraAgent사용- OpenAI의 GPT-4o 모델에 연결
- 실제 기능을 위한 날씨 도구(weather tool) 구현
- 터미널에서 대화형 채팅 인터페이스 제공
시작해 볼게요!
사전 요구사항 (Prerequisites)
시작하기 전에 다음이 있는지 확인하세요:
1. OpenAI API 키 제공
먼저 API 키를 설정해 볼게요:
# Set your OpenAI API key
export OPENAI_API_KEY=your-api-key-here
2. pnpm 설치
pnpm이 없다면:
# Install pnpm
npm install -g pnpm
1단계 – 프로젝트 초기화 (Step 1 – Initialize your project)
AG-UI 클라이언트용 새 디렉터리를 만드세요:
mkdir my-ag-ui-client
cd my-ag-ui-client
새 Node.js 프로젝트를 초기화하세요:
pnpm init
TypeScript와 기본 구성 설정
TypeScript와 필수 개발 의존성을 설치하세요:
pnpm add -D typescript @types/node tsx
tsconfig.json 파일을 만드세요:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
package.json 스크립트를 업데이트하세요:
{
"scripts": {
"start": "tsx src/index.ts",
"dev": "tsx --watch src/index.ts",
"build": "tsc",
"clean": "rm -rf dist"
}
}
2단계 – AG-UI와 의존성 설치 (Step 2 – Install AG-UI and dependencies)
핵심 AG-UI 패키지와 의존성을 설치하세요:
# Core AG-UI packages
pnpm add @ag-ui/client @ag-ui/core @ag-ui/mastra
# Mastra ecosystem packages
pnpm add @mastra/core @mastra/client-js @mastra/memory @mastra/libsql
# Mastra peer dependencies
pnpm add zod
3단계 – 에이전트 만들기 (Step 3 – Create your agent)
기본 대화형 에이전트를 만들어 볼게요. src/agent.ts를 만드세요:
import { Agent } from "@mastra/core/agent"
import { MastraAgent } from "@ag-ui/mastra"
import { Memory } from "@mastra/memory"
import { LibSQLStore } from "@mastra/libsql"
export const agent = new MastraAgent({
resourceId: "cliExample",
agent: new Agent({
id: "ag-ui-assistant",
name: "AG-UI Assistant",
instructions: `
You are a helpful AI assistant. Be friendly, conversational, and helpful.
Answer questions to the best of your ability and engage in natural conversation.
`,
model: "openai/gpt-4o",
memory: new Memory({
storage: new LibSQLStore({
id: "storage-memory",
url: "file:./assistant.db",
}),
}),
}),
threadId: "main-conversation",
})
에이전트에서 어떤 일이 일어나나요?
- MastraAgent – Mastra 에이전트를 AG-UI 프로토콜 어댑터로 감쌉니다
- Model Configuration – 고품질 응답을 위해 OpenAI GPT-4o 사용
- Memory Setup – 대화 컨텍스트를 위해 LibSQL을 사용한 영구 메모리 구성
- Instructions – 유용한 대화를 위한 기본 지침을 에이전트에 제공
4단계 – CLI 인터페이스 만들기 (Step 4 – Create the CLI interface)
이제 대화형 채팅 인터페이스를 만들어 볼게요. src/index.ts를 만드세요:
import * as readline from "readline"
import { agent } from "./agent"
import { randomUUID } from "@ag-ui/client"
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
async function chatLoop() {
console.log("🤖 AG-UI Assistant started!")
console.log("Type your messages and press Enter. Press Ctrl+D to quit.\n")
return new Promise<void>((resolve) => {
const promptUser = () => {
rl.question("> ", async (input) => {
if (input.trim() === "") {
promptUser()
return
}
console.log("")
// Pause input while processing
rl.pause()
// Add user message to conversation
agent.messages.push({
id: randomUUID(),
role: "user",
content: input.trim(),
})
try {
// Run the agent with event handlers
await agent.runAgent(
{}, // No additional configuration needed
{
onTextMessageStartEvent() {
process.stdout.write("🤖 Assistant: ")
},
onTextMessageContentEvent({ event }) {
process.stdout.write(event.delta)
},
onTextMessageEndEvent() {
console.log("\n")
},
}
)
} catch (error) {
console.error("❌ Error:", error)
}
// Resume input
rl.resume()
promptUser()
})
}
// Handle Ctrl+D to quit
rl.on("close", () => {
console.log("\n👋 Thanks for using AG-UI Assistant!")
resolve()
})
promptUser()
})
}
async function main() {
await chatLoop()
}
main().catch(console.error)
CLI 인터페이스에서 어떤 일이 일어나나요?
- Readline Interface – 사용자 입력을 위한 대화형 프롬프트 생성
- Message Management – 각 사용자 입력을 에이전트의 대화 기록에 추가
- Event Handling – AG-UI 이벤트를 수신해 실시간 피드백 제공
- Streaming Display – 생성되는 대로 에이전트 응답 표시
5단계 – 어시스턴트 테스트 (Step 5 – Test your assistant)
새 AG-UI 클라이언트를 실행해 봅시다:
pnpm dev
다음이 보여야 해요:
🤖 AG-UI Assistant started!
Type your messages and press Enter. Press Ctrl+D to quit.
>
다음 같은 질문을 시도해 보세요:
- "Hello! How are you?"
- "What can you help me with?"
- "Tell me a joke"
- "Explain quantum computing in simple terms"
에이전트가 실시간으로 텍스트를 스트리밍하며 응답하는 것을 볼 수 있을 거예요!
6단계 – AG-UI 이벤트 흐름 이해 (Step 6 – Understanding the AG-UI event flow)
메시지를 보내면 어떤 일이 일어나는지 분석해 볼게요:
- 사용자 입력 – 질문을 입력하고 Enter를 누릅니다
- 메시지 추가 – 입력이 대화 기록에 추가됩니다
- 에이전트 처리 – 에이전트가 요청을 분석하고 응답을 구성합니다
- 응답 생성 – 에이전트가 응답을 다시 스트리밍합니다
- 스트리밍 출력 – 단어 단위로 응답이 나타나는 것을 봅니다
처리 중인 이벤트 유형:
onTextMessageStartEvent– 에이전트가 응답 시작onTextMessageContentEvent– 응답의 각 청크onTextMessageEndEvent– 응답 완료
7단계 – 도구 기능 추가 (Step 7 – Add tool functionality)
이제 동작하는 채팅 인터페이스가 있으니, 도구를 만들어 실제 기능을 추가해 볼게요. 날씨 도구로 시작하겠습니다.
첫 번째 도구 만들기
에이전트가 사용할 수 있는 날씨 도구를 만들어 볼게요. 디렉터리 구조를 만드세요:
mkdir -p src/tools
src/tools/weather.tool.ts를 만드세요:
import { createTool } from "@mastra/core/tools"
import { z } from "zod"
interface GeocodingResponse {
results: {
latitude: number
longitude: number
name: string
}[]
}
interface WeatherResponse {
current: {
time: string
temperature_2m: number
apparent_temperature: number
relative_humidity_2m: number
wind_speed_10m: number
wind_gusts_10m: number
weather_code: number
}
}
export const weatherTool = createTool({
id: "get-weather",
description: "Get current weather for a location",
inputSchema: z.object({
location: z.string().describe("City name"),
}),
outputSchema: z.object({
temperature: z.number(),
feelsLike: z.number(),
humidity: z.number(),
windSpeed: z.number(),
windGust: z.number(),
conditions: z.string(),
location: z.string(),
}),
execute: async (inputData) => {
return await getWeather(inputData.location)
},
})
const getWeather = async (location: string) => {
const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(
location
)}&count=1`
const geocodingResponse = await fetch(geocodingUrl)
const geocodingData = (await geocodingResponse.json()) as GeocodingResponse
if (!geocodingData.results?.[0]) {
throw new Error(`Location '${location}' not found`)
}
const { latitude, longitude, name } = geocodingData.results[0]
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`
const response = await fetch(weatherUrl)
const data = (await response.json()) as WeatherResponse
return {
temperature: data.current.temperature_2m,
feelsLike: data.current.apparent_temperature,
humidity: data.current.relative_humidity_2m,
windSpeed: data.current.wind_speed_10m,
windGust: data.current.wind_gusts_10m,
conditions: getWeatherCondition(data.current.weather_code),
location: name,
}
}
function getWeatherCondition(code: number): string {
const conditions: Record<number, string> = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Foggy",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
56: "Light freezing drizzle",
57: "Dense freezing drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
66: "Light freezing rain",
67: "Heavy freezing rain",
71: "Slight snow fall",
73: "Moderate snow fall",
75: "Heavy snow fall",
77: "Snow grains",
80: "Slight rain showers",
81: "Moderate rain showers",
82: "Violent rain showers",
85: "Slight snow showers",
86: "Heavy snow showers",
95: "Thunderstorm",
96: "Thunderstorm with slight hail",
99: "Thunderstorm with heavy hail",
}
return conditions[code] || "Unknown"
}
날씨 도구에서 어떤 일이 일어나나요?
- Tool Definition – 도구 인터페이스를 정의하기 위해 Mastra의
createTool사용 - Input Schema – 도구가 위치 문자열을 받는다고 명시
- Output Schema – 반환되는 날씨 데이터의 구조 정의
- API Integration – Open-Meteo의 무료 날씨 API에서 데이터 가져오기
- Data Processing – 날씨 코드를 사람이 읽을 수 있는 상태로 변환
에이전트 업데이트
이제 에이전트가 날씨 도구를 사용하도록 업데이트할게요. src/agent.ts를 업데이트하세요:
import { weatherTool } from "./tools/weather.tool" // <--- Import the tool
export const agent = new MastraAgent({
agent: new Agent({
// ...
tools: { weatherTool }, // <--- Add the tool to the agent
// ...
}),
threadId: "main-conversation",
})
도구를 처리하도록 CLI 업데이트
src/index.ts의 CLI 인터페이스를 도구 이벤트를 처리하도록 업데이트하세요:
// Add these new event handlers to your agent.runAgent call:
await agent.runAgent(
{}, // No additional configuration needed
{
// ... existing event handlers ...
onToolCallStartEvent({ event }) {
console.log("🔧 Tool call:", event.toolCallName)
},
onToolCallArgsEvent({ event }) {
process.stdout.write(event.delta)
},
onToolCallEndEvent() {
console.log("")
},
onToolCallResultEvent({ event }) {
if (event.content) {
console.log("🔍 Tool call result:", event.content)
}
},
}
)
날씨 도구 테스트
이제 애플리케이션을 재시작하고 날씨에 대해 물어보세요:
pnpm dev
다음 같은 질문을 시도해 보세요:
- "What's the weather like in London?"
- "How's the weather in Tokyo today?"
- "Is it raining in Seattle?"
에이전트가 날씨 도구를 사용해 실제 데이터를 가져오고 자세한 응답을 제공하는 것을 볼 수 있을 거예요!
8단계 – 더 많은 기능 추가 (Step 8 – Add more functionality)
브라우저 도구 만들기
웹 브라우징 기능을 추가해 볼게요. 먼저 open 패키지를 설치하세요:
pnpm add open
src/tools/browser.tool.ts를 만드세요:
import { createTool } from "@mastra/core/tools"
import { z } from "zod"
import { open } from "open"
export const browserTool = createTool({
id: "open-browser",
description: "Open a URL in the default web browser",
inputSchema: z.object({
url: z.url().describe("The URL to open"),
}),
outputSchema: z.object({
success: z.boolean(),
message: z.string(),
}),
execute: async (inputData) => {
try {
await open(inputData.url)
return {
success: true,
message: `Opened ${inputData.url} in your default browser`,
}
} catch (error) {
return {
success: false,
message: `Failed to open browser: ${error}`,
}
}
},
})
두 도구 모두로 에이전트 업데이트
두 도구를 모두 포함하도록 src/agent.ts를 업데이트하세요:
import { Agent } from "@mastra/core/agent"
import { MastraAgent } from "@ag-ui/mastra"
import { Memory } from "@mastra/memory"
import { LibSQLStore } from "@mastra/libsql"
import { weatherTool } from "./tools/weather.tool"
import { browserTool } from "./tools/browser.tool"
export const agent = new MastraAgent({
resourceId: "cliExample",
agent: new Agent({
id: "ag-ui-assistant",
name: "AG-UI Assistant",
instructions: `
You are a helpful assistant with weather and web browsing capabilities.
For weather queries:
- Always ask for a location if none is provided
- Use the weatherTool to fetch current weather data
For web browsing:
- Always use full URLs (e.g., "https://www.google.com")
- Use the browserTool to open web pages
Be friendly and helpful in all interactions!
`,
model: "openai/gpt-4o",
tools: { weatherTool, browserTool }, // Add both tools
memory: new Memory({
storage: new LibSQLStore({
id: "storage-memory",
url: "file:./assistant.db",
}),
}),
}),
threadId: "main-conversation",
})
이제 "Open Google for me" 또는 "Show me the weather website"처럼 어시스턴트에게 웹사이트를 열어 달라고 요청할 수 있어요.
9단계 – 클라이언트 배포 (Step 9 – Deploy your client)
클라이언트 빌드
프로덕션 빌드를 만드세요:
pnpm build
시작 스크립트 만들기
package.json에 추가하세요:
{
"bin": {
"weather-assistant": "./dist/index.js"
}
}
빌드된 dist/index.js에 shebang을 추가하세요:
#!/usr/bin/env node
// ... rest of your compiled code
실행 가능하게 만드세요:
chmod +x dist/index.js
전역 링크
CLI를 전역으로 설치하세요:
pnpm link --global
이제 어디서나 weather-assistant를 실행할 수 있어요!
클라이언트 확장 (Extending your client)
이제 AG-UI 클라이언트가 견고한 기반이 되었어요. 개선을 위한 몇 가지 아이디어:
더 많은 도구 추가
- 계산기 도구 – 수학 연산용
- 파일 시스템 도구 – 파일 읽기/쓰기용
- API 도구 – 다른 서비스 연결용
- 데이터베이스 도구 – 데이터 조회용
인터페이스 개선
- 풍부한 서식 –
chalk같은 라이브러리로 컬러 출력 - 진행 표시기 – 장기 연산에 대한 로딩 상태 표시
- 구성 파일 – 사용자가 설정을 커스터마이즈할 수 있게
- 명령줄 인자 – 다른 모드와 옵션 지원
영속성 추가
- 대화 기록 – 채팅 세션 저장·복원
- 사용자 선호도 – 사용자 설정 기억
- 도구 결과 캐싱 – 비싼 API 호출 캐시
클라이언트 공유 (Share your client)
유용한 것을 만들었나요? 커뮤니티와 공유를 고려해 보세요:
- 오픈소스화 – GitHub에 코드 공개
- npm에 게시 –
npm install로 설치 가능하게 - 문서 만들기 – 다른 사람들이 작업을 이해하고 확장하도록 돕기
- 토론 참여 – AG-UI GitHub Discussions에 경험 공유
결론 (Conclusion)
처음부터 완전한 AG-UI 클라이언트를 만들었어요! 날씨 어시스턴트가 핵심 개념을 보여줍니다:
- 실시간 스트리밍을 갖춘 이벤트 기반 아키텍처
- 실제 기능을 위한 도구 통합
- 컨텍스트 유지를 위한 대화 메모리
- 사용자 참여를 위한 대화형 CLI 인터페이스
여기서부터 간단한 CLI 도구에서 복잡한 대화형 애플리케이션까지 어떤 사용 사례든 지원하도록 클라이언트를 확장할 수 있어요. AG-UI 프로토콜이 기반을 제공하고, 여러분의 창의성이 가능성을 만들어 냅니다.
즐거운 빌드 되세요! 🚀
더 알아보기 (Learn more)
- AG-UI 개요 — 프로토콜 기본 개념
- 서버 빌드 — AG-UI 호환 서버 구현
- Middleware — 이벤트 변환·가로채기