미들웨어

미들웨어 (Middleware)

기존 프로토콜, 인프로세스 에이전트, 커스텀 솔루션을 AG-UI로 연결하는 방법을 다루는 페이지예요. 미들웨어 구현은 기존 프로토콜과 애플리케이션을 AG-UI 이벤트로 번역(translate) 합니다.

출처: 문서

본문

소개 (Introduction)

미들웨어 구현을 사용하면 기존 프로토콜과 애플리케이션을 AG-UI 이벤트로 번역할 수 있어요. 이 접근 방식은 여러분의 기존 시스템과 AG-UI 사이에 브리지를 만들어, 현재 애플리케이션에 에이전트 기능을 추가하는 데 아주 적합합니다.

미들웨어 구현을 써야 할 때 (When to use a middleware implementation)

미들웨어는 유연한 선택지예요. 기존 프로토콜과 애플리케이션을 AG-UI 이벤트로 번역해 여러분의 기존 시스템과 AG-UI 사이에 브리지를 만들 수 있습니다.

미들웨어는 다음에 아주 좋습니다:

  • 기존 프로토콜이나 API를 범용적으로 번역할 때
  • 기존 시스템이나 프레임워크의 제약 안에서 작업할 때
  • 에이전트 프레임워크나 시스템을 직접 통제할 수 없을 때

