@bluedus/aiplug-sdk
v1.0.3
Published
AIPlug Enterprise AI Agent SDK - React & JavaScript
Maintainers
Readme
@bluedus/aiplug-sdk
AIPlug Enterprise AI Agent SDK — React & JavaScript.
AIPlug Core 서버(/api/v1/chat, SSE)에 붙어 RAG · Function Calling · 실행 확인(confirm) · 출처 인용 · 지도/차트 시각화를 갖춘 AI 채팅 UI를 만든다. 헤드리스 함수부터 완성형 드롭인 컴포넌트까지 3가지 형태로 제공한다.
설치
npm install @bluedus/aiplug-sdkReact는 peer dependency다(선택 — 함수형만 쓰면 불필요).
react >= 17, react-dom >= 17지도(leaflet)·차트(echarts)는 SDK 의존성이라 따로 설치할 필요가 없다. leaflet.css 도 SDK 가
런타임에 주입하므로 앱에서 import 하지 않는다. (자세히)
지도를 안 쓰는 앱이라도 번들이 커지지 않는다 — /leaflet·/echarts 는 별도 진입점이고 내부에서도
동적 import 라, 해당 진입점을 import 하지 않으면 번들러가 아예 포함하지 않는다(node_modules 용량만 늘어난다).
진입점
| import 경로 | 내용 | 프레임워크 |
|------|------|------|
| @bluedus/aiplug-sdk/stream | 스트리밍 코어 — createChatStream, SSE 파서, reducer, fetch 함수 | 중립 (vanilla/vue/react) |
| @bluedus/aiplug-sdk/stream/react | React 훅 · 컴포넌트 — useAIPlugChat, StreamingChat, BlockRenderer | React |
| @bluedus/aiplug-sdk/leaflet | 지도 블록 렌더러 — LeafletMapBlock | React |
| @bluedus/aiplug-sdk/echarts | 차트 블록 렌더러 — EChartsChartBlock | React |
| @bluedus/aiplug-sdk | 레거시 클라이언트 — AIPlugClient, ChatSession | 중립 |
| @bluedus/aiplug-sdk/react | 레거시 React — AgentChat, useAIPlug | React |
⚠️ 동기(비스트리밍) 방식은 더 이상 동작하지 않습니다.
AIPlugClient.sendMessage(),ChatSession.send(),AgentChat의streaming={false}는/api/v2/chat을 호출하는데, 이 엔드포인트는 Core 1.0.0 정리 과정에서 제거되었습니다(404). 신규 개발은stream계열을 사용하세요. 동기 방식이 필요해지면 Core에 v1 동기 엔드포인트를 추가한 뒤 다시 지원할 예정입니다.
3가지 사용 형태
1. 함수형 (헤드리스)
UI를 직접 만들고 스트림만 받는다. 프레임워크 무관.
import { createChatStream } from '@bluedus/aiplug-sdk/stream';
const stream = createChatStream(
{ baseUrl: 'https://core.example.com', publicKey: 'pk_...', tenantId: 'T-1' },
{ prompt: '매생이는 언제 자라나요?' },
);
for await (const event of stream) {
if (event.type === 'token') process.stdout.write(event.data.text);
if (event.type === 'done') console.log('\n완료', event.data.messageId);
}2. 툴킷 (훅 + 블록 렌더러)
상태 관리는 SDK가, 레이아웃은 앱이. 가장 많이 쓰는 형태.
'use client';
import { useAIPlugChat, BlockRenderer } from '@bluedus/aiplug-sdk/stream/react';
const config = { baseUrl: '...', publicKey: 'pk_...', tenantId: 'T-1' };
export function Chat() {
const { messages, status, isLoading, send } = useAIPlugChat(config);
return (
<div>
{messages.map((m) => (
<div key={m.id}>
{m.role === 'user' ? <p>{m.blocks[0]?.type === 'TEXT' && m.blocks[0].text}</p>
: <BlockRenderer message={m} />}
</div>
))}
<button disabled={isLoading} onClick={() => send('안녕')}>전송</button>
</div>
);
}3. 드롭인 (완성형)
config 만 주면 채팅 UI 전체가 렌더된다. 입력창·모델 선택·확인 버튼·피드백·복사까지 포함.
'use client';
import { StreamingChat } from '@bluedus/aiplug-sdk/stream/react';
<StreamingChat config={{ baseUrl: '...', publicKey: 'pk_...', tenantId: 'T-1' }} />주요 기능
SSE 스트리밍 — 토큰 단위 실시간 렌더. thinking(Agent 탐색·RAG 검색·Tool 실행 단계) 상태 표시.
블록 렌더링 — 응답이 ContentBlock[](TEXT / TABLE / IMAGE / CHART / MAP / PENDING / STEPS / INTENT / STEP / REFERENCES / KEYWORDS)로 정규화된다. components prop 으로 타입별 렌더러 교체 가능.
지도·차트 시각화 — LLM이 낸 GeoJSON을 Leaflet 지도로(point/line/polygon), 수치 데이터를 ECharts 차트로 렌더한다. 기본 렌더러를 별도 진입점으로 제공하므로 components 에 주입만 하면 된다. 지도·차트 렌더링 참조.
생성 중 스켈레톤 — 지도·차트·표는 내용이 다 도착해야 렌더할 수 있어 그동안 화면이 비어 보인다. 파서가 아직 닫히지 않은 펜스·작성 중인 표를 감지해 그 자리에 셔머 스켈레톤("지도 생성 중…")을 깔고, 완성되면 같은 자리에서 교체한다. 작성 중인 문법(백틱·파이프·![)이 화면에 새지 않는다. 앱이 따로 할 일은 없다.
출처 인용 — 본문·표 셀의 [n] 마커가 인용 칩으로 렌더되고, 클릭하면 하단 참고목록의 해당 출처로 스크롤 + 하이라이트된다. 스크롤 로직은 BlockRenderer 내장이라 앱에서 핸들러가 필요 없다.
출처 분류 · 원본 다운로드 — 참고목록이 자료 종류(파일 / 어떤 DB의 무슨 자료 / 스킬)별로 묶여 나오고, 파일 출처는 원본을 내려받을 수 있다. 출처 목록 참조.
멀티 에이전트 진행 표시 — 여러 Agent가 단계별로 답을 이어 쓸 때, 어느 단계까지 왔고 어떤 Agent가 맡고 있는지 표시한다. 스트리밍 중에는 SSE로, 대화를 다시 열면 본문에 남은 펜스로 같은 카드가 복원된다. 기본은 꺼져 있다. 멀티 에이전트 진행 표시 참조.
질문 의도 정리 — 멀티 에이전트 답변 맨 앞에 "이 질문은 무엇을 묻고 있는지"를 한두 문장으로 붙인다. 의도가 잘못 잡혔으면 사용자가 첫 문단에서 알아챈다. 질문 의도 정리 참조.
이어서 물어보기 — 답변 끝에 후속 질문을 버튼으로 제안하고, 대화 시작 화면에는 관리자가 등록한 추천 질문을 띄운다. 눌러서 바로 보낸다. 이어서 물어보기 참조.
되묻기(clarify) — 질문이 애매하면 답변을 만들지 않고 멈춰서 방향을 되묻는다. 0.8.0 추가. 되묻기 참조.
실행 확인(confirm) — Tool 실행·오케스트레이션·Agent 사용 동의를 pending 상태로 받아 confirm(action) / acceptConsent(accepted) 로 응답한다. 0.5.7 부터 대기 상태가 암호화 토큰으로 왕복한다. 실행 확인(confirm) 참조.
피드백 — 응답·출처 단위 따봉(👍/👎)과 코멘트. FeedbackContext + FeedbackBar, 또는 훅의 feedback API.
복사 — 메시지 전체(messageToText) 및 블록 단위(blockToText) 복사. 표는 마크다운 표로 직렬화된다. 자세한 내용은 아래 참조.
모델 선택 — useModels 로 테넌트 사용 가능 모델을 조회하고, send(text, modelId) 로 턴마다 지정.
대화 이력 — useConversations 로 목록 조회, loadConversation(id) 로 복원(표·이미지·출처 포함).
지도·차트 렌더링
지도(MAP)와 차트(CHART)는 렌더 라이브러리가 필요해 기본 렌더러가 자동으로 붙지 않는다. 별도 진입점에서 import 해 components 로 주입한다(주입 안 하면 "렌더러 주입 필요" 안내 문구가 표시된다).
'use client';
import { BlockRenderer } from '@bluedus/aiplug-sdk/stream/react';
import { LeafletMapBlock } from '@bluedus/aiplug-sdk/leaflet';
import { EChartsChartBlock } from '@bluedus/aiplug-sdk/echarts';
const BLOCK_COMPONENTS = { MAP: LeafletMapBlock, CHART: EChartsChartBlock };
<BlockRenderer message={message} components={BLOCK_COMPONENTS} />
BLOCK_COMPONENTS는 컴포넌트 밖(모듈 스코프)에 두자. 렌더마다 새 객체를 만들면 지도·차트가 매번 재생성된다.
StreamingChat 드롭인도 동일하게 components prop 을 받는다.
leaflet.css — 앱에서 import 하지 않아도 된다
LeafletMapBlock 은 마운트 시 leaflet.css 를 <head> 최상단에 <style> 로 자동 주입한다(문서당 1회, 이미지는 data URI 로 인라인되어 있어 추가 요청이 없다). 그래서 앱은 leaflet/dist/leaflet.css 를 import 할 필요가 없다 — Next.js App Router 처럼 전역 CSS import 위치에 제약이 있는 환경에서 특히 편하다.
- 앱이 이미 leaflet.css 를 로드했으면 감지해서 주입하지 않는다(중복 없음).
<head>최상단에 넣으므로 앱 CSS 가 항상 뒤에 와서 오버라이드가 정상 동작한다.- SSR 에서는 아무 것도 하지 않는다.
인라인 <style> 을 막는 엄격한 CSP(style-src 에 unsafe-inline 없음)를 쓴다면 주입을 끄고 앱에서 직접 로드한다.
import 'leaflet/dist/leaflet.css';
// ...
<BlockRenderer message={m} components={{ MAP: (p) => <LeafletMapBlock {...p} injectCss={false} /> }} />인라인된 CSS 는 [email protected] 스냅샷이며 LEAFLET_CSS_VERSION 으로 확인할 수 있다. leaflet 버전을 올릴 때는 npm run gen:leaflet-css 로 재생성한다.
MAP 블록 스펙
{
type: 'MAP',
title: '조사 지점',
spec: {
center?: [number, number], // [위도, 경도] — 생략 시 데이터 범위로 자동 맞춤(fitBounds)
zoom?: number,
geojson: GeoJSONFeatureCollection,
}
}GeoJSON 표준을 그대로 쓴다(coordinates 는 [경도, 위도] 순서). Point·LineString·Polygon 모두 지원하며, 각 feature 의 properties 로 표시를 제어한다.
| properties 키 | 용도 |
|------|------|
| name | 마커 툴팁 · 팝업 제목 |
| popup | 팝업 본문 |
| color | 선·테두리 색 (기본 accentColor) |
| fillColor | 채움 색 (기본 color) |
LeafletMapBlock props: height(기본 320), tileUrl·attribution(기본 OSM — 내부망 타일서버로 교체 가능), accentColor, injectCss(기본 true).
마커는 이미지 아이콘이 아니라 circleMarker 로 그린다(번들러별 아이콘 경로 문제 회피). 팝업 텍스트는 HTML 이스케이프된다.
CHART 블록 스펙
{
type: 'CHART',
chartType: ChartType, // 아래 15종
title: '월별 방문자',
spec: { xAxis: { data: [...] }, series: [{ name: '...', data: [...] }] }
}지원 종류 (15종) — 데이터가 답해야 할 질문에 맞춰 고른다.
| 종류 | 쓰임 | 최소 스펙 |
|------|------|------|
| bar | 항목 간 수치 비교 | xAxis.data + series[].data |
| horizontalBar | 항목명이 길 때(학명 등) — 축이 반전된다 | 〃 |
| line | 시간 추이 | 〃 |
| area | 추이 + 누적량 강조 (line + areaStyle) | 〃 |
| scatter | 두 지표의 상관 | series[].data: [[x,y], …] |
| heatmap | 항목×지표 매트릭스 | xAxis.data·yAxis.data + [[x,y,v], …] |
| pie | 전체 대비 비중 (항목 6개 이하) | xAxis.data(라벨) + series[0].data(값) |
| donut | 〃 (가운데가 빈 형태) | 〃 |
| funnel | 단계별 감소 | 〃 |
| radar | 한 대상의 여러 지표를 한눈에 (3~8개) | xAxis.data(지표명) + series[].data |
| treemap | 계층 구조의 비중 | data: [{name, value, children}] |
| sunburst | 〃 (방사형) | 〃 |
| tree | 계층 구조의 갈래 — 분류체계·계통도 | data: [{name, children}] (value 불필요) |
| graph | 다대다 관계망 — 생물↔화합물↔타겟↔질병 | nodes: [{name, category}] + links: [{source, target}] |
| gauge | 단일 지표 하나 | value, max |
EChartsChartBlock 이 종류에 맞는 완전한 ECharts option 으로 보정한다 — 축 계열엔 축·grid·범례를,
비축 계열(pie·radar·treemap 등)엔 축을 붙이지 않고 각자 필요한 구조를 채운다. 숫자 배열만 줘도
{name, value} 로 짝지어 주고, radar 는 indicator 를 생략하면 데이터 최대값으로 자동 생성한다.
tree 는 루트가 여럿이면 rootName 으로 묶어 하나로 만들고(ECharts 는 루트 하나만 그린다), 루트 이름과 가장 긴 잎 이름의 글자 폭을 재어 좌우 여백을 잡는다(한글은 전각으로 계산). 좁은 화면에서 계급이 6단이면 라벨이 겹치는데, 감추면 계급 이름이 없어져 분류체계 구실을 못 하므로 전부 보이게 두고 확대·이동(roam)으로 읽게 한다. graph 는 연결 수에 비례해 노드 크기를 키우고 노드 수에 따라 반발력을 조절한다 — 고정값을 쓰면 5개일 땐 흩어지고 50개일 땐 뭉친다. category 는 문자열로 주면 범례 인덱스로 바꿔 준다. 노드가 40개를 넘으면 라벨을 감춘다(겹쳐서 읽을 수 없다).
ECharts 표준 형태로 바로 줘도 그대로 통과한다. 보정 함수는 buildOption(block, palette) 으로
export 되어 있어 커스텀 렌더러에서 재사용할 수 있다.
긴 축 라벨(학명 등)은 grid.containLabel 로 잘리지 않게 처리된다.
EChartsChartBlock props: height(기본 280), palette(시리즈 색상 배열).
차트 색상
기본 팔레트는 다크 서피스 기준으로 검증된 8색 고정 순서다. 명도 밴드·채도 하한·색약(CVD) 인접 구분 ΔE·일반시야 구분·대비를 모두 통과한 조합이며, 순서 자체가 색약 안전장치이므로 임의로 섞지 않는다. 항목이 8개를 넘으면 9번째 색을 만들지 말고 "기타"로 접거나 차트를 나눈다.
blue #3987e5 · orange #d95926 · aqua #199e70 · yellow #c98500
magenta #d55181 · green #008300 · violet #9085e9 · red #e66767heatmap 처럼 크기(magnitude)를 나타내는 스케일은 무지개가 아니라 단일 hue 명암 램프를 쓴다.
시리즈가 하나인 막대·선은 모든 마크가 한 색이다(의도된 동작). 막대마다 색을 바꾸면 막대 길이가 이미 보여주는 정보를 색으로 중복 인코딩하면서 색약 검사에는 구조적으로 실패한다. 화면을 다채롭게 하려면 색이 아니라 차트 종류를 데이터에 맞게 섞는 쪽이 맞다.
팔레트를 바꾸려면 palette prop 으로 넘기되, 새 조합은 반드시 색약·대비 검증을 거칠 것.
커스텀 렌더러
기본 렌더러 대신 앱 자체 구현을 쓰려면 같은 자리에 넣으면 된다. 스켈레톤도 교체 가능하다.
<BlockRenderer
message={message}
components={{
MAP: MyMapBlock, // { block: MapContentBlock }
CHART: MyChartBlock, // { block: ChartContentBlock }
PENDING: MySkeleton, // { block: PendingContentBlock } — 기본 BlockSkeleton
STEPS: MyStepProgress, // { block: StepsContentBlock } — 기본 StepProgress
INTENT: MyIntentSummary, // { block: IntentContentBlock } — 기본 IntentSummary
}}
/>표 행 클릭 → 상세 화면 연결 (views)
조회 결과를 표로 보여주고 나면 "이 행을 눌러 상세를 보고 싶다" 가 곧바로 따라온다. 그런데 LLM 은 화면을 여는 코드를 만들 수 없다 — 답변은 한 번 그려지고 끝나는 정적 산출물이고, 클릭 이후는 브라우저에서 살아 있는 코드만 할 수 있다.
그래서 LLM 은 "무엇을 열지" 만 선언하고, 실행 권한은 앱이 등록한 함수에만 둔다.
앱: 열 수 있는 화면을 등록한다
<StreamingChat
config={config}
views={{
contract: {
label: '계약 상세',
description: '계약 1건의 상세 정보', // 서버로 전달돼 LLM 판단 근거가 된다
params: { id: { type: 'uuid' } },
open: (p) => setContractId(p.id), // 모달이든 라우팅이든 앱이 정한다
},
unit: {
label: '호실 상세',
params: { id: { type: 'uuid' }, siteId: { type: 'uuid' } },
open: (p) => setUnit({ unitId: p.id, siteId: p.siteId }),
},
}}
/>BlockRenderer 도 같은 views prop 을 받는다. 등록하지 않으면 표는 예전처럼 정적으로 그려진다.
스킬: 숨김 컬럼으로 식별자를 실어 보낸다
LLM 이 낼 수 있는 건 마크다운 표뿐이라 식별자를 실을 자리가 셀밖에 없다. _ 로 시작하는 컬럼은 화면에 그리지 않고 행 클릭 대상으로만 쓴다.
| 계약번호 | 단지 | _view | _id | _siteId |
| --- | --- | --- | --- | --- |
| S00MQ | 평촌자이 | contract | c7f3a91e | site-1 |_view— 열 화면의 키. 별칭_type·_modalKey·_viewKey도 받는다(앞에 있는 것이 우선)- 나머지
_xxx— 파라미터._를 뗀 이름으로open에 전달된다 (_siteId→siteId)
별칭을 넓게 받는 이유는 LLM 이 지시받은 이름을 그대로 쓰지 않기 때문이다. _type 으로 지시했는데 _modalKey 로 낸 사례가 실측에서 나왔고, 그 이름만 보던 앱에서는 표의 모든 행이 클릭 불가가 됐다. 같은 이유로 값이 -·N/A·없음 이면 "값 없음" 으로 보고 그 행만 클릭을 끈다(LLM 이 빈 칸 대신 채워 넣는 표기다).
파서가 이 컬럼들을 headers/rows 에서 빼내 TableContentBlock.rowActions 로 옮기므로, UUID 가 화면에 노출되지 않으면서 행 클릭에는 쓸 수 있다. 숨김 컬럼이 없으면 rowActions 자체가 없다(기존 표와 완전히 동일).
표 파싱은 SDK 가 하므로 서버 버전은 가리지 않는다. 다만 두 가지가 필요하다.
- 스킬 응답이
_view·_id를 실제로 내려줄 것 — 프롬프트만으로는 만들어지지 않는다 - LLM 이 그 컬럼을 표에 유지할 것 — 서버 기본 프롬프트(
TABLE_GUIDE)에 규약이 반영돼 있으면 자동이고, 아니면 에이전트 프롬프트에 직접 적는다
지시가 없으면 LLM 이 식별자를 HTML 주석이나 표 아래 별도 줄로 내보내는 일이 실제로 생긴다. 그러면 UUID 가 화면에 노출되면서 행 클릭은 켜지지 않는다 — 표 안에 컬럼으로 넣으라고 못박아야 한다.
검증은 두 번 한다
등록 시점 — 화면 키가 camelCase 가 아니거나 open 이 함수가 아니면 그 화면을 빼고 개발 모드에서 무엇이 틀렸는지 찍는다. 조용히 넘어가면 "표는 나오는데 클릭만 안 되는" 상태가 되고, 원인이 프롬프트인지 스킬인지 등록 코드인지 구분할 수 없다.
클릭 시점 — 등록되지 않은 화면이거나 params 규격에 어긋나면 클릭이 아예 켜지지 않는다. 커서만 바뀌고 눌러도 아무 일이 없는 "유령 클릭" 이 생기지 않는다.
_view: contract, _id: 1c5676755838400daecde4bcb4b3d18a → 열림
_view: contract, _id: democtanyang → 차단 (uuid 아님 — LLM 전사 오류)
_view: tenant, _id: ... → 차단 (등록 안 된 화면)
_view: unit, _id: u1 (siteId 없음) → 차단 (필수 파라미터 누락)params 를 생략하면 검증 없이 그대로 넘긴다. required: false 면 그 파라미터는 없어도 된다.
규약이 어긋났을 때 — 앱이 보충한다
여기까지가 정방향이다. 그런데 프롬프트는 테넌트가 콘솔에서 직접 쓴다. SDK 가 "이렇게 써야 동작한다" 고 요구하면 그 순간 플랫폼으로서는 실패다. 실제로 자주 어긋난다.
- 컬럼 이름을 다르게 낸다 (
_type으로 지시했는데_modalKey로 냄) - 값을
-로 채운다 (스킬이 화면 키를 안 주면 "값 없음" 규칙을 따라간다) _siteId처럼 모달에 필요한 값을 프롬프트가 아예 빼버린다
이름·표기 흔들림은 SDK 가 흡수한다(별칭·"값 없음" 표기). 하지만 "이 표는 민원 표다" 같은 판단은 도메인 지식이라 범용 SDK 에 넣을 수 없다. 그 자리를 앱에 연다.
<BlockRenderer
views={views}
resolveRowAction={(action, ctx) => {
if (action) return action; // 규약대로 왔으면 그대로
// 화면 키를 못 찾았을 때만 — 숨김 컬럼 값은 ctx.meta 에 살아 있다
if (!ctx.meta.id) return null;
const h = ctx.headers.join(' ');
const view = h.includes('민원') ? 'complaint'
: h.includes('계약번호') ? 'contract'
: h.includes('전용면적') ? 'unit'
: undefined;
return view ? { view, params: ctx.meta } : null;
}}
/>ctx 는 { headers, row, meta, rowIndex, title } 다. meta 는 화면 키를 못 찾은 행에도 남는다 — _view 가 비었어도 _id 는 살아 있으므로, 앱이 나머지 근거로 대상을 만들 수 있다.
action 을 그대로 돌려주면 기본 동작, null 이면 그 행은 클릭이 꺼진다. 규약대로 온 행을 다른 화면으로 바꾸는 것도 가능하다(재정의).
메커니즘은 SDK, 지식은 앱. 이 경계가 지켜지면 테넌트가 프롬프트를 어떻게 쓰든 앱이 대응할 수 있다.
커스텀 렌더러에서도 그대로 쓴다
표 디자인을 바꾼다고 행 클릭 배선까지 다시 짜야 하면 아무도 커스텀하지 않는다. useTableRows 가 해석을 대신한다.
function MyTable({ block }) {
const { headers, rows, getRowAction } = useTableRows(block);
return (
<table>
{rows.map((row, i) => {
const open = getRowAction(i); // 실행 불가면 null
return <tr key={i} onClick={open ?? undefined}
style={open ? { cursor: 'pointer' } : undefined}>…</tr>;
})}
</table>
);
}커스텀은 스타일만의 문제 여야 한다.
어느 Agent 가 답할지 지정 (agentIds)
기본은 서버가 질문을 보고 Agent 를 고른다. 그런데 화면 영역마다 다룰 내용이 정해져 있으면 그 자리에 어느 Agent 가 응답할지 앱이 정하고 싶다.
const config = {
baseUrl, publicKey, tenantId,
agentIds: ['종정보-agent', 'marinedb-agent', '효능분석-agent'],
};배열 순서대로 실행되고, 앞 단계 답변이 다음 단계로 전달된다. 한 번의 호출로 끝나므로 Agent 마다 따로 부르는 것보다 왕복도 토큰도 줄어든다.
제한으로도 쓸 수 있다
여기 없는 Agent 는 돌지 않는다. 위젯이 건드릴 수 있는 범위를 요청 단위로 좁히는 셈이다.
순서를 서버에 맡기려면 — planWithin
구성은 매번 달라도 되고 범위만 고정하고 싶을 때가 있다.
{ agentIds: [...], planWithin: true }켜면 서버가 그 안에서 질문에 맞는 것만 골라 순서를 정한다. 대신 순서가 보장되지 않는다 — 화면 구성이 순서에 묶여 있다면 켜지 말 것. 후보 검색을 건너뛰므로 자동 선택보다는 빠르다.
사용자가 흐름을 고르게 하려면
관리 화면에 저장해 둔 흐름 목록은 useInit 이 함께 내려준다. 별도 호출이 없다. SDK 1.0.2+.
const { orchestrations } = useInit(config);
const [flowId, setFlowId] = useState<string | null>(null);
// 고른 값을 config 에 넣으면 그 흐름으로 답한다
const cfg = useMemo(() => ({ ...base, orchestrationId: flowId }), [flowId]);각 항목은 { id, name, description } 이다. description 은 관리 화면에 적은
"이 흐름이 무엇을 해주는지" 라 그대로 보여줘도 된다 — 이름만 있으면 사용자가 고를 수 없다.
목록이 비면 선택 UI 를 그리지 않는다. 구버전 서버는 이 값을 안 내려주고 등록된 흐름이 없을 수도 있는데, 빈 셀렉트만 남으면 사용자는 고장으로 읽는다. "자동" 을 목록 맨 위 기본으로 두는 편이 좋다 — 흐름을 고르는 것은 특수한 선택이지 기본이 아니다.
다른 값과의 관계
| 값 | 뜻 | 우선순위 |
|---|---|---|
| orchestrationId | 관리 화면에 저장해 둔 흐름 | 1 |
| agentIds | 이 요청에서 즉석으로 만드는 흐름 | 2 |
| agentId | Agent 하나로 단독 처리 | 3 |
| (없음) | 서버가 질문을 보고 계획 | 4 |
조합이 질문 수만큼 늘어나 미리 저장할 수 없는 자리에는 agentIds, 늘 같은 흐름으로 답해야
하는 자리에는 orchestrationId 가 맞다.
최대 8 개. 없거나 다른 테넌트의 Agent 가 섞이면 조용히 건너뛰지 않고 오류로 돌려준다 —
3 단계를 요청했는데 2 단계 답이 오면 어느 자리가 빠졌는지 알 방법이 없다.
core 1.7.0+ 필요(구버전은 이 값을 무시하고 자동 선택으로 떨어진다).
멀티 에이전트 진행 표시
질문 하나를 여러 Agent가 단계로 나눠 처리하면(오케스트레이션) 답변이 만들어지는 동안 화면에 아무 단서가 없다. STEPS 블록이 답변 맨 위에 진행 상황을 그린다.
멀티 에이전트 · 1 / 2 단계
✓ 1단계 자원정보 에이전트
◐ 2단계 활성분석 에이전트 진행 중
◐ 전략제안 에이전트 진행 중
○ 논문·특허 에이전트기본은 꺼져 있다. 진행 상황을 보여주고 싶은 자리가 앱마다 달라서(상단 고정·사이드·숨김) 0.5.4 부터 옵트인으로 바꿨다. 켜면 답변 맨 위에 붙고, 단계가 하나뿐이면(단일 Agent) 켜도 아무것도 렌더하지 않는다.
병렬 단계는 한 줄이 아니다
서버는 같은 번호에 놓인 Agent 를 동시에 돌린다. 그래서 한 단계에 줄이 여럿일 수 있고, 위 예에서 2단계는 Agent 셋이다.
1.0.3 부터 이 경우를 제대로 그린다.
- 세는 단위는 단계다. 분모가 단계 수인데 분자가 Agent 수면 완료 시
4 / 2 단계가 된다. 한 단계는 그 안의 Agent 가 전부 끝나야 끝난 것으로 센다. - 번호는 묶음의 첫 줄에만 찍는다. 셋 다
2단계라고 쓰면 단계가 셋인 것처럼 읽힌다. 나머지 줄은 번호 칸을 비워 이름이 세로로 정렬된다.
1.0.2 이하는 두 가지가 다 어긋나고, 같은 번호를 React key 로 써서 콘솔에 중복 key 경고가 뜬다. 오케스트레이션에 병렬 단계가 있다면 1.0.3 이상을 쓴다.
<StreamingChat config={config} showStepProgress />
// 또는
<BlockRenderer message={m} config={config} showStepProgress />카드를 안 켜도 진행 상황은 상태줄에 뜬다(status.step). 아래 참조.
블록이 만들어지는 경로는 둘인데 결과는 같다.
| 시점 | 출처 |
|------|------|
| 스트리밍 중 | orchestration_plan 으로 목록을 그리고, step_status 로 해당 줄만 갱신 |
| done · 대화 복원 | 본문 맨 앞의 ```aiplug-steps 펜스 (parseMarkdownBlocks 가 파싱) |
reduceEvent 는 본문에 펜스가 있으면 그쪽을 권위로 삼고 SSE 로 만든 블록을 버린다 — 카드가 두 번 그려지지 않는다. 대화 이력에서 불러온 메시지도 같은 파서를 타므로 스트리밍 화면과 복원 화면이 어긋나지 않는다.
단계 상태(OrchestrationStepStatus)는 서버와 1:1 이다.
| 상태 | 표시 |
|------|------|
| WAITING · APPROVED | ○ 흐리게 |
| RUNNING | ◐ 진행 중 |
| PENDING_CONFIRM | ! 승인 대기 — tool_confirm 과 같이 온다 |
| COMPLETED | ✓ |
| SKIPPED | – 건너뜀 |
| REJECTED · FAILED | × 중단됨 · 실패 |
색은 --aiplug-accent · --aiplug-ok · --aiplug-warn · --aiplug-error 를 쓴다(테마 참조). 표시 형식을 바꾸려면 components.STEPS 로 교체한다 — block.steps[].intent 에 각 단계의 작업 지시문이 들어 있어 툴팁·상세 뷰에 쓸 수 있다.
진행 표시줄에도 단계가 뜬다. status.thinking 은 첫 토큰이 오면 지워지지만 status.step 은
다음 단계로 넘어갈 때까지 남는다 — 그래서 4단계짜리 답변 내내 "답변 생성 중" 하나만 떠 있지 않다.
직접 UI 를 만든다면 status.thinking || status.step 순으로 고르면 된다.
본문의 단계 구분
멀티 에이전트 답변은 여러 Agent 가 쓴 글이 이어 붙어 한 덩어리로 온다. 어디서 담당이 바뀌었는지 보이지 않으면 "그냥 소제목이 하나 더 나온" 것처럼 읽힌다.
0.5.5 부터 서버가 단계 경계에 ```aiplug-step 마커를 내려보내고, BlockRenderer 가 그
마커를 기준으로 뒤따르는 블록들을 한 구간으로 묶어 좌측 세로선과 라벨을 붙인다.
│ 3단계 · 해양생물 종/자원정보 에이전트
│ 이번 단계의 분류·분포 정보만 기준으로 보면 …
│
│ 4단계 · MarineDB 특화 에이전트 ← 생성 중이면 강조색
│ ### 천연물·표적 연결성 검토
│ 현재 제공된 자료에서는 …마크다운 제목(###)만으로는 안 된다. 단계 제목과 단계 안의 소제목이 같은 층이라 구분이 안 되고,
LLM 이 제목 규칙을 안 지키면(번호 매기기 등) 경계가 아예 사라진다. 마커는 서버가 붙이므로
모델 출력과 무관하게 확정된다.
세로선을 쓰는 이유는 답변이 세로로 길기 때문이다 — 가로 구분선은 스크롤을 내리면 지금 어느 단계를 읽는지 알 수 없다. 대신 본문 왼쪽에 18px 정도가 들어간다.
마커가 없으면 예전처럼 평평하게 그린다. 단일 Agent 답변, 구버전 core 응답, 대화 이력에서 불러온 과거 메시지가 모두 여기 해당하므로 따로 분기할 필요가 없다.
렌더링을 바꾸려면 StepSection 을 직접 쓰거나, groupBySteps 대신 message.blocks 에서
type === 'STEP' 을 직접 다루면 된다.
질문 의도 정리
멀티 에이전트는 단계마다 다른 Agent 가 다른 자료를 보고 답한다. 정보량은 늘지만 답변이 원 질문에서 조금씩 미끄러져 "내가 물어본 게 이거였나" 싶어진다. 답변 맨 앞에 무엇에 답하는지 못박아, 의도가 잘못 잡혔으면 첫 문단에서 알아채게 한다.
질문 의도
감태를 포함한 국내 해조류 자원 중 화장품 원료로 쓸 수 있는 후보와 그 근거를 묻고 있습니다.표시용만이 아니다. 서버는 같은 문장을 모든 단계 입력의 기준점으로도 쓴다 — 전 단계가 같은 문장을 보고 있으면 미끄러지는 폭이 줄어든다. 그래서 이 옵션을 켜면 화면뿐 아니라 답변 내용 자체가 달라진다.
누가 그 문장을 쓸지는 config.intentSummary 로 정한다. 어느 값이든 LLM 호출은 늘지 않는다.
| 값 | 동작 |
|------|------|
| PLANNER (기본) | 플래너가 실행 계획을 짤 때 같이 쓴다. 모든 단계보다 먼저 확정돼 1단계부터 기준점이 선다 |
| FIRST_STEP | 1단계 Agent 가 본문 도입부로 쓴다. 플래너 프롬프트를 안 건드려 JSON 이 흔들리는 환경에서 안전하지만, 1단계 자신에겐 기준점이 없다 |
| OFF | 만들지 않는다. 답변 구조가 이전과 같아진다 |
const config: AIPlugClientConfig = { baseUrl, publicKey, tenantId, intentSummary: 'PLANNER' };서버 설정이 아니라 요청마다 보내므로 앱별로 다르게 둘 수 있다. PLANNER 는 별도 블록
(INTENT)으로, FIRST_STEP 은 1단계 본문 도입부로 나온다. 렌더러는 components.INTENT 로 교체한다.
실행 확인(confirm)
위험한 스킬은 실행 전에 사용자 승인을 받는다. 서버가 승인을 요구하는 기준은 둘이다 — 스킬에 실행 전 확인이 켜져 있거나, 위험도가 HIGH 이거나.
흐름
tool_confirm ──→ pending 상태 (message · toolNames · confirmToken)
│ 사용자가 진행/취소
confirm(action) ──→ 같은 confirmToken 을 서버로 되돌려줌
│
tool_start / tool_result ──→ 실행 → 답변 계속confirmToken — 반드시 되돌려줘야 한다
서버는 승인 대기 상태를 저장하지 않는다. 온프레미스에서 Valkey 를 올리지 못하는 환경이 있어, 대기 상태 전체를 암호화해 응답에 실어 보낸다. 이 토큰이 없으면 서버는 무엇을 승인하는지 알 수 없다.
useAIPlugChat · useAIPlug 를 쓰면 훅이 알아서 보관·전달하므로 앱이 할 일은 없다.
직접 스트림을 다루는 경우에만 챙기면 된다.
let token: string | null = null;
for await (const e of stream) {
if (e.type === 'tool_confirm') token = e.data.confirmToken ?? null;
}
// 사용자가 승인하면
createChatStream(config, '네, 진행해줘', {
conversationId,
confirmAction: 'APPROVED',
confirmToken: token, // ← 이 값이 없으면 chat.confirm.token_missing
});토큰은 10분 뒤 만료된다. 지나면 chat.confirm.expired 로 내려오며, 사용자는 질문을 다시 해야 한다.
⚠️
0.5.7은 core0.5.7+ 를 요구한다. 구버전 core 는confirmToken을 무시하고 서버 저장소(Valkey)에서 대기 상태를 찾으므로, 서버를 함께 올려야 한다.
tool_start — 실행 중 표시
tool_result 는 실행이 끝나야 온다. 조회가 오래 걸리는 스킬이면 그동안 아무 이벤트도 없어
화면이 멈춘 것처럼 보인다. 0.5.7 부터 실행 직전에 tool_start 가 먼저 오고,
스킬의 제한 시간이 함께 실려 "최대 n초" 를 표시할 수 있다.
| 필드 | 설명 |
|------|------|
| toolName | LLM 함수명 |
| displayName | 콘솔 표시명 — 사용자에게 보여줄 이름 |
| timeoutSeconds | 이 스킬의 제한 시간(초) |
| index · total | 한 번에 여러 스킬을 부를 때의 순번 |
이어서 물어보기
두 가지가 있다. 화면에서는 둘 다 눌러서 바로 보내는 버튼이고, 렌더러도 FollowUpChips 하나를 쓴다.
| | 추천 질문 | 후속 질문 |
|---|---|---|
| 언제 | 대화 시작 화면 | 답변 뒤 |
| 누가 | 관리자가 등록(고정) | LLM 이 생성(그때그때) |
| 경로 | useInit().sampleQuestions | FOLLOWUPS 블록 |
후속 질문
서버가 답변 끝에 펜스로 내려보내고 BlockRenderer 가 칩으로 그린다. onFollowUp 을 넘겨야 눌린다.
<BlockRenderer
message={message}
onFollowUp={busy ? undefined : send} // 생성 중에는 잠근다
busy={busy}
/>StreamingChat 은 내부에서 연결하므로 앱이 할 일이 없다. 낼 것이 없으면 서버가 펜스를 생략하므로
칩 영역도 나오지 않는다.
추천 질문
const { initMessage, sampleQuestions } = useInit(config);
{messages.length === 0 && sampleQuestions.length > 0 && (
<FollowUpChips block={{ type: 'FOLLOWUPS', items: sampleQuestions }} onSelect={send} />
)}서버 설정과 config.sampleQuestions 를 합쳐서 준다. 한쪽이 다른 쪽을 덮지 않는다 —
관리자가 등록한 질문은 그때그때 바뀌는 운영 항목이고, 앱에 넣어둔 질문은 그 서비스가 늘 답할 수
있는 기본 질문이라 성격이 다르다. 서버 것이 앞에 오고, 같은 문장은 한 번만 나온다.
둘 다 없으면 빈 배열이므로 length > 0 만 보면 된다. 관리 화면에 등록하기 전에도 앱에 적어둔
질문으로 먼저 쓸 수 있고, 나중에 서버 설정이 붙으면 앱 코드를 고치지 않아도 앞에 얹힌다.
키워드(keywords)
답변이 끝나면 서버가 본문에서 뽑은 키워드를 KEYWORDS 블록으로 보낸다. 기본 렌더러
KeywordChips 가 자동으로 칩을 그리므로 아무것도 안 해도 표시된다.
누를 수 있게 하려면 onKeywordSelect 를 넘긴다.
<StreamingChat
config={config}
onKeywordSelect={(k) => openGraphNode(k.text, k.type)}
/>기본 동작을 두지 않은 이유는, 키워드를 눌렀을 때 무엇이 일어나야 하는지가 앱마다
다르기 때문이다. 채팅으로 다시 묻게 하려면 (k) => send(k.text) 를 넘기면 된다.
interface KeywordEntity { text: string; type?: string; }type 은 개체 유형이다(종명·물질·지역 …). 고정 목록이 아니다 — 서버가 자료
성격에 맞춰 정하므로, 값을 비교해 분기하지 말고 문자열 그대로 보여줄 것. 모델이 분류를
정하지 못했거나 등록 태그로 채워진 키워드는 type 이 없다.
키워드가 하나도 없으면 블록 자체가 오지 않는다. 빈 칩 영역은 그려지지 않는다.
소급되지 않는다.
core 1.0.0이전에 쌓인 답변에는 키워드가 저장되어 있지 않아, 그 대화를 다시 열면 칩이 없다. 새 답변부터 정상 표시된다.
되묻기(clarify)
질문이 애매할 때 답변을 만들지 않고 멈춘다. 무엇이 빠졌는지 말하고 선택지를 준 뒤, 사용자가 고르면 그때 진행한다.
기간이 지정되지 않았습니다. 어느 기준으로 볼까요?
[ 최근 30일 ] [ 최근 90일 ] [ 올해 전체 ]
직접 입력 [ ] [보내기]후속 질문과 무엇이 다른가
겉모습은 비슷하지만 성격이 반대다.
| | 후속 질문 | 되묻기 |
|---|---|---|
| 언제 | 답을 다 준 뒤 | 답을 주기 전 |
| 무시하면 | 대화가 끝난 상태 | 대화가 멈춰 있음 |
| 본문 | 답변이 있다 | 답변이 없다 |
| 블록 | FOLLOWUPS | CLARIFY |
그래서 칩이 아니라 테두리 있는 카드로 그리고, 목록에 없는 답을 쓸 입력칸을 함께 둔다. 선택지가 전부 아닐 때 빠져나갈 길이 없으면 사용자가 갇힌다.
켜는 곳
에이전트 옵션 useClarify 다. 기본은 꺼져 있고, 끈 에이전트는 아무 비용도 들지 않는다.
되묻기는 사용자를 한 번 더 멈춰 세우는 대가로 정확도를 사는 거라, 조회 조건이 결과를 크게
바꾸는 업무에서만 값어치가 있다.
켜져 있어도 애매할 때만 발동한다. 조건이 분명한 질문은 그대로 진행하므로 매번 한 단계가 늘어나지는 않는다.
앱이 할 일 — confirmToken 을 되돌려준다
<BlockRenderer
message={message}
onFollowUp={busy ? undefined : send} // CLARIFY 도 같은 핸들러를 쓴다
busy={busy}
/>고른 값을 보낼 때 응답에 실려 온 confirmToken 을 함께 보내야 한다. 사용자가 보내는 건
"최근 30일" 한 마디뿐이라, 토큰이 없으면 서버는 무엇을 묻던 중이었는지 알 수 없다 —
그냥 새 질문으로 처리된다. 원래 질문은 토큰 안에 들어 있다.
StreamingChat 은 내부에서 연결하므로 앱이 할 일이 없다.
커스텀 렌더러
<BlockRenderer
message={message}
components={{ CLARIFY: MyClarifyCard }}
/>{ block, onSelect, disabled } 를 받는다. block.allowFreeText 가 false 면 직접 입력을 막는다 —
목록 밖 답이 의미 없는 질문에서만 서버가 그렇게 내려보낸다.
출처 목록
참고목록은 자료 종류별로 묶여 나온다. sourceType 만으로는 DB 출처가 전부 "DB" 한 덩어리라
논문 자료인지 자원 자료인지 구분이 안 되기 때문이다.
파일 · 2건
[1] 2024_해조류_활성연구.pdf ↓
[2] 감태_추출물_시험성적서.pdf ↓
해양생물 논문 초록 · 3건
[3] 감태 (Ecklonia cava)
...
스킬 · 1건
[6] 자원 재고 조회Reference 에 필드 두 개가 추가됐다.
| 필드 | 내용 |
|------|------|
| sourceGroup | 묶음 이름. FILE 은 "파일", DB 는 테이블 설명(등록 시 사람이 적어둔 값), TOOL 은 "스킬" |
| documentId | FILE 이면 파일 ID. 이 값이 있으면 원본을 내려받을 수 있다 |
분류명이 하나도 안 오면(구버전 core) 예전처럼 평평한 목록으로 떨어진다.
원본 파일 다운로드
파일 출처에는 ↓ 버튼이 붙는다. SDK 는 파일시스템에 접근할 수 없으므로 core 가 스트리밍한다.
publicKey 를 body 로 보내야 해서 <a href> 로 걸 수 없다(쿼리스트링에 실으면 액세스 로그·
브라우저 히스토리에 API 키가 남는다). blob 으로 받아 저장을 트리거하며, 한글 파일명은
Content-Disposition 의 filename*(RFC 5987)에서 복원한다.
StreamingChat 을 쓰면 그냥 동작한다. 커스텀 UI 라면 BlockRenderer 에 config 를 넘겨야
버튼이 나온다 — 안 넘기면 조용히 숨는다(에러 없이 버튼만 사라지므로 원인을 찾기 어렵다).
<BlockRenderer message={message} components={components} config={config} />Provider 로 감싸도 된다. 여러 블록에서 쓸 일이 생기면 이쪽이 낫다.
import { AIPlugConfigContext, BlockRenderer } from '@bluedus/aiplug-sdk/stream/react';
<AIPlugConfigContext.Provider value={config}>
<BlockRenderer message={message} />
</AIPlugConfigContext.Provider>직접 호출할 수도 있다.
import { saveRagFile, fetchRagFile } from '@bluedus/aiplug-sdk/stream';
await saveRagFile(config, ref.documentId, ref.title); // 저장까지 트리거
const { blob, fileName } = await fetchRagFile(config, ref.documentId); // blob 만복사 기능
import { CopyButton, BlockCopySlot, messageToText, blockToText }
from '@bluedus/aiplug-sdk/stream/react';
// 메시지 전체 복사 (본문만 — 출처/키워드 제외가 기본)
<CopyButton text={messageToText(message)} />
// 아이콘 형태 (버블 우상단 등)
<CopyButton variant="icon" text={messageToText(message)} disabled={isLoading} />
// 임의 블록에 hover 복사 아이콘 얹기
<BlockCopySlot text={blockToText(block)}>{children}</BlockCopySlot>| API | 설명 |
|------|------|
| messageToText(message, options?) | 메시지 → 평문. { includeReferences?, includeKeywords? } 둘 다 기본 false(본문만) |
| blockToText(block, options?) | 단일 블록 → 평문. 표는 마크다운 표로 직렬화 |
| CopyButton | text 필수. variant('button'|'icon'), label, disabled, onCopied |
| BlockCopySlot | 자식 블록 우상단에 hover 시 나타나는 복사 아이콘 래퍼 |
StreamingChat 은 버블 우상단(메시지 전체)과 표·긴 텍스트(200자 이상) 블록에 복사 아이콘을 자동 노출한다. CopyButton 은 비보안 컨텍스트(http)에서 Clipboard API 를 못 쓸 때 textarea + execCommand 로 폴백한다.
주요 API
@bluedus/aiplug-sdk/stream
| API | 설명 |
|------|------|
| createChatStream(config, options) | SSE 채팅 스트림 생성 |
| parseSseStream / parseSseChunk | SSE → 타입드 이벤트 |
| parseMarkdownBlocks(md, streaming?) | 마크다운 → ContentBlock[] (표·펜스·이미지 인식). streaming=true 면 미완 구조를 PENDING 으로 |
| reduceEvent / emptyAssistantMessage | 이벤트 → Message 누적 (토큰을 Message.raw 에 쌓고 매번 재파싱) |
| fetchModels(config) | 사용 가능 LLM 모델 목록 |
| fetchConversations / fetchConversationMessages | 대화 목록 · 복원 |
| saveRagFile(config, fileId, fallbackName?) | 출처 원본 파일을 받아 브라우저 저장 트리거 |
| fetchRagFile(config, fileId) | 출처 원본을 { blob, fileName } 으로만 반환(저장은 앱이 처리) |
| submitFeedback(input) | 피드백 전송 |
| messageToText / blockToText | 복사·공유용 평문 변환 |
@bluedus/aiplug-sdk/stream/react
| API | 설명 |
|------|------|
| useAIPlugChat(config) | { messages, status, isLoading, pending, feedback, send, loadConversation, confirm, acceptConsent, stop, clear } |
| StreamingChat | 완성형 드롭인 채팅 UI |
| BlockRenderer | message.blocks → 컴포넌트 매핑. props: components(블록별 오버라이드), config(출처 다운로드용), views(표 행 클릭 대상), resolveRowAction(대상 보충), onFollowUp·busy(후속 질문 칩), showStepProgress(기본 false) |
| useTableRows(block) | 커스텀 TABLE 렌더러용. { headers, rows, getRowAction(i) } — 행 클릭 배선을 다시 짜지 않아도 된다 |
| validateViews / createRowHandler | views 검증 · 대상 → 실행 함수 (직접 스트림을 다룰 때) |
| ViewsContext / useViews | 등록된 화면 목록 컨텍스트. BlockRenderer 가 자동 제공 |
| CitationText / ReferenceList / KeywordChips | 기본 블록 렌더러 |
| BlockSkeleton | 생성 중(PENDING) 블록 셔머 스켈레톤 — BlockRenderer 기본값 |
| StepProgress | 멀티 에이전트 단계 진행(STEPS) 렌더러 — showStepProgress 를 켤 때만. 단계 1개면 렌더 안 함 |
| IntentSummary | 질문 의도 정리(INTENT) 렌더러 — BlockRenderer 기본값 |
| StepSection | 단계 구간 래퍼(좌측 세로선 + 라벨). BlockRenderer 가 STEP 마커 기준으로 자동 적용 |
| FeedbackBar / FeedbackContext / useFeedback | 피드백 UI · 컨텍스트 |
| AIPlugConfigContext / useAIPlugConfig | 블록 렌더러가 서버를 직접 부를 때 쓰는 config 컨텍스트(출처 다운로드). StreamingChat 은 자동 제공 |
| CopyButton / BlockCopySlot | 복사 UI |
| useInit / useModels / useConversations | 초기화 · 모델 · 대화 이력 |
| useAutoScroll / AutoScrollArea | 새 메시지 시 하단 이동(사용자가 위에서 읽는 중엔 낚아채지 않음) |
@bluedus/aiplug-sdk/leaflet · /echarts
| API | 설명 |
|------|------|
| LeafletMapBlock | MAP 블록 렌더러 (leaflet·CSS 모두 SDK 가 챙긴다) |
| EChartsChartBlock | CHART 블록 렌더러 (15종 차트, 검증된 팔레트 내장) |
| buildOption(block, palette) | CHART spec → 완전한 ECharts option 보정(커스텀 렌더러용) |
| ensureLeafletCss() | leaflet.css 수동 주입(1회 보장). 주입했으면 true |
| LEAFLET_CSS / LEAFLET_CSS_VERSION | 인라인된 CSS 원문 · 원본 leaflet 버전 |
두 라이브러리는 SDK 의존성이라 자동 설치된다. 어떤 이유로 해석에 실패해도 렌더러는 오류로 죽지 않고 안내 박스를 표시한다.
테마 (CSS 변수)
컴포넌트는 인라인 스타일 + CSS 변수 폴백을 쓴다. 앱에서 --aiplug-* 를 정의하면 전체 톤이 바뀐다.
:root {
--aiplug-bg: #0f172a;
--aiplug-fg: #e2e8f0;
--aiplug-muted: #94a3b8;
--aiplug-border: rgba(148,163,184,0.2);
--aiplug-card-bg: rgba(148,163,184,0.06);
--aiplug-input-bg: rgba(148,163,184,0.08);
--aiplug-accent: #6366f1;
--aiplug-accent-bg: rgba(99,102,241,0.12);
--aiplug-accent-border: rgba(129,140,248,0.45);
--aiplug-error: #ef4444;
--aiplug-error-bg: #7f1d1d;
--aiplug-error-fg: #fee2e2;
/* 인용 칩 */
--aiplug-cite-fg: #cbd5e1;
--aiplug-cite-bg: rgba(56,189,248,0.14);
--aiplug-cite-border: rgba(56,189,248,0.45);
/* 생성 중 스켈레톤 셔머 */
--aiplug-skeleton-base: rgba(148,163,184,0.10);
--aiplug-skeleton-hi: rgba(148,163,184,0.22);
}Next.js (App Router) 주의
React 컴포넌트·훅은 클라이언트 전용이다. 사용하는 파일 최상단에 'use client' 를 선언할 것.
'use client';
import { StreamingChat } from '@bluedus/aiplug-sdk/stream/react';빌드 시 "use client" ... was ignored 경고가 뜨는데, 번들러가 모듈 레벨 지시어를 제거하면서 나는 것으로 소비 앱이 위처럼 선언하면 문제되지 않는다.
서버 요구사항
Core 서버가 아래를 제공해야 한다. 버전별 필요 사항은 버전 이력 참조.
| 엔드포인트 | 용도 |
|------|------|
| POST /api/v1/chat | SSE 채팅 스트림 |
| POST /api/v1/chat/init | 초기화(테넌트 인사말 등) |
| POST /api/v1/models | 모델 목록 |
| POST /api/v1/conversations · /messages | 대화 목록 · 복원 |
| POST /api/v1/feedback | 피드백 |
| POST /api/v1/rag/files/download | 출처 원본 파일 다운로드 (0.5.3+) |
POST /api/v1/chat/init 응답의 orchestrations 가 useInit().orchestrations 로 온다 (1.7.0+). 구버전은 이 필드가 없어 빈 배열이 된다.
agentIds·planWithin·orchestrationId 는 POST /api/v1/chat 본문에 실린다. 구버전 core 는 모르는 필드를 무시하므로 그대로 자동 선택으로 동작한다 (1.7.0+ 필요).
SSE 이벤트: metadata · thinking · token · references · keywords · tool_confirm · tool_start · tool_result · orchestration_plan · step_status · done · blocked · error. 모르는 이벤트는 무시하므로 구버전 서버와도 동작한다(해당 기능만 비활성).
멀티 에이전트 진행 표시를 쓰려면 core 가 아래 두 이벤트를 보내야 한다. 안 보내도 나머지는 정상 동작한다.
event: orchestration_plan
{"totalSteps":3,"steps":[{"order":1,"agentId":"…","agentName":"자원정보 에이전트","intent":"…","status":"WAITING"}, …]}
event: step_status
{"order":2,"totalSteps":3,"agentId":"…","agentName":"활성분석 에이전트","status":"RUNNING"}서버는 마크다운 원문만 보낸다. 표·차트·지도 인식은 전부 SDK(parseMarkdownBlocks)가 하므로
content_block·block_pending 같은 블록 이벤트는 더 이상 쓰지 않는다. 파서가 SDK 한 곳뿐이라
스트리밍 중 화면과 완료 후 화면이 구조적으로 어긋날 수 없다.
core 는 아래 형태로 내려주면 된다 — 표는 표준 마크다운 표, 차트·지도는 펜스 코드블록:
| 컬럼1 | 컬럼2 |
| --- | --- |
| 값1 | 값2 |
```aiplug-chart
{"chartType":"bar","title":"제목","xAxis":{"data":["A","B"]},"series":[{"name":"계열","data":[1,2]}]}
```
```aiplug-map
{"title":"제목","geojson":{"type":"FeatureCollection","features":[…]}}
```멀티 에이전트 응답이면 각 단계 본문 앞에 경계 마커가 붙는다. 이게 있어야 SDK 가 담당이 바뀌는 지점을 알 수 있다. 단계가 하나뿐이면 붙이지 않는다.
```aiplug-step
{"order":4,"totalSteps":5,"agentName":"MarineDB 특화 에이전트"}
```그리고 본문 맨 앞에 진행 상황 펜스가 하나 더 붙는다. 대화를 다시 열었을 때 카드를 복원하는 용도라 저장 본문에 남아 있어야 한다(SSE 는 스트리밍이 끝나면 사라진다).
```aiplug-steps
{"totalSteps":3,"steps":[{"order":1,"agentName":"자원정보 에이전트","status":"COMPLETED"}, …]}
```aiplug-sources 펜스(출처 정리)와 aiplug-image 펜스(이미지 생성)는 서버가 소비·치환하므로 화면에 렌더되지 않는다.
Props 참조 (AgentChat)
| Prop | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| baseUrl | string | 필수 | AIPlug Core 서버 URL |
| publicKey | string | 필수 | 테넌트 Public Key |
| tenantId | string | 필수 | 테넌트 ID |
| agentId | string \| null | null | Agent ID (null이면 자동 선택) |
| userId | string \| null | null | 사용자 식별자 (로그용) |
| title | string | 'AI Agent' | 헤더 타이틀 |
| placeholder | string | '메시지를 입력하세요...' | 입력창 placeholder |
| welcomeMessage | string | — | 첫 안내 메시지 |
| height | string | '560px' | 컴포넌트 높이 |
| width | string | '400px' | 컴포넌트 너비 |
| dark | boolean | false | 다크 테마 |
| showSources | boolean | true | RAG 출처 표시 |
| showToolCalls | boolean | true | Tool 실행 상태 표시 |
| streaming | boolean | true | ⚠️ false 사용 불가 — 동기 방식은 /api/v2/chat 을 호출하는데 Core에서 제거됨(404). 항상 true 로 둘 것 |
AgentChat/useAIPlug/AIPlugClient/ChatSession은 레거시입니다. 신규 개발은stream계열(StreamingChat,useAIPlugChat)을 사용하세요.
Props 참조 (StreamingChat)
| Prop | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| config | AIPlugClientConfig | 필수 | { baseUrl, publicKey, tenantId, agentId?, agentIds?, planWithin?, orchestrationId?, userId? } |
| components | BlockComponentOverrides | — | 블록 타입별 컴포넌트 오버라이드 |
| placeholder | string | '메시지를 입력하세요' | 입력창 placeholder |
| emptyHint | string | — | 메시지 없을 때 안내 문구 |
| className | string | — | 루트 컨테이너 클래스 |
| showStepProgress | boolean | false | 멀티 에이전트 진행 카드 표시 |
| views | ViewRegistry | — | 표 행 클릭으로 열 화면 목록. 등록한 것만 열린다 |
| resolveRowAction | RowActionResolver | — | 행 대상 보충·재정의. 규약이 어긋났을 때 앱의 도메인 지식으로 채운다 |
버전 이력
| 버전 | 변경 내용 |
|------|-----------|
| 1.0.3 | 병렬 단계가 있는 진행 카드 수정. 서버는 같은 번호에 놓인 Agent 를 동시에 돌리는데, 카드는 단계 하나에 Agent 하나를 전제하고 있었다. (1) 세는 단위 — 분모는 단계 수인데 분자로 Agent 수를 세고 있었다. Agent 넷이 2단계에 걸쳐 있으면 완료 시 4 / 2 단계 가 떴다. 같은 order 끼리 묶어 세고, 한 단계는 그 안의 Agent 가 전부 끝나야 끝난 것으로 본다. (2) React key — order 만 key 로 써서 병렬 줄끼리 충돌했고, 콘솔에 중복 key 경고가 났다. order 와 agentId 를 합친다. (3) 번호 표기 — 병렬 셋에 2단계 가 세 번 찍히면 단계가 셋인 것처럼 읽힌다. 번호는 묶음의 첫 줄에만 쓰고 나머지는 칸을 비워 이름이 세로로 정렬되게 했다. 병렬 단계가 없는 흐름은 보이는 결과가 이전과 같다. 서버 요구사항 없음 — orchestration_plan 은 원래부터 같은 번호를 여럿 실어 보내고 있었다. |
| 1.0.2 | 흐름 목록 조회(useInit().orchestrations). 관리 화면에 저장해 둔 흐름 목록을 준다. 그동안 붙이는 앱이 흐름을 고르게 하려 해도 목록을 가져올 방법이 없었다 — 관리 API 는 쿠키 인증이라 publicKey 만 든 앱이 못 부른다. 새 엔드포인트를 만드는 대신 /chat/init 응답에 실었다. 이 호출은 화면을 열 때 어차피 한 번 나가므로 호출이 늘지 않고 추가 훅도 필요 없다. { id, name, description } 이며 단계 구성은 오지 않는다(사용자에게 보여줄 값이 아니고, 실려 있으면 관리자가 흐름을 고친 순간 앱이 든 정보가 낡는다). 구버전 서버는 이 필드를 안 주므로 빈 배열이 된다. (1.0.1 의 agentIds 가 "요청마다 구성을 앱이 정하는" 길이라면, 이쪽은 관리 화면에 저장해 둔 흐름을 사용자가 고르게 하는 길이다. 앞의 것은 화면 영역마다 담당이 고정된 자리에, 뒤의 것은 사용자가 답변 방식을 바꿔 볼 수 있는 자리에 맞는다.) |
| 1.0.1 | 요청 단위 Agent 지정(agentIds·planWithin). 기본은 서버가 질문을 보고 Agent 를 고르는데, 화면 영역마다 다룰 내용이 정해져 있는 자리에서는 그 결정을 앱이 하고 싶다. 그렇다고 저장된 흐름(orchestrationId)으로 만들 수도 없다 — "미역 논문"은 종 정보+논문+효능, "갯지렁이 분양"은 종 정보+절차+보유 현황처럼 조합이 질문 수만큼 늘어나 미리 등록해 둘 수가 없다. 그동안은 Agent 마다 따로 호출해 앱이 이어 붙였고, 왕복과 토큰이 그만큼 들었다. (1) config.agentIds — 배열 순서대로 실행되고 앞 단계 답변이 다음 단계로 전달된다. 한 번의 호출로 끝난다. 여기 없는 Agent 는 돌지 않으므로 범위 제한으로도 쓸 수 있다. (2) config.planWithin — 같은 배열을 순서가 아니라 후보 목록으로 읽는다. 범위는 좁히되 구성은 서버에 맡길 때 쓰며, 대신 순서가 보장되지 않는다. 후보 검색을 건너뛰어 자동 선택보다 빠르다. 두 의미를 한 값에 담으면 반드시 어긋나므로 플래그로 갈랐다. (3) 우선순위 — orchestrationId > agentIds > agentId > 자동. 여러 개를 지정한 것이 하나를 지정한 것보다 구체적인 요구라고 본다. (4) 최대 8 개이며, 없거나 다른 테넌트의 Agent 가 섞이면 조용히 건너뛰지 않고 오류다 — 3 단계를 요청했는데 2 단계 답이 오면 어느 자리가 비었는지 알 방법이 없고, 앱은 그것도 모른 채 화면을 구성한다. core 1.7.0+ 필요(구버전은 값을 무시하고 자동 선택으로 떨어진다). |
| 1.0.0 | 키워드 칩 실동작 · 유형 구분 · 대화 복원. KEYWORDS 블록은 0.2.0부터 타입·파서·렌더러가 다 있었지만 서버가 그 이벤트를 한 번도 보내지 않았다 — 문서에는 제공된다고 적혀 있어, 붙이는 쪽에서 답변 본문을 긁어 자체 추출하는 일이 벌어졌다. core 가 이제 실제로 보낸다. (1) KeywordEntity·entities 추가 — { text, type }. type 은 개체 유형(종명·물질·지역 …)이며 고정 목록이 아니다. AI-PLUG 는 테넌트마다 도메인이 달라서 목록을 코어에 박으면 다른 테넌트에서 전부 어긋난다. 서버가 자료 성격에 맞는 짧은 분류명을 정하고 화면은 그대로 보여준다 — 값을 비교해 분기하지 말 것. 기존 items(문자열 배열)는 그대로 두고 나란히 보낸다. 서버와 SDK 버전이 항상 같이 올라가지 않으므로 양쪽 다 읽히는 구간이 필요하다. (2) onKeywordSelect prop — StreamingChat·BlockRenderer. 후속 질문 칩과 달리 기본 동작을 정하지 않았다. 키워드를 누르면 무엇이 일어나야 하는지가 앱마다 다르다(지식그래프 노드 열기 / 재검색 / 재질문). 채팅으로 다시 묻게 하려면 (k) => send(k.text) 를 넘긴다. 안 넘기면 칩은 보이되 눌리지 않는다. (3) 대화 복원 지원 — 키워드는 본문 마크다운 밖으로 오므로 저장하지 않으면 다시 열었을 때만 사라진다. 실시간엔 보이는데 복원하면 없으면 사용자에게는 고장으로 읽힌다. core 가 ap_conv_message.keywords 에 저장하고 복원 응답에 싣는다. 소급되지 않는다 — 그 전에 쌓인 답변은 계속 비어 있고, 그때는 칩을 아예 그리지 않는다. core 1.0.0+ 필요(구버전 서버면 기능만 비활성). |
| 0.7.1 | 본문 링크 표시 정리. 답변에 주소가 그대로 박히면 한 줄을 통째로 차지해 문단을 읽을 수 없다(실측: 쿼리까지 96자인 URL 이 한 답변에 두 번). (1) 평문 URL 은 칩으로 — 도메인/…/마지막조각 으로 줄여 보여주고 테두리·아이콘을 붙인다. href 는 원본 그대로이고 전체 주소는 title 로 남는다. 45자 이하는 손대지 않는다. 쿼리스트링을 버리는 게 핵심이다 — 길이의 대부분을 차지하면서 사람에게 아무 정보도 주지 않는다. (2) 레이블 링크([제44조 원문](url))는 밑줄 인라인 유지 — 글쓴이가 문장의 일부로 쓴 것이라 문장에 녹아드는 편이 읽기 좋고, 표 셀에서도 행마다 무게가 실리지 않는다. 정상 동작에서는 칩을 볼 일이 없으므로 칩이 보이면 그 자체가 신호다. (3) 링크 색을 --aiplug-cite-fg 로 뺐다 — 하드코딩 보라색이라 라이트 테마에서 떴다. 아이콘은 인라인 SVG 다(아이콘 폰트는 로드 전까지 리거처 글자가 그대로 보인다). |
| 0.7.0 | 행 연결을 양방향으로 — 앱이 보충하는 자리(resolveRowAction). 0.6.x 는 표가 규약(_view·_id)대로 와야만 동작했는데, 프롬프트는 테넌트가 콘솔에서 직접 쓴다. SDK 가 특정 형식을 요구하면 그 순간 플랫폼으로서 실패다 — 실측에서 컬럼 이름이 다르고(_modalKey), 값이 - 로 채워지고, 모달에 필요한 _siteId 가 프롬프트에서 아예 빠지는 일이 모두 나왔다. (1) rowMeta 보존 — 숨김 컬럼 값을 TableContentBlock.rowMeta 에 항상 남긴다. 예전에는 화면 키를 못 찾으면 rowActions 가 통째로 null 이 되면서 _id 까지 같이 사라져, 앱이 보충하려 해도 재료가 없었다. 이제 _view 가 비어도 나머지 값은 살아남는다. (2) resolveRowAction prop — (action, ctx) => RowAction | null. 파서가 만든 대상을 앱이 보충하거나 재정의한다. ctx 는 { headers, row, meta, rowIndex, title }. "이 표는 민원 표다" 같은 판단은 도메인 지식이라 범용 SDK 에 넣을 수 없으므로 앱이 채운다 — 메커니즘은 SDK, 지식은 앱. StreamingChat·BlockRenderer 둘 다 받고, useTableRows 를 쓰는 커스텀 렌더러에도 자동 적용된다. (3) resolveRowAction export 를 createRowHandler 로 개명 — 같은 이름의 prop 이 생겨 뜻이 겹쳤다. 하는 일(대상 → 실행 함수)에 맞춘 이름이다. ViewsContext 의 값 형태도 { views, resolve } 로 바뀌었다. 둘 다 0.6.0 에서 하루 전 추가된 것이라 영향 범위가 없다고 보고 별칭을 남기지 않았다 — 직접 쓰고 있었다면 이름만 바꾸면 된다. |
| 0.6.1 | 화면 키 별칭 확대 · "값 없음" 표기 처리. 0.6.0 은 _view·_type 만 화면 키로 봤는데, LLM 은 지시받은 컬럼 이름을 그대로 쓰지 않는다 — _type 으로 지시했는데 _modalKey 로 낸 사례가 실측에서 나왔고, 그러면 파서가 컬럼은 숨기면서 rowActions 는 못 만들어 표의 모든 행이 클릭 불가가 된다(식별자는 사라지고 클릭도 안 되는, 안 고친 것만 못한 상태). _modalkey·_viewkey 를 별칭에 추가하고 앞선 것이 이기도록 했다. 아울러 값이 -·—·N/A·없음 이면 "값 없음" 으로 보고 그 행만 클릭을 끈다 — LLM 이 빈 칸 대신 이런 표기를 채우는 일이 잦은데, 그대로 두면 등록되지 않은 화면을 가리켜 조용히 실패한다. 화면 키 후보 컬럼은 파라미터에서도 제외해 modalKey 같은 값이 엉뚱한 파라미터로 섞이지 않는다. |
| 0.6.0 | 표 행 클릭 → 상세 화면 연결(views). 조회 결과를 표로 보여주고 나면 "이 행을 눌러 상세를 보고 싶다" 가 곧바로 따라오는데, SDK 에 액션 개념이 없어 앱마다 직접 만들어야 했다. 실제로 한 앱은 숨김 컬럼 파싱·화이트리스트 검증·Context 배선에 250줄을 썼다 — 표 디자인 하나 바꾸려다 배선까지 전부 새로 짜게 되는 구조였다. (1) views 등록 — StreamingChat·BlockRenderer 에 views prop 추가. 앱은 { label, description, params, open } 만 선언하고, 파싱·검증·클릭 배선은 SDK 가 맡는다. LLM 은 화면을 여는 코드를 만들 수 없으므로(응답은 정적 산출물이고 클릭 이후는 브라우저 코드만 할 수 있다) "무엇을 열지" 선언만 하고 실행 권한은 등록된 open 에만 둔다. 등록하지 않으면 표는 예전처럼 정적으로 그려진다. (2) 숨김 컬럼 규약 — 마크다운 표의 _ 접두 컬럼(_view·_id·_siteId …)을 파서가 headers/rows 에서 빼내 TableContentBlock.rowActions 로 옮긴다. LLM 이 낼 수 있는 건 마크다운 표뿐이라 식별자를 실을 자리가 셀밖에 없는데, 그대로 두면 UUID 가 화면에 노출된다. _type 은 별칭으로 받는다(규약 이전 앱 하위호환). 숨김 컬럼이 없으면 rowActions 자체가 없어 기존 표 렌더는 완전히 동일하다. (3) 이중 검증 — 등록 시점에 키 규약·open 타입을 검사해 개발 모드에서 무엇이 틀렸는지 찍고(조용히 넘어가면 "표는 나오는데 클릭만 안 되는" 상태가 되어 원인을 못 찾는다), 클릭 시점에 미등록 화면·형식 불일치·필수 파라미터 누락을 걸러 유령 클릭을 구조적으로 막는다. LLM 이 UUID 를 옮겨 적다 틀리는 전사 오류도 type: 'uuid' 로 잡힌다. (4) useTableRows 훅 — components.TABLE 로 디자인을 갈아끼워도 행 클릭 배선을 다시 짜지 않는다. 커스텀은 스타일만의 문제여야 한다. (5) 타입 export 확대 — RowAction·ViewDefinition·ViewParamSpec·ViewRegistry 와 블록 타입(TableContentBlock 등)을 루트에서 내보낸다(그동안 앱이 React.ComponentProps<...> 로 역산했다). 서버 버전 무관 — 표 파싱은 SDK 가 하므로 어느 core 에서도 동작한다. 다만 숨김 컬럼을 스킬 응답이 실제로 내려줘야 하고, LLM 이 그 컬럼을 표에 유지하도록 지시가 필요하다. 서버 기본 프롬프트(TABLE_GUIDE)에 규약이 반영된 빌드면 자동이고, 아니면 에이전트 프롬프트에 직접 적으면 된다. |
| 0.5.11 | react 오류 수정
| 0.5.10 | 재질문 스타일변경
| 0.5.9 | 이어서 물어볼 질문 · 추천 질문. (1) 후속 질문 칩(FOLLOWUPS 블록) — 예전에는 LLM 이 본문에 "~도 알려드릴까요?" 라고 썼다. 형식상 붙는 문장이라 매번 나왔고, 누를 것이 없어 물어보려면 사용자가 그 문장을 다시 타이핑해야 했다. 이제 서버가 답변 끝에 ```aiplug-followups 펜스(문자열 배열)로 내고, FollowUpChips 가 눌러서 바로 보내는 버튼으로 그린다. 다른 펜스 블록과 같은 경로라 대화를 다시 열어도 같은 칩이 복원된다. BlockRenderer 에 onFollowUp(보통 훅의 send)·busy prop 추가 — 안 넘기면 칩이 보이되 눌리지 않는다. 복사(blockToText)에서는 제외한다. components.FOLLOWUPS 로 교체 가능. (2) 추천 질문(useInit().sampleQuestions) — 대화 시작 화면은 백지라 무엇을 물어볼 수 있는지 알기 어렵다. 관리 화면(기본 프롬프트)에 등록한 목록을 /chat/init 이 내려주고, config.sampleQuestions 와 합쳐서 준다(서버 것이 앞, 중복 제거). 한쪽이 다른 쪽을 덮지 않는다 — 등록분은 지금 밀고 싶은 질문이고 앱 값은 늘 답할 수 있는 기본이라 성격이 다르다. 서버 컬럼이 없어도 앱에 하드코딩해 먼저 쓸 수 있다. 초기 화면에는 FollowUpChips 를 그대로 재사용하면 된다. core 0.5.9+ 에서 서버 설정이 동작한다(구버전은 필드를 안 내려주므로 앱 값 유지). |
| 0.5.8 | 스킬 사용 승인 카드로 변경
| 0.5.7 | 실행 확인 토큰(confirmToken) · 실행 중 표시(tool_start). (1) confirmToken — 승인 대기 상태를 서버에 저장하지 않고 암호화 토큰으로 왕복시킨다. 온프레미스에서 Valkey 를 올리지 못하는 환경이 있어 통신만으로 풀도록 바꿨고, 그 과정에서 승인이 특정 요청에 묶이게 됐다(예전에는 confirmId 를 내려주기만 하고 검증하지 않아, 그 대화에 APPROVED 만 보내면 대기 중인 스킬이 실행됐다). PendingState.confirmToken 추가, confirm(action) 이 자동으로 되돌려준다 — 훅을 쓰면 앱 수정은 필요 없다. 직접 스트림을 다루면 createChatStream({ confirmToken }) 로 넘길 것. 만료(10분)는 chat.confirm.expired, 누락은 chat.confirm.token_missing. core 0.5.7+ 필요 — 구버전 서버는 토큰을 무시하고 Valkey 를 찾는다. (2) tool_start 이벤트 — 스킬 실행 직전에 displayName·timeoutSeconds 와 함께 전송. tool_result 는 실행이 끝나야 와서, 오래 걸리는 조회 중에는 화면이 멈춘 것처럼 보였다. (3) 확인 카드 개선(AgentChat) — 답변 본문과 같은 모양이라 읽고 넘어가기 쉬웠던 승인 요청을 경고 박스로 분리하고(제목 + 테두리), 진행 버튼만 채워 어디를 눌러야 하는지 보이게 했다. 서버 문구도 위험도에 맞게 갈린다 — 읽기 전용 조회에까지 "외부 시스템에 영향을 줄 수 있습니다"가 붙던 것을 고쳤다. |
| 0.5.6 | conversation 추가된대화 active설정
| 0.5.5 | 본문의 단계 구분 렌더링. 멀티 에이전트 답변은 여러 Agent 가 쓴 글이 이어 붙어 오는데, 어디서 담당이 바뀌었는지 보이지 않아 "소제목이 하나 더 나온" 것처럼 읽혔다. 단계 제목과 단계 안의 소제목이 둘 다 ### 라 층이 구분되지 않고, LLM 이 제목 규칙을 안 지키면 경계가 아예 사라지는 것이 원인이었다. 서버가 단계 경계에 ```aiplug-step 마커를 내려보내고, BlockRenderer 가 그 마커 기준으로 뒤따르는 블록들을 한 구간으로 묶어 좌측 세로선과 라벨을 붙인다(StepSection). 블록을 1:1 로 그리던 것을 구간 단위로 바꾼 것이라 기존 렌더 경로가 달라졌지만, 마커가 없으면 그룹 하나만 나와 예전과 동일하게 그린다 — 단일 Agent 답변·구버전 core·대화 복원 중 과거 메시지가 모두 여기 해당한다. 스트리밍 중에는 마지막 구간이 강조색으로 표시돼 지금 어느 Agent 가 쓰는지 보인다. core 0.5.5+ 필요(마커 미지원 서버면 구분선만 안 나옴). |
| 0.5.4 | 질문 의도 정리 · 진행 카드 옵트인 전환 · 커스텀 UI 다운로드 수정. (1) 질문 의도 정리 — config.intentSummary(PLANNER|FIRST_STEP|OFF, 기본 PLANNER). 멀티 에이전트는 단계마다 다른 Agent 가 다른 자료를 봐서 답변이 원 질문에서 미끄러지는데, 무엇에 답하는지 맨 앞에 못박고 서버가 같은 문장을 모든 단계 입력의 기준점으로도 쓴다. 신규 INTENT 블록 + IntentSummary 렌더러(components.INTENT 로 교체 가능). 어느 값이든 LLM 호출은 늘지 않는다. (2) showStepProgress 옵트인 — 진행 카드가 0.5.2~0.5.3 에서는 항상 나왔으나 기본 false 로 바뀌었다. 이전처럼 보이게 하려면 showStepProgress 를 켤 것. 보여줄 자리가 앱마다 달라 선택으로 돌렸고, 카드를 꺼도 진행 상황은 상태줄(status.step)에 남는다. (3) BlockRenderer 에 config prop 추가 — 커스텀 UI 에서 출처 다운로드 버튼이 안 나오던 문제. 컨텍스트가 없으면 버튼이 조용히 숨어 원인을 찾기 어려웠다. Provider 로 감싸는 대신 prop 하나로 해결된다. (4) 서버 쪽 변경(단계 요약 인계, 테넌트별 RAG 범위 정책, 이전 턴 이력 주입)은 SDK 수정이 필요 없다. |
| 0.5.3 | 출처 분류 · 원본 파일 다운로드 · 멀티 에이전트 진행 라벨 유지. (1) 출처 분류 — Reference 에 sourceGroup 추가. 참고목록이 "파일 / 테이블 설명 / 스킬" 로 묶여 나온다. sourceType 만으로는 DB 출처가 전부 한 덩어리라 논문 자료인지 자원 자료인지 구분이 안 됐다. 분류명은 등록 시 사람이 적어둔 값(테이블 설명)이라 서버가 추론하지 않으므로 오분류가 없다. 분류명이 없으면(구버전 core) 평평한 목록으로 폴백. (2) 원본 다운로드 — Reference.documentId 가 있는 파일 출처에 ↓ 버튼. saveRagFile / fetchRagFile 로 직접 호출도 가능하다. publicKey 를 body 로 보내야 해 a[href] 대신 blob 으로 받으며, 한글 파일명은 filename*(RFC 5987)에서 복원한다. 블록 렌더러가 config 를 알아야 해서 AIPlugConfigContext 를 추가했다 — StreamingChat 은 자동 제공, BlockRenderer 단독 사용 시 컨텍스트가 없으면 버튼만 숨는다. core 0.5.3+ 필요(POST /api/v1/rag/files/download). (3) 진행 라벨 유지 — reducer 가 첫 토큰에 status.thinking 을 지워서 4단계짜리 오케스트레이션 답변 내내 "답변 생성 중" 하나만 떠 있던 문제. 단계 전이에서만 갱신되는 status.step 을 따로 둬 다음 단계까지 라벨이 남는다. |
| 0.5.2 | 멀티 에이전트 진행 표시(STEPS 블록) 추가. 여러 Agent가 단계로 나눠 답을 이어 쓸 때 어느 단계까지 왔고 누가 맡고 있는지 답변 맨 위에 그린다. 스트리밍 중에는 orchestration_plan·step_status SSE 로, done·대화 복원 시에는 본문의 ```aiplug-steps 펜스로 같은 블록이 만들어져 두 화면이 어긋나지 않는다(둘 다 있으면 펜스가 권위, 중복 렌더 없음). 기본 렌더러 StepProgress 가 자동으로 붙고 단계가 1개면 아무것도 그리지 않으므로 단일 Agent 대화에는 영향이 없다. components.STEPS 로 교체 가능. 구버전 core 는 두 이벤트를 안 보내므로 기능만 비활성된다. |
| 0.5.0 | 분류체계·관계망 차트 2종 추가 (13종 → 15종). (1) tree — 계통수(dendrogram). treemap·sunburst 와 같은 {name, children} 을 받지만 보여주는 것이 다르다: 비중이면 treemap/sunburst, 갈래면 tree. 문>강>목>과>속>종 같은 분류체계에 쓴다. 루트가 여럿이면 rootName 으로 묶고, 루트·잎 라벨의 실제 글자 폭으로 좌우 여백을 잡는다 (퍼센트로 두면 컨테이너가 좁을 때 루트 이름이 통째로 잘린다). 좁은 화면에서는 확대·이동으로 읽는다. (2) graph — force 레이아웃 관계망. 생물↔화합물↔타겟↔질병처럼 다대다 연결이 핵심일 때 쓴다. 연결 수에 비례한 노드 크기, 노드 수에 따른 반발력 자동 조절, category 문자열→범례 자동 변환, 40개 초과 시 라벨 숨김. 계층 데이터에 graph 를 쓰면 실뭉치가 되므로 tree 를 쓸 것. |
| 0.4.2 | 출처 그룹핑·재넘버링 고도화 — 파일/DB 중복 출처 병합, 멀티 에이전트 오케스트레이션 출처 반영, 스트리밍 중 인용번호 깜빡임 수정 |
| 0.4.1 | 출처 파일 목록 추가
| 0.4.2 | 차트 13종 확대 · 검증된 팔레트 · leaflet/echarts 의존성 내재화. (1) 차트 종류 4종 → 13종 — horizontalBar(긴 라벨), area, heatmap, donut, funnel, radar(다지표 프로파일), treemap·sunburst(계층 비중), gauge 추가. buildOption이 종류별로 축·시리즈·범례를 채우므로 서버/LLM은 최소 필드만 주면 되고, 숫자 배열은 {name,value}로 자동 결합, radar의 indicator는 생략 시 자동 생성된다. ChartType 타입 export. (2) 팔레트 교체 — 기존 팔레트가 다크 서피스에서 명도 밴드 이탈(5색)·색약 인접 구분 ΔE 5.7로 검증 실패였다. 명도·채도·CVD·일반시야·대비를 모두 통과하는 8색 고정 순서로 교체하고, heatmap 값 스케일은 무지개 대신 단일 hue 명암 램프 적용. 단일 시리즈는 한 색 유지(막대 길이의 중복 인코딩 방지). (3) 긴 축 라벨 잘림 수정 — grid.containLabel로 가로 막대의 y축 학명이 컨테이너 밖으로 잘리던 문제 해결. (4) leaflet·echarts를 peerDependency → dependency로 이동 — 앱이 따로 설치할 필요가 없다. 두 진입점은 별도 번들 + 동적 import라 미사용 앱의 번들 크기는 늘지 않는다(node_modules 용량만). |
| 0.4.1 | 스트리밍 중 지도·차트 깜박임 수정. 토큰마다 마크다운을 재파싱하면서 block 객체가 새로 만들어져, 내용이 같아도 useEffect가 재실행되며 지도를 파괴·재생성했다(멀티에이전트에서 다음 에이전트가 토큰을 뱉는 내내 반복). 의존성을 객체 참조가 아닌 내용 기반 키로 바꿔 실제 내용이 바뀔 때만 다시 그리도록 수정. |
| 0.4.0 | GIS 지도 렌더링 · 차트 기본 렌더러 · 생성 중 스켈레톤 로딩. (1) MAP 블록 추가 — LLM이 낸 GeoJSON(point/line/polygon)을 Leaflet 지도로 렌더. 신규 진입점 @bluedus/aiplug-sdk/leaflet의 LeafletMapBlock(선택적 peer leaflet, 동적 import라 미설치 앱은 번들 영향 없음). properties의 name/popup/color/fillColor로 표시 제어, center/zoom 생략 시 fitBounds 자동 맞춤, 타일서버 교체(tileUrl) 지원. MapContentBlock·GeoJSON 타입 추가. leaflet.css 는 SDK가 런타임 주입하므로 앱에서 import 할 필요가 없다(이미지까지 data URI로 인라인, 중복 로드 감지, <head> 최상단 삽입으로 앱 CSS 우선. 엄격한 CSP 대비 injectCss={false} 제공, ensureLeafletCss/LEAFLET_CSS_VERSION export, npm run gen:leaflet-css로 재생성). (2) CHART 기본 렌더러 추가 — 그동안 앱마다 자체 구현하던 차트를 신규 진입점 @bluedus/aiplug-sdk/echarts의 EChartsChartBlock으로 제공(선택적 peer echarts). core가 보내는 {xAxis, series}를 chartType별 완전한 ECharts option으로 보정(buildOption export). (3) 생성 중 스켈레톤 로딩 — 지도·차트·표는 마커 JSON이 완성돼야 렌더되어 그동안 화면이 비어 보이던 문제 해결. 신규 SSE 이벤트 block_pending(blockType·title)을 받아 PENDING 블록으로 셔머 스켈레톤을 깔고, 완성 content_block 도착 시 같은 타입의 스켈레톤을 그 자리에서 교체한다. done/error/blocked 시 미해소 스켈레톤은 자동 제거되어 잔상이 남지 않는다. 기본 컴포넌트 BlockSkeleton(components.PENDING으로 교체 가능). (4) BlockComponentOverrides에 MAP/PENDING 추가, blockToText가 MAP/PENDING을 복사 대상에서 제외. 서버(core) 0.4.0+ 필요 — <aiplug:render-map> 플랫폼 툴·MAP ContentBlock·block_pending 이벤트(마커 여는 태그 감지 시점 전송). 구버전 서버에서도 SDK는 정상 동작하며 해당 기능만 비활성(하위호환). |
| 0.3.13 | 영역복사하기추가
| 0.3.12 | 복사하기 버그 수정
| 0.3.11 | 복사하기 버그 수정
| 0.3.10 | 복사하기 버튼 추가
| 0.3.9 | core 간소화 endpoint변경
| 0.3.8 | 대화 이력 limit수정
| 0.3.7 | 본문 URL 자동 링크 · 인용 스크롤 SDK 내장 · 자동 스크롤 훅 · 표시 불가 출처 제외 안내. (1) 벌거벗은 URL 자동 링크화 — renderMarkdown이 https?:// 로 시작하는 평문 URL을 새 탭 <a>로 변환(기존엔 raw 노출). 마크다운 링크([t](url)) 변환 뒤 처리하고 href="..." 속성 안 URL은 제외해 이중처리 방지. (2) 인용 클릭 스크롤을 BlockRenderer로 이관 — [n] 클릭 → 해당 참고자료로 스크롤+하이라이트 로직을 BlockRenderer 래퍼가 자체 처리(소비 앱/StreamingChat이 핸들러 안 달아도 됨). e.currentTarget 스코프 탐색으로 답변이 여러 개일 때 aiplug-ref-{n} id 중복으로 위 답변으로 튀던 버그 해결. StreamingChat의 중복 핸들러 제거. (3) 자동 스크롤 훅/컴포넌트 추가 — useAutoScroll(trigger, options) 훅 + AutoScrollArea 드롭인. messages.length 트리거로 새 메시지/응답 시작 시에만 최하단 이동(토큰마다 X), stickToBottom 옵션으로 유저가 위에서 읽는 중엔 낚아채지 않음. 헤드리스(hook) UI는 AutoScrollArea로 스크롤 컨테이너만 교체하면 됨. (4) 표시 불가 출처 제외 안내 — 정리본(LLM 요약)도 안전 표시명(metadata name/title 등)도 없어 화면에 못 띄우는 DB 출처는 참고목록에서 제외하고, 하단에 "표시할 수 없는 출처 N건은 제외되었습니다" 안내를 표시. references 이벤트에 excludedCount 추가 → ReferencesContentBlock으로 전달 → ReferenceList footer 렌더(표시할 항목이 하나도 없으면 안내도 안 뜸). 서버(core) 필요 — RAG references 조립 시 안전 표시명 폴백(도메인 무관 관용 키만) + 제외 카운트, references SSE 이벤트 excludedCount 필드. 변경 파일: utils/markdown.ts, stream/react/BlockRenderer.tsx, stream/react/StreamingChat.tsx, stream/react/blocks/ReferenceList.tsx, stream/react/useAutoScroll.ts, stream/react/AutoScrollArea.tsx, stream/react/index.ts, stream/core/reducer.ts, stream/core/types.ts, types/index.ts. |
| 0.3.6 | 본문 인용 [n] 클릭 이동 + 마커 스타일 개선 · 참고목록 정리. (1) [n] 클릭 → 참고자료 스크롤 — 기존엔 인용 마커 [n]이 url이 있을 때만 외부 링크(새 탭)였던 것을, 항상 답변 하단 참고목록의 해당 출처로 스크롤 이동하도록 변경. renderMarkdown의 linkifyCitations가 [n]을 페이지 내 앵커(#aiplug-ref-{n})로 변환하고, StreamingChat의 어시스턴트 버블이 클릭을 위임 처리해 scrollIntoView + 짧은 하이라이트를 준다. 같은 버블 내에서만 대상을 탐색해 답변이 여러 개일 때 id 충돌을 방지하며, url 없는 DB 출처도 이제 클릭 가능하다. (2) 인용 마커 스타일 — [n]을 본문색에 가까운 중립 회색 칩(얇은 테두리 강조)으로 렌더하고, --aiplug-cite-fg/-bg/-border CSS 변수로 앱 테마 오버라이드 가능. (3) 참고목록 정리 — sourceType 그룹 헤더("DB" 등)를 제거해 flat 리스트로 바꾸고, 각 항목에 id={aiplug-ref-{index}}를 부여(스크롤 타깃). url 있는 항목은 제목 ↗ 강조 링크(새 탭)로 표시한다. 서버 무관 · 프론트 전용 · 하위호환(references 없는 데이터는 기존과 동일 렌더). 변경 파일: utils/markdown.ts, stream/react/StreamingChat.tsx, stream/react/blocks/ReferenceList.tsx. |
| 0.3.5 | core init message 추가
| 0.3.4 | 이력 출처 렌더 수정
| 0.3.3 | 대화 복원 시 참고자료(references) 표시 + 생성 이미지 제목/카드 UI. (1) references 복원 — 0.3.2까진 실시간 스트리밍에서만 보이던 RAG 참고자료가 과거 대화 복원 시엔 누락되던 것을, 서버가 인용된 출처를 메시지에 저장하고 복원 API로 반환하도록 개선. ConversationMessageItem에 references 필드 추가, loadConversation이 이를 UIMessage.references로 매핑 → AgentChat이 답변 하단에 출처 칩([n] 제목)으로 렌더한다. 저장·표시되는 references는 민감정보가 제거된 필터링본(인용된 출처만, DB 출처는 LLM 정리 요약)으로, 원본 검색 청크는 노출하지 않는다. (2) 생성 이미지 제목/카드 — 기존엔 생성 이미지가 캡션 없이 원본 크기로 노출되던 것을, ImageContentBlock에 title 필드를 추가하고 <figure>+<figcaption> 카드(최대 360px, 하단 제목 캡션)로 렌더. 제목은 마커의 title 속성(한글)을 우선 사용하고 없으면 altText(프롬프트)로 폴백한다. 서버(core) 0.3.3+ 필요 — AP_CONV_MESSAGE에 REFERENCE_ITEMS(jsonb) 컬럼 추가, POST /api/v1/conversations/messages 응답 MessageItem에 references 추가, <aiplug:generate-image> 마커에 title 속성 지원. 기존 동작 무변경(하위호환 — references/title 없는 구버전 데이터는 각각 미표시/폴백). |
| 0.3.2 | 대화 복원 시 표/차트/이미지 블록 렌더링 + 마크다운 링크 지원. (1) 복원 메시지 구조화 블록 지원 — 0.3.1에선 과거 대화 복원 시 텍스트(content)만 렌더해 표·차트가 마크다운 raw로 노출되던 것을, 서버가 저장된 원문을 ContentBlock으로 재파싱해 blocks로 함께 반환하도록 개선. fetchConversationMessages의 ConversationMessageItem에 blocks 필드 추가, loadConversation이 이를 UIMessage.contents로 매핑 → 실시간 응답과 동일하게 ContentBlockRenderer가 표·차트를 렌더한다. (2) 마크다운 링크 렌더링 — renderMarkdown이 [텍스트](url)를 클릭 가능한 <a>(새 창)로 변환(기존엔 raw 노출). 인용 마커 [n] 변환보다 먼저 처리해 충돌 방지. (3) 빈 표 방지 — 데이터 행이 없는 표(헤더만 존재)는 렌더하지 않도록 TableBlock에 가드 추가(약한 모델이 헤더만 낸 빈 표가 덜렁 나오던 문제 해결). 서버(core) 0.3.2+ 필요 — POST /api/v1/conversations/messages 응답의 MessageItem에 blocks(assistant 메시지 재파싱 결과) 추가, content(raw)는 하위호환 유지. 기존 동작 무변경(하위호환). |
| 0.3.1 | 대화 이력(사이드바) 지원 추가 — ChatGPT/Claude식 과거 대화 목록 + 클릭 복원. 브라우저 단위 식별자(config.userId = localStorage anon_id)를 서버 clientId로 저장해, 같은 브라우저의 과거 대화를 목록으로 조회하고 클릭 시 메시지를 복원한다. 신규 API: useConversations(config) 훅(대화 목록 + reload), useAIPlugChat에 loadConversation(conversationId) 추가(불러온 대화로 화면 복원 + 이후 send가 그 대화로 이어짐). core 함수 fetchConversations / fetchConversationMessages도 export. 서버(core) 0.3.1+ 필요 — AP_CONVERSATION에 CLIENT_ID/LOGIN_USER_ID 컬럼 추가, POST /api/v1/conversations(목록)·POST /api/v1/conversations/messages(복원) 엔드포인트. 복원 메시지는 텍스트(content)만 — 표/차트 등 구조화 블록은 추후 지원. 기존 send 시그니처·동작 무변경(하위호환). |
| 0.3.0 | 선택한 모델을 chat 요청에 반영 — 0.2.7에서 모델 목록 조회·선택 UI까지 구현됐으나 실제 /api/v3/chat 요청엔 반영되지 않던 것을 연결. send(text, modelId?) 시그니처 확장으로 선택 모델(selectedModel.modelId)을 요청 body의 llmModelId로 전달. ChatStreamOptions.modelId, ChatRequestBody.llmModelId 추가, StreamingChat이 입력바에 모델 셀렉트를 내장(useModels 연동). 라우팅 정책: 매 턴 변경 허용(대화 도중 모델 변경 시 그 턴부터 반영) + conversation 라우팅 핀 갱신(이후 턴 기본값으로 고정) + 유효하지 않은 modelId는 기존 라우팅 체인으로 자동 fallback. 서버(core) 0.3.0+ 필요 — ChatRequest.llmModelId 수신, ChatLlmTargetResolver가 tenant에 enabled 등록된 모델 범위에서만 resolve(cross-tenant/비활성 모델 차단), USER_SELECTED 라우팅 소스 추가. |
| 0.2.7 | 모델 목록 조회 및 지정 호출
| 0.2.6 | 마크다운 렌더 정리 — (1) 표 셀 안 마크다운 렌더링: DefaultTable이 헤더/셀의 **굵게**·*이탤릭*·`코드`·~~취소선~~을 변환(기존엔 **한국** 처럼 raw 노출). 신규 renderInlineMarkdown export. (2) 인용블록(> …) 지원: 기존엔 >가 그대로 찍히던 것을 <blockquote>로 렌더. (3) 줄바꿈 정리: 구분선 ***/___ 인식, 블록 요소(제목/리스트/표/인용/hr) 직후 불필요한 <br/> 제거로 과한 간격 완화. (4) 제목 줄분리: 스트리밍 중 …제공되었습니다.# 🐟 제목처럼 앞 문장에 붙은 ATX 제목을 줄 분리해 제목으로 렌더(문장부호+#{1,6} 패턴만 → C#·#태그 오탐 방지). 표/차트 점진 렌더링(content_block 이벤트) 추가. 스트리밍 중 표가 하나 완성될 때마다 즉시 렌더 — 기존엔 done 후에야 모든 표가 한꺼번에 보였음. 신규 SSE 이벤트 content_block(완성된 구조화 블록 1개)을 parseSse/reducer가 처리해 순서대로 블록 추가. done 시 mergeDoneBlocks가 서버 권위 블록으로 최종 교체(중복 없음). 서버(core) 0.2.6+ 필요 — core가 스트리밍 토큰에서 <aiplug:...> 마커·마크다운 표 구간을 분리해 완성 즉시 content_block으로 전송하고, 이모지(surrogate pair)가 토큰 경계로 쪼개져 ??로 깨지던 문제도 서버측에서 수정. |
| 0.2.5 | MessageId 추가 parseSse.ts
| 0.2.4 | 표 중복 렌더링 수정 · 피드백 활성화 안정화. done 완료 시 서버 contentBlocks를 본문 권위 소스로 사용하도록 reducer(mergeDoneBlocks) 재작성 — 스트리밍 중 누적된 raw 토큰(LLM이 낸 마크다운 표 원문 등)은 폐기하고, 스트림 전용 블록(REFERENCES/KEYWORDS)만 보존. 본문 마크다운 표 + 렌더된 표가 중복으로 보이던 문제 해결. 아울러 done.messageId → Message.messageId 매핑을 보정해 완료된 답변에 따봉 바가 항상 노출되도록 함. (서버측: render-table 마커 관용 파싱 + 마크다운 표 백스톱으로 약한 모델의 빈 표/마크다운 표도 보정.) |
| 0.2.3 | StreamingChat 응답 피드백 내장 — 드롭인이 내부에서 FeedbackContext.Provider로 감싸고 어시스턴트 답변 하단에 FeedbackBar를 자동 노출(done으로 messageId가 온 완료 답변에 한함). 0.2.2에선 hook의 feedback API는 제공됐지만 StreamingChat에 연결돼 있지 않아 드롭인에선 따봉이 보이지 않던 문제 수정. 툴킷 형태(useAIPlugChat + 직접 Provider)는 기존과 동일. |
| 0.2.2 | 응답·출처 피드백 추가 — 따봉(👍/👎)·코멘트. 응답 단위(FeedbackBar) + 출처 단위(ReferenceList 자동). FeedbackContext/useFeedback, hook feedback API, submitFeedback 함수(POST /api/v1/feedback). Message.messageId·done.messageId 추가(피드백 타겟). 계약: 변경마다 "현재 전체 상태" 전송, 둘 다 비면 평가 취소(삭제). Reference.title/url optional 화(내부 문서·파일명 미정 대응). |
| 0.2.1 | exports 맵 누락 수정 — ./stream, ./stream/react 가 해결되지 않던 문제 패치. |
| 0.2.0 | 블록 기반 스트리밍 챗 모듈 추가 (@bluedus/aiplug-sdk/stream). 함수(createChatStream)·툴킷(useAIPlugChat + BlockRenderer)·챗봇(StreamingChat) 3형태. REFERENCES/KEYWORDS ContentBlock, [n] 인용 마커→출처 링크, sourceType 그룹 참고목록, CSS 변수(--aiplug-*) 테마, components prop 블록 오버라이드. 기존 toolkit(AgentChat/useAIPlug/ChatSession) 무변경. |
| 0.1.9 | 마크다운 렌더러 내장(외부 의존성 없음). systemStatus 파생 상태 추가. (상세 changelog 확인 필요) |
| 0.1.8 | blocked SSE 이벤트에 source 필드(SYSTEM/POLICY/KANANA) 추가. (상세 changelog 확인 필요) |
| 0.1.7 | Phase 진행 상태 표시 추가 — 스트리밍 중 Agent 탐색·RAG 검색·Tool 실행 등 처리 단계 실시간 표시. thinkingStep 상태 추가. onThinking 콜백 추가. |
| 0.1.6 | SSE 스트리밍 추가 — /api/v3/chat 연동. 토큰 실시간 렌더링. isStreaming 상태, streaming 옵션 추가. streamMessage() API 추가. |
| 0.1.5 | 서버 에러 메시지 반환 추가 |
| 0.1.4 | 타입 에러 수정. |
| 0.1.3 | Platform 내장 기능 추가 — 이미지 생성·차트·테이블 ContentBlock 자동 렌더링. AgentResponse.contents, UIMessage.contents 타입 추가. |
| 0.1.2 | Confirm 흐름 추가 — PENDING_TOOL_CONFIRM, PENDING_ORCHESTRATION_CONFIRM, PENDING_CONSENT 자동 처리. /api/v2/chat 전환. confirmAction, agentAccepted 파라미터 지원. |
| 0.1.1 | 버그 수정 |
| 0.1.0 | 최초 릴리즈 |
배포 (GitLab CI)
# package.json version 을 올린 뒤
npm run build
git tag v0.7.1
git push origin v0.7.1GitLab CI/CD Variables에 NPM_TOKEN 등록 필요.
배포 전 확인:
package.json의exports에./stream,./stream/react,./leaflet,./echarts매핑이 있고,npm run build후dist/에 해당 모듈이 실제로 나오는지 확인.dist/esm/leaflet.js·echarts.js가leaflet/echarts를 번들에 포함하지 않고 동적 import 로 남겨두는지도 함께 확인(grep "import('leaflet')" dist/esm/leaflet.js). 호스트 앱은 배포 후npm i @bluedus/[email protected]로 업그레이드해야 표 중복 수정·피드백 활성화가 반영됩니다.0.3.0+StreamingChat은 입력바에 모델 선택 셀렉트를 자동 포함합니다(테넌트에 등록된 모델이 2개 이상일 때). 선택값은 chat 요청에llmModelId로 전달되며, 별도 prop 설정은 필요 없습니다.0.3.1+ 대화 이력(사이드바)은 툴킷 형태 전용입니다 —useConversations로 목록을 그리고, 클릭 시useAIPlugChat의loadConversation(conversationId)을 호출해 복원합니다.StreamingChat드롭인에는 사이드바가 포함되지 않습니다(앱에서 직접 조립).0.3.2+ 과거 대화 복원 시 표·차트·이미지가 실시간과 동일하게 렌더됩니다. 서버(core) 0.3.2+ 필수 — 복원 API가blocks를 반환하지 않으면 표는 여전히 텍스트로만 보입니다(구버전 서버 하위호환:contentfallback).0.3.3+ 대화 복원 시 참고자료(references)가 표시되고, 생성 이미지가 제목 카드로 렌더됩니다. 서버(core) 0.3.3+ 필수 — 복원 API가references를 반환하지 않으면 참고자료는 표시되지 않습니다(구버전 서버 하위호환: 미표시). 이미지 제목은 서버가<aiplug:generate-image title="...">마커를 지원해야 채워지며, 미지원 시 프롬프트(altText)로 폴백합니다.0.4.0+ 지도·차트 기본 렌더러는 자동으로 붙지 않습니다.components={{ MAP: LeafletMapBlock, CHART: EChartsChartBlock }}로 주입해야 합니다(주입하지 않으면 안내 문구만 표시 — 오류 아님).0.4.2+ 부터leaflet·echarts는 SDK 의존성이라 앱이 따로 설치할 필요가 없습니다.leaflet.css도 SDK가 런타임 주입하므로 앱 import는 불필요하며, 엄격한 CSP 환경에서만injectCss={false}+ 앱에서 직접 로드하십시오.0.6.0+ 표 행 클릭은views를 등록해야만 켜집니다(등록 안 하면 기존과 동일한 정적 표 — 오류 아님). 표 파싱은 SDK 가 하므로 서버 버전은 가리지 않습니다. 다만 숨김 컬럼(_view·_id)은 스킬 응답이 실제로 내려줘야 하며, LLM 이 그 컬럼을 표에 유지하도록 지시가 필요합니다 — 서버 기본 프롬프트에 규약이 반영된 빌드면 자동이고, 아니면 에이전트 프롬프트에 직접 적으십시오.0.3.7+ 본문 평문 URL이 자동 링크되고,[n]인용 클릭 스크롤이 SDK(BlockRenderer) 내장이라 앱에서 핸들러가 필요 없습니다. 자동 스크롤은useAutoScroll/AutoScrollArea로 제공(헤드리스 UI는 스크롤 컨테이너만 교체). 표시 불가 출처 제외 안내는 서버(core)가excludedCount를 내려줘야 뜹니다(구버전 서버 하위호환: 안내 미표시).
라이선스
UNLICENSED — ㈜블루더스 내부용
