클라이언트 빌드

클라이언트 빌드 (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 클라이언트를 만들게요:

  1. @ag-ui/mastra의 MastraAgent 사용
  2. OpenAI의 GPT-4o 모델에 연결
  3. 실제 기능을 위한 날씨 도구(weather tool) 구현
  4. 터미널에서 대화형 채팅 인터페이스 제공

시작해 볼게요!

사전 요구사항 (Prerequisites)

시작하기 전에 다음이 있는지 확인하세요:

  • Node.js 22.13.0 이상
  • OpenAI API 키
  • pnpm 패키지 매니저
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",
})

에이전트에서 어떤 일이 일어나나요?

  1. MastraAgent – Mastra 에이전트를 AG-UI 프로토콜 어댑터로 감쌉니다
  2. Model Configuration – 고품질 응답을 위해 OpenAI GPT-4o 사용
  3. Memory Setup – 대화 컨텍스트를 위해 LibSQL을 사용한 영구 메모리 구성
  4. 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 인터페이스에서 어떤 일이 일어나나요?

  1. Readline Interface – 사용자 입력을 위한 대화형 프롬프트 생성
  2. Message Management – 각 사용자 입력을 에이전트의 대화 기록에 추가
  3. Event Handling – AG-UI 이벤트를 수신해 실시간 피드백 제공
  4. 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)

메시지를 보내면 어떤 일이 일어나는지 분석해 볼게요:

  1. 사용자 입력 – 질문을 입력하고 Enter를 누릅니다
  2. 메시지 추가 – 입력이 대화 기록에 추가됩니다
  3. 에이전트 처리 – 에이전트가 요청을 분석하고 응답을 구성합니다
  4. 응답 생성 – 에이전트가 응답을 다시 스트리밍합니다
  5. 스트리밍 출력 – 단어 단위로 응답이 나타나는 것을 봅니다

처리 중인 이벤트 유형:

  • 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}&current=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"
}

날씨 도구에서 어떤 일이 일어나나요?

  1. Tool Definition – 도구 인터페이스를 정의하기 위해 Mastra의 createTool 사용
  2. Input Schema – 도구가 위치 문자열을 받는다고 명시
  3. Output Schema – 반환되는 날씨 데이터의 구조 정의
  4. API Integration – Open-Meteo의 무료 날씨 API에서 데이터 가져오기
  5. 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)

유용한 것을 만들었나요? 커뮤니티와 공유를 고려해 보세요:

  1. 오픈소스화 – GitHub에 코드 공개
  2. npm에 게시 – npm install로 설치 가능하게
  3. 문서 만들기 – 다른 사람들이 작업을 이해하고 확장하도록 돕기
  4. 토론 참여 – AG-UI GitHub Discussions에 경험 공유

결론 (Conclusion)

처음부터 완전한 AG-UI 클라이언트를 만들었어요! 날씨 어시스턴트가 핵심 개념을 보여줍니다:

  • 실시간 스트리밍을 갖춘 이벤트 기반 아키텍처
  • 실제 기능을 위한 도구 통합
  • 컨텍스트 유지를 위한 대화 메모리
  • 사용자 참여를 위한 대화형 CLI 인터페이스

여기서부터 간단한 CLI 도구에서 복잡한 대화형 애플리케이션까지 어떤 사용 사례든 지원하도록 클라이언트를 확장할 수 있어요. AG-UI 프로토콜이 기반을 제공하고, 여러분의 창의성이 가능성을 만들어 냅니다.

즐거운 빌드 되세요! 🚀

더 알아보기 (Learn more)