- AbstractAgentλ AG-UI νλ‘ν μ½μμ μμ΄μ νΈ μν μ μννλ λͺ¨λ μ°Έμ¬μκ° κ΅¬νν΄μΌ νλ μΆμ λ² μ΄μ€ ν΄λμ€
run(input) β Observable<BaseEvent>λ¨ νλμ μΆμ λ©μλλ§ κ΅¬ννλ©΄ AG-UI μ°Έμ¬μκ° λλ μ΅μ κ³μ½(contract)- ꡬν μμΉμ λ°λΌ μν μ΄ λ¬λΌμ§λ μλ©΄ μΆμν: λ°±μλμμ ꡬννλ©΄ AI μ€ν μλ², νλ‘ νΈμλμμ ꡬννλ©΄ μ΄λ²€νΈ μμ ν΄λΌμ΄μΈνΈ
ν΄λΉ κ°λ μ΄ νμν μ΄μ
- μλ²Β·ν΄λΌμ΄μΈνΈκ° λμΌν μΈν°νμ΄μ€λ₯Ό μ¬μ©ν΄ transport λ°©μ(HTTP, WebSocket λ±)κ³Ό 무κ΄νκ² μ°κ²° κ°λ₯
run()νλλ§ κ΅¬ννλ©΄ μν κ΄λ¦¬Β·μ΄λ²€νΈ κ²μ¦Β·λ―Έλ€μ¨μ΄Β·κ΅¬λ μ μλ¦Όμ΄ μλμΌλ‘ μ²λ¦¬λ¨- λ°±μλ μμ΄μ νΈλ₯Ό κ΅μ²΄ν΄λ νλ‘ νΈμλ μ½λ λ³κ²½μ΄ λΆνμν λ²€λ λ λ¦½μ± ν보
AS-IS: μ§μ ꡬν μ
sequenceDiagram autonumber participant F as Frontend participant B as Backend Agent F->>B: HTTP POST (raw fetch) B-->>F: SSE raw bytes Note over F: bytes νμ±<br/>μν κ΄λ¦¬<br/>μ΄λ²€νΈ κ²μ¦<br/>λͺ¨λ μ§μ ꡬν
TO-BE: AbstractAgent μ¬μ© μ
sequenceDiagram autonumber participant F as Frontend participant HA as "HttpAgent (AbstractAgent)" participant B as "Backend (AbstractAgent)" F->>HA: runAgent(input) HA->>B: HTTP POST + RunAgentInput B-->>HA: SSE raw bytes HA-->>HA: bytes β Observable[BaseEvent] λ³ν Note over HA: transformChunks<br/>verifyEvents<br/>apply (state/messages μλ κ°±μ )<br/>subscribers μλ¦Ό HA-->>F: subscribers μ½λ°±
ꡬν μμΉμ λ°λ₯Έ μν μ°¨μ΄
κ°μ μΆμ ν΄λμ€μ§λ§ μ΄λμ ꡬννλλμ λ°λΌ μν μ΄ μμ ν λ¬λΌμ§λ€.
// λ°±μλ β LLM νΈμΆ ν μ΄λ²€νΈ μ§μ μμ±
class BackendAgent extends AbstractAgent {
run(input: RunAgentInput): Observable<BaseEvent> {
return new Observable(subscriber => {
const stream = await llm.stream(input.messages)
subscriber.next({ type: EventType.RUN_STARTED, ... })
for await (const chunk of stream) {
subscriber.next({ type: EventType.TEXT_MESSAGE_CONTENT, delta: chunk.text })
}
subscriber.next({ type: EventType.RUN_FINISHED, ... })
subscriber.complete()
})
}
}
// νλ‘ νΈμλ β μλ² SSEλ₯Ό μμ ν΄ Observableλ‘ λ³ν (ν΅μ¬ μλμ½λ)
class FrontendAgent extends AbstractAgent {
run(input: RunAgentInput): Observable<BaseEvent> {
return new Observable(subscriber => {
fetch(url, { method: "POST", body: JSON.stringify(input) })
.then(res => res.body.getReader())
.then(async reader => {
while (true) {
const { done, value } = await reader.read() // Uint8Array μ²ν¬
if (done) break
const event = parseSSE(value) // JSON.parse(...)
subscriber.next(event) // re-emit
}
subscriber.complete()
})
})
}
}
// β HttpAgentκ° FrontendAgentλ₯Ό μ΄λ―Έ ꡬνν΄ μ 곡
const agent = new HttpAgent({ url: "https://my-backend/agent" })| κ΅¬λΆ | λ°±μλ ꡬν | νλ‘ νΈμλ ꡬν |
|---|---|---|
run()μ μν | LLM νΈμΆ β μ΄λ²€νΈ μμ± | HTTP bytes β Observable λ³ν |
| μ΄λ²€νΈ λ°©ν₯ | emit (μ°½μ‘°) | re-emit (λ³νΒ·μ λ¬) |
| κΈ°λ³Έ μ 곡 ꡬν체 | μμ (μ§μ ꡬν) | HttpAgent |
μ€μ AIμ ν΅μ νλ €λ©΄ μλ²λ λ°λμ μ€ν μ€μ΄μ΄μΌ νλ€
run() μμμ 무μμ νΈμΆνλ , μ€μ AI μ²λ¦¬κ° μΌμ΄λλ μλ²κ° λ μμ΄μΌ νλ€.
Ollama λ‘컬 μλ² μ°λ: run() β fetch("http://localhost:11434") β Ollama μλ² (νμ μ€ν)
μΈλΆ LLM API μ°λ: run() β fetch("https://api.openai.com") β OpenAI μλ² (μΈλΆ, μμ μ€ν)
λ¨, μλ² μμ΄ λμνλ κ²½μ°λ μλ€ β ν μ€νΈμ© MockAgent:
// ν
μ€νΈ λͺ©μ μΌλ‘λ§ μ¬μ©. μ€μ AIμ ν΅μ νμ§ μμ
class MockAgent extends AbstractAgent {
run(input: RunAgentInput): Observable<BaseEvent> {
return of([κ³ μ λ μ΄λ²€νΈλ€])
}
}
// CopilotKitμ selfManagedAgents propμ΄ μ΄ ν¨ν΄μ μ§μ
<CopilotKitProvider selfManagedAgents={{ mock: new MockAgent() }}>run()μ΄ λ°ννλ Observableμ μλ―Έ β νλ‘ νΈμλμμ run()μ ꡬννλ μ΄μ
μ μ΄ μ§λ¬Έμ΄ μκΈ°λκ°
μλ²κ° μ΄λ―Έ μ΄λ²€νΈλ₯Ό λ§λ€μ΄ μ λ¬νλ ν΄λΌμ΄μΈνΈλ κ·Έλ₯ μ΅μ λΉλ§ νλ©΄ λ κ² κ°λ€λ μ§κ΄μ΄ μλ€. κ·Έλ λ€λ©΄ νλ‘ νΈμλμμ run()μ ꡬνν νμκ° μλκ°?
λ΅μ β무μμ μ΅μ λΉνλλβ μ μλ€. μλ²μ Observable<BaseEvent>λ λ€νΈμν¬λ₯Ό ν΅κ³Όνλ μκ° μλ©Έλλ€. HTTPλ λ°μ΄νΈλ§ μ λ¬ν μ μκΈ° λλ¬Έμ΄λ€. νλ‘ νΈμλ run()μ μ΄λ²€νΈλ₯Ό μ°½μ‘°νλ κ²μ΄ μλλΌ μλ©Έλ Observableμ raw bytesλ‘λΆν° 볡μνλ μν μ΄λ€.
sequenceDiagram autonumber participant B as "BackendAgent.run()" participant NET as "Network (HTTP)" participant C as "FrontendAgent.run()" participant PP as "runAgent() Pipeline" Note over B: Observable[BaseEvent] μμ± B->>B: subscriber.next(TEXT_MESSAGE_CONTENT, delta:μλ ) Note over B,NET: Observableμ λ€νΈμν¬ μ μ‘ λΆκ°<br/>μ§λ ¬ν νμ B->>NET: SSE data: type=TEXT_MESSAGE_CONTENT, delta=μλ Note over NET: raw bytes (Uint8Array)<br/>Observable μλ©Έ NET->>C: Uint8Array μ²ν¬ μμ Note over C: bytes β Observable 볡μ νμ<br/>νλ‘ νΈμλ run()μ μν C->>C: TextDecoder β SSE ν μ€νΈ C->>C: SSE νμ± β JSON.parse() C->>C: subscriber.next(event) re-emit C->>PP: Observable[BaseEvent] μ λ¬ Note over PP: transformChunks β verifyEvents<br/>β apply β subscribers
runAgent() νΈμΆ νλ¦ β μ€μ λ©μλ μ€ν μμ
runAgent()μ νΈμΆ 주체λ **Application(νλ‘ νΈμλ UI)**μ΄λ€. νΈμΆ μ΄ν λ΄λΆ νμ΄νλΌμΈμ΄ μλμΌλ‘ μ€νλλ€.
sequenceDiagram autonumber box Frontend participant App as "Application (UI)" participant AA as "AbstractAgent.runAgent()" participant HA as "HttpAgent.run()" participant TR as "transformHttpEventStream()" participant PP as Pipeline participant SUB as Subscribers end box Backend participant B as "BackendAgent.run()" end App->>AA: agent.runAgent(parameters) AA->>AA: prepareRunAgentInput() AA->>HA: this.run(input) HA->>B: HTTP POST + RunAgentInput (JSON body) B->>B: LLM νΈμΆ β BaseEvent emit B-->>HA: SSE raw bytes HA->>TR: transformHttpEventStream() TR->>TR: SSE νμ± β JSON.parse() TR-->>AA: Observable[BaseEvent] AA->>PP: transformChunks β verifyEvents β apply PP-->>SUB: onMessagesChanged / onStateChanged SUB-->>App: UI μ λ°μ΄νΈ
RunAgentInput β λκ° μ΄λ€ κ°μ λ£λκ°
Frontend κ΄μ β AbstractAgentκ° μλμΌλ‘ 쑰립
prepareRunAgentInput()μ΄ runAgent() νΈμΆ μ μλμΌλ‘ ꡬμ±νλ€. κ°λ°μκ° μ§μ μ±μΈ νμκ° μλ€.
// agent.ts - prepareRunAgentInput()
{
threadId: this.threadId, // AbstractAgentκ° μμ±Β·κ΄λ¦¬ (uuid)
runId: uuidv4(), // λ§€ μ€νλ§λ€ μλ‘ μμ±
messages: this.messages, // λμ λ λν μ΄λ ₯ (μλ κ΄λ¦¬)
state: this.state, // νμ¬ κ³΅μ μν (μλ κ΄λ¦¬)
tools: parameters?.tools, // runAgent() νΈμΆ μ κ°λ°μκ° μ λ¬
context: parameters?.context, // runAgent() νΈμΆ μ κ°λ°μκ° μ λ¬
forwardedProps: parameters?.forwardedProps, // μΆκ° props μ λ¬μ©
}κ°λ°μκ° μ§μ λ£λ κ°μ tools, context, forwardedPropsλΏμ΄λ€:
agent.runAgent({
tools: [{ name: "search", description: "...", parameters: schema }],
context: [{ description: "User timezone", value: "Asia/Seoul" }],
})
// threadId, runId, messages, state λ AbstractAgentκ° μλ μ²λ¦¬Backend κ΄μ β HTTP POST bodyλ‘ μμ , run()μ νλΌλ―Έν°λ‘ μ λ¬
Backendλ RunAgentInputμ λ§λ€μ§ μκ³ λ°λλ€. Frontendκ° μ‘°λ¦½ν κ°μ κ·Έλλ‘ νμ©νλ€.
class BackendAgent extends AbstractAgent {
run(input: RunAgentInput): Observable<BaseEvent> {
// input.messages β LLM context (μ΄μ λν μ΄λ ₯)
// input.tools β LLMμ λκΈΈ tool λͺ©λ‘
// input.state β νμ¬ κ³΅μ μν μ°Έμ‘°
// input.threadId β μΈμ
μΆμ μ©
const response = await llm.chat({
messages: input.messages,
tools: input.tools,
})
...
}
}| νλ | μμ± μ£Όμ²΄ | Frontend μν | Backend μν |
|---|---|---|---|
threadId | AbstractAgent (μλ) | μΈμ μλ³ | λ‘κ·ΈΒ·μΆμ μ© |
runId | AbstractAgent (μλ) | μ€ν μλ³ | λ‘κ·ΈΒ·μΆμ μ© |
messages | AbstractAgent (μλ λμ ) | λν μ΄λ ₯ μ μ‘ | LLM contextλ‘ μ¬μ© |
state | AbstractAgent (μλ κ΄λ¦¬) | νμ¬ μν μ μ‘ | μν μ°Έμ‘°Β·λΆκΈ° |
tools | κ°λ°μ β runAgent() | tool λͺ©λ‘ μ λ¬ | LLMμ tool λ±λ‘ |
context | κ°λ°μ β runAgent() | λΆκ° μ 보 μ λ¬ | ν둬ννΈ λ³΄κ° |
Subscribers β μν λ³ν ꡬλ
raw μ΄λ²€νΈκ° μλ μ²λ¦¬ ν κ²°κ³Όλ₯Ό λ°λ μ½λ°± μμ€ν μ΄λ€.
agent.subscribe({
onMessagesChanged: ({ messages }) => { /* μ λ©μμ§ μμ μ */ },
onStateChanged: ({ state }) => { /* μν λ³κ²½ μ */ },
onRunFinishedEvent: ({ result }) => { /* μ€ν μλ£ μ */ },
onRunFailed: ({ error }) => { /* μλ¬ λ°μ μ */ },
})raw BaseEventλ₯Ό μ§μ λ³΄λ €λ©΄ run(input).subscribe(event => ...) μ¬μ©.