만들게 될 것 (What you'll build)

이 가이드에서는 다음을 수행하는 미들웨어 에이전트를 만듭니다:

  1. AbstractAgent 클래스 확장
  2. OpenAI의 GPT-4o 모델에 연결
  3. OpenAI 응답을 AG-UI 이벤트로 번역
  4. 애플리케이션과 함께 인프로세스로 실행

이 접근 방식은 AG-UI 프로토콜의 모든 힘을 유지하면서 기존 코드베이스와 통합할 수 있는 최대한의 유연성을 제공합니다.

시작해 볼게요!

사전 요구 사항 (Prerequisites)

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

  • Node.js v16 이상
  • OpenAI API 키

1. OpenAI API 키 제공

먼저 API 키를 설정합니다:

# Set your OpenAI API key
export OPENAI_API_KEY=your-api-key-here

2. 빌드 유틸리티 설치

다음 도구들을 설치하세요:

brew install protobuf
npm i nx
curl -fsSL https://get.pnpm.io/install.sh | sh -

1단계 – 통합 스캐폴드 (Step 1 – Scaffold your integration)

저장소를 클론하는 것부터 시작합니다

git clone [email protected]:ag-ui-protocol/ag-ui.git
cd ag-ui/

미들웨어 스타터 템플릿을 복사해 OpenAI 통합을 만듭니다:

cp -r integrations/middleware-starter integrations/openai

메타데이터 갱신 (Update metadata)

integrations/openai/package.json을 열고 필드를 새 폴더에 맞게 갱신합니다:

{
  "name": "@ag-ui/openai",
  "author": "Your Name <[email protected]>",
  "version": "0.0.1",

  ... rest of package.json
}

다음으로 integrations/openai/src/index.ts 안의 클래스 이름을 갱신합니다:

// change the name to OpenAIAgent
export class OpenAIAgent extends AbstractAgent {}

마지막으로 apps/dojo/src/menu.ts에 추가해 여러분의 통합을 dojo에 소개합니다:

// ...
export const menuIntegrations: MenuIntegrationConfig[] = [
  // ...

  {
    id: "openai",
    name: "OpenAI",
    features: ["agentic_chat"],
  },
]

그리고 apps/dojo/src/agents.ts:

// ...
import { OpenAIAgent } from "@ag-ui/openai"

export const agentsIntegrations: AgentIntegrationConfig[] = [
  // ...

  {
    id: "openai",
    agents: async () => {
      return {
        agentic_chat: new OpenAIAgent(),
      }
    },
  },
]

2단계 – dojo 의존성에 패키지 추가 (Step 2 – Add package to dojo dependencies)

apps/dojo/package.json을 열고 패키지 @ag-ui/openai를 추가합니다:

{
  "name": "demo-viewer",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@ag-ui/agno": "workspace:*",
    "@ag-ui/langgraph": "workspace:*",
    "@ag-ui/mastra": "workspace:*",
    "@ag-ui/middleware-starter": "workspace:*",
    "@ag-ui/server-starter": "workspace:*",
    "@ag-ui/server-starter-all-features": "workspace:*",
    "@ag-ui/vercel-ai-sdk": "workspace:*",
    "@ag-ui/openai": "workspace:*", <- Add this line

  ... rest of package.json
}

3단계 – dojo 시작 (Step 3 – Start the dojo)

이제 여러분의 작업이 동작하는 모습을 봅니다:

# Install dependencies
pnpm install

# Compile the project and run the dojo
pnpm dev

http://localhost:3000으로 가서 드롭다운에서 OpenAI를 선택하세요. 지금은 스텁 에이전트가 **Hello world!**라고 답하는 걸 볼 수 있어요.

그 스텁 에이전트가 하는 일은 다음과 같습니다:

// integrations/openai/src/index.ts
import {
  AbstractAgent,
  BaseEvent,
  EventType,
  RunAgentInput,
} from "@ag-ui/client"
import { Observable } from "rxjs"

export class OpenAIAgent extends AbstractAgent {
  run(input: RunAgentInput): Observable<BaseEvent> {
    const messageId = Date.now().toString()
    return new Observable<BaseEvent>((observer) => {
      observer.next({
        type: EventType.RUN_STARTED,
        threadId: input.threadId,
        runId: input.runId,
      } as any)

      observer.next({
        type: EventType.TEXT_MESSAGE_START,
        messageId,
      } as any)

      observer.next({
        type: EventType.TEXT_MESSAGE_CONTENT,
        messageId,
        delta: "Hello world!",
      } as any)

      observer.next({
        type: EventType.TEXT_MESSAGE_END,
        messageId,
      } as any)

      observer.next({
        type: EventType.RUN_FINISHED,
        threadId: input.threadId,
        runId: input.runId,
      } as any)

      observer.complete()
    })
  }
}

4단계 – OpenAI를 AG-UI로 브리지 (Step 4 – Bridge OpenAI with AG-UI)

스텁을 OpenAI에서 완성 스트리밍하는 진짜 에이전트로 바꿔 봅시다.

OpenAI SDK 설치 (Install the OpenAI SDK)

먼저 OpenAI SDK가 필요합니다:

cd integrations/openai
pnpm install openai

AG-UI 되짚어보기 (AG-UI recap)

AG-UI 에이전트는 AbstractAgent를 확장하고 일련의 이벤트를 내보내서 다음을 알립니다:

  • 수명주기 이벤트 (RUN_STARTED, RUN_FINISHED, RUN_ERROR)
  • 콘텐츠 이벤트 (TEXT_MESSAGE_*, TOOL_CALL_*, 등)

스트리밍 에이전트 구현 (Implement the streaming agent)

이제 스텁 에이전트를 진짜 OpenAI 통합으로 바꿉니다. 핵심 차이는 하드코딩된 "Hello world!" 메시지를 보내는 대신 OpenAI의 API에 연결하고 AG-UI 이벤트를 통해 응답을 스트리밍한다는 점입니다.

구현은 스텁과 같은 이벤트 흐름을 따르지만, 생성자에서 OpenAI 클라이언트 초기화를 추가하고 모의 응답을 실제 API 호출로 대체합니다. 또한 응답에 도구 호출이 있다면 처리해, 필요할 때 함수를 완전히 사용할 수 있는 에이전트로 만듭니다.

// integrations/openai/src/index.ts
import {
  AbstractAgent,
  RunAgentInput,
  EventType,
  BaseEvent,
} from "@ag-ui/client"
import { Observable } from "rxjs"

import { OpenAI } from "openai"

export class OpenAIAgent extends AbstractAgent {
  private openai: OpenAI

  constructor(openai?: OpenAI) {
    super()
    // Initialize OpenAI client - uses OPENAI_API_KEY from environment if not provided
    this.openai = openai ?? new OpenAI()
  }

  run(input: RunAgentInput): Observable<BaseEvent> {
    return new Observable<BaseEvent>((observer) => {
      // Same as before - emit RUN_STARTED to begin
      observer.next({
        type: EventType.RUN_STARTED,
        threadId: input.threadId,
        runId: input.runId,
      } as any)

      // NEW: Instead of hardcoded response, call OpenAI's API
      this.openai.chat.completions
        .create({
          model: "gpt-4o",
          stream: true, // Enable streaming for real-time responses
          // Convert AG-UI tools format to OpenAI's expected format
          tools: input.tools.map((tool) => ({
            type: "function",
            function: {
              name: tool.name,
              description: tool.description,
              parameters: tool.parameters,
            },
          })),
          // Transform AG-UI messages to OpenAI's message format
          messages: input.messages.map((message) => ({
            role: message.role as any,
            content: message.content ?? "",
            // Include tool calls if this is an assistant message with tools
            ...(message.role === "assistant" && message.toolCalls
              ? {
                  tool_calls: message.toolCalls,
                }
              : {}),
            // Include tool call ID if this is a tool result message
            ...(message.role === "tool"
              ? { tool_call_id: message.toolCallId }
              : {}),
          })),
        })
        .then(async (response) => {
          const messageId = Date.now().toString()

          // NEW: Stream each chunk from OpenAI's response
          for await (const chunk of response) {
            // Handle text content chunks
            if (chunk.choices[0].delta.content) {
              observer.next({
                type: EventType.TEXT_MESSAGE_CHUNK, // Chunk events open and close messages automatically
                messageId,
                delta: chunk.choices[0].delta.content,
              } as any)
            }
            // Handle tool call chunks (when the model wants to use a function)
            else if (chunk.choices[0].delta.tool_calls) {
              let toolCall = chunk.choices[0].delta.tool_calls[0]

              observer.next({
                type: EventType.TOOL_CALL_CHUNK,
                toolCallId: toolCall.id,
                toolCallName: toolCall.function?.name,
                parentMessageId: messageId,
                delta: toolCall.function?.arguments,
              } as any)
            }
          }

          // Same as before - emit RUN_FINISHED when complete
          observer.next({
            type: EventType.RUN_FINISHED,
            threadId: input.threadId,
            runId: input.runId,
          } as any)

          observer.complete()
        })
        // NEW: Handle errors from the API
        .catch((error) => {
          observer.next({
            type: EventType.RUN_ERROR,
            message: error.message,
          } as any)

          observer.error(error)
        })
    })
  }
}

내부에서 무슨 일이 일어나는가? (What happens under the hood?)

여러분의 에이전트가 하는 일을 쪼개 보겠습니다:

  1. 설정 (Setup) – OpenAI 클라이언트를 만들고 RUN_STARTED를 내보냅니다
  2. 요청 (Request) – 사용자 메시지를 stream: true와 함께 chat.completions로 보냅니다
  3. 스트리밍 (Streaming) – 각 청크를 TEXT_MESSAGE_CHUNK 또는 TOOL_CALL_CHUNK로 전달합니다
  4. 완료 (Finish) – RUN_FINISHED(또는 문제가 있으면 RUN_ERROR)를 내보내고 observable을 완료합니다

5단계 – 에이전트와 채팅 (Step 5 – Chat with your agent)

dojo 페이지를 새로고침하고 타이핑을 시작하세요. GPT-4o가 실시간으로 단어 단위로 답을 스트리밍하는 것을 볼 수 있어요.

AG-UI를 어떤 프로토콜로든 브리지 (Bridging AG-UI to any protocol)

방금 구현한 패턴 — 입력 번역, 스트리밍 청크 전달, AG-UI 이벤트 방출 — 은 사실상 모든 백엔드에 적용됩니다:

  • REST 또는 GraphQL API
  • WebSockets
  • MQTT 같은 IoT 프로토콜

에이전트를 프런트엔드에 연결 (Connect your agent to a frontend)

CopilotKit 같은 도구는 AG-UI를 이미 이해하고 플러그앤플레이 React 컴포넌트를 제공합니다. 여러분의 에이전트 엔드포인트를 가리키면 완전한 기능의 채팅 UI가 즉시 나옵니다.

통합 공유 (Share your integration)

다른 사람이 재사용할 수 있는 커스텀 어댑터를 만들었나요? 커뮤니티 기여를 환영합니다!

  1. AG-UI 저장소를 포크하세요
  2. integrations 아래에 패키지를 추가하세요. 자세한 내용과 명명 규칙은 기여 (Contributing)를 참고하세요.
  3. 사용 사례와 설계 결정을 설명하는 풀 리퀘스트를 여세요

질문이 있거나, 피드백이 필요하거나, 먼저 아이디어를 검증하고 싶다면 GitHub Discussions 게시판에서 스레드를 시작하세요: AG-UI GitHub Discussions 게시판.

여러분의 통합이 다음 릴리스에 실려 전체 AG-UI 생태계 성장에 도움을 줄 수 있어요.

결론 (Conclusion)

이제 OpenAI를 위한 완전한 기능의 AG-UI 어댑터와 그것을 테스트할 로컬 플레이그라운드가 생겼습니다. 여기서부터 여러분은:

  • 도구 호출을 추가해 에이전트를 강화할 수 있어요
  • 통합을 npm에 게시할 수 있어요
  • AG-UI를 다른 어떤 모델이나 서비스로도 브리지할 수 있어요

즐겁게 빌드하세요!

더 알아보기 (Learn more)