에이전트리아(Agentria)에서 완성한 에이전트를 API로 배포했다면, 외부 서비스에서 이 API 엔드포인트(Endpoint)를 호출해 에이전트와 대화할 수 있습니다. Agent API는 채팅방 생성·조회·삭제, 비동기 채팅, SSE(Server-Sent Events) 스트리밍 채팅을 지원하는 REST API입니다.
이 가이드에서는 각 엔드포인트를 호출하는 방법과, 대화 맥락을 유지하며 실행 결과를 안전하게 받아오는 방법을 안내합니다.
chat_room_id를 지정하지 않으면 요청마다 새 채팅방이 자동 생성될 수 있습니다. 대화 맥락을 이어가려면 1단계에서 만든(또는 이전 응답에서 받은) 채팅방 ID를 계속 같은 값으로 전달해야 합니다.
external_request_id는 클라이언트가 요청을 보내기 전에 직접 발급하는 식별자로, 두 가지 역할을 합니다. 첫째, 동일한 external_request_id로 재요청이 오면 서버가 같은 요청으로 판단해 409 Conflict로 거부하므로, 네트워크 재시도 등으로 인한 중복 실행(멱등성, Idempotency 보장)을 방지할 수 있습니다. 둘째, 클라이언트가 요청 시점에 이미 이 식별자를 확보해두는 방식이므로, 서버가 응답으로 request_id를 돌려주기 전부터 이 값으로 해당 요청을 추적·조회할 수 있습니다.
Agent API의 SSE는 최초 request_id 메타 이벤트를 제외한 모든 이벤트가 {"ability_node_id", "ability_node_name", "results"} 형태로 한 번 더 감싸진 형태로 직렬화됩니다. Ability API의 SSE(response/error 이벤트의 body가 결과 스키마 그대로 오는 방식)와 다른 부분이므로, 두 API를 함께 연동할 때 특히 주의합니다.
event_type
발생 시점
body 형태
request_id
스트림 시작 직후 (최초, 1회)
plain 문자열(UUID). 감싸는 형태 없이 그대로 전달되며, 스트림 유실 시 결과 재조회용 요청 ID
node
각 노드 실행 종료 시점 (중간 이벤트, 노드 개수만큼 반복)
{"ability_node_id":, "ability_node_name":, "results":<노드 실행 결과 dict>}
최종 이벤트에서는 results 필드 안에 APIResponseSchema가 통째로 한 번 더 들어있다는 점이 중요합니다(3단계의 상태 조회 응답과 같은 구조). 따라서 최종 상태를 판별할 때는 chunk.status가 아니라 chunk.results.status를 확인해야 합니다.
constformData = newFormData();formData.append('params_json',JSON.stringify({input_message:'안녕하세요',chat_room_id:'e36644fa-0b2e-11f0-b130-143627ec67e9'}));formData.append('debug','false');constresponse = awaitfetch(`${host}/api/agent/my-agent/sse`,{method:'POST',headers:{'X-API-KEY':'your-api-key'},body:formData});// 스트림 유실 시 GET .../{requestId}/result 재조회용. 첫 request_id 이벤트로도 갱신됩니다.letrequestId = response.headers.get('X-Agent-Request-Id');// 한 번의 read() 가 여러 이벤트를 합쳐서 줄 수 있으므로 line-buffered 파싱이 필요합니다.constreader = response.body.getReader();constdecoder = newTextDecoder();letbuf = '';letcurrentEvent = null;constchunks = [];// 모든 chunk 누적letfinalChunk = null;while(true){const{done,value} = awaitreader.read();if(done)break;buf += decoder.decode(value,{stream:true});letnl;while((nl = buf.indexOf('\n')) !== -1){constline = buf.slice(0,nl);buf = buf.slice(nl + 1);if(line === ''){currentEvent = null;continue;}// 이벤트 경계if(line.endsWith(':')){currentEvent = line.slice(0, -1);continue;}// request_id 이벤트의 body는 감싸는 형태 없는 plain UUID 문자열 — JSON.parse 전에 처리if(currentEvent === 'request_id'){requestId = line;continue;}letbody;try{body = JSON.parse(line);}catch{continue;}chunks.push(body);// 중간 vs 최종conststatus = body.results?.status;if(body.ability_node_id === 0 || status === 'COMPLETED' || status === 'FAILURE'){finalChunk = body;// results 안에 APIResponseSchemaconsole.log('[final]',status,finalChunk.results.results);}else{console.log('[node]',body.ability_node_id,body.ability_node_name,body.results);}}}if(finalChunk?.results?.status === 'FAILURE'){console.error('실패:',finalChunk.results.failure_reason);}// 최종 chunk 없이 스트림이 끊긴 경우 — 재전송(재실행) 대신 재조회if(!finalChunk && requestId){constres = awaitfetch(`${host}/api/agent/my-agent/${requestId}/result`,{headers:{'X-API-KEY':'your-api-key'}});constresult = awaitres.json();// status 가 COMPLETED/FAILURE 가 될 때까지 폴링}
constformData = newFormData();formData.append('params_json',JSON.stringify({input_message:'안녕하세요',chat_room_id:'e36644fa-0b2e-11f0-b130-143627ec67e9'}));formData.append('debug','false');constresponse = awaitfetch(`${host}/api/agent/my-agent/sse`,{method:'POST',headers:{'X-API-KEY':'your-api-key'},body:formData});// 스트림 유실 시 GET .../{requestId}/result 재조회용. 첫 request_id 이벤트로도 갱신됩니다.letrequestId = response.headers.get('X-Agent-Request-Id');// 한 번의 read() 가 여러 이벤트를 합쳐서 줄 수 있으므로 line-buffered 파싱이 필요합니다.constreader = response.body.getReader();constdecoder = newTextDecoder();letbuf = '';letcurrentEvent = null;constchunks = [];// 모든 chunk 누적letfinalChunk = null;while(true){const{done,value} = awaitreader.read();if(done)break;buf += decoder.decode(value,{stream:true});letnl;while((nl = buf.indexOf('\n')) !== -1){constline = buf.slice(0,nl);buf = buf.slice(nl + 1);if(line === ''){currentEvent = null;continue;}// 이벤트 경계if(line.endsWith(':')){currentEvent = line.slice(0, -1);continue;}// request_id 이벤트의 body는 감싸는 형태 없는 plain UUID 문자열 — JSON.parse 전에 처리if(currentEvent === 'request_id'){requestId = line;continue;}letbody;try{body = JSON.parse(line);}catch{continue;}chunks.push(body);// 중간 vs 최종conststatus = body.results?.status;if(body.ability_node_id === 0 || status === 'COMPLETED' || status === 'FAILURE'){finalChunk = body;// results 안에 APIResponseSchemaconsole.log('[final]',status,finalChunk.results.results);}else{console.log('[node]',body.ability_node_id,body.ability_node_name,body.results);}}}if(finalChunk?.results?.status === 'FAILURE'){console.error('실패:',finalChunk.results.failure_reason);}// 최종 chunk 없이 스트림이 끊긴 경우 — 재전송(재실행) 대신 재조회if(!finalChunk && requestId){constres = awaitfetch(`${host}/api/agent/my-agent/${requestId}/result`,{headers:{'X-API-KEY':'your-api-key'}});constresult = awaitres.json();// status 가 COMPLETED/FAILURE 가 될 때까지 폴링}
constformData = newFormData();formData.append('params_json',JSON.stringify({input_message:'안녕하세요',chat_room_id:'e36644fa-0b2e-11f0-b130-143627ec67e9'}));formData.append('debug','false');constresponse = awaitfetch(`${host}/api/agent/my-agent/sse`,{method:'POST',headers:{'X-API-KEY':'your-api-key'},body:formData});// 스트림 유실 시 GET .../{requestId}/result 재조회용. 첫 request_id 이벤트로도 갱신됩니다.letrequestId = response.headers.get('X-Agent-Request-Id');// 한 번의 read() 가 여러 이벤트를 합쳐서 줄 수 있으므로 line-buffered 파싱이 필요합니다.constreader = response.body.getReader();constdecoder = newTextDecoder();letbuf = '';letcurrentEvent = null;constchunks = [];// 모든 chunk 누적letfinalChunk = null;while(true){const{done,value} = awaitreader.read();if(done)break;buf += decoder.decode(value,{stream:true});letnl;while((nl = buf.indexOf('\n')) !== -1){constline = buf.slice(0,nl);buf = buf.slice(nl + 1);if(line === ''){currentEvent = null;continue;}// 이벤트 경계if(line.endsWith(':')){currentEvent = line.slice(0, -1);continue;}// request_id 이벤트의 body는 감싸는 형태 없는 plain UUID 문자열 — JSON.parse 전에 처리if(currentEvent === 'request_id'){requestId = line;continue;}letbody;try{body = JSON.parse(line);}catch{continue;}chunks.push(body);// 중간 vs 최종conststatus = body.results?.status;if(body.ability_node_id === 0 || status === 'COMPLETED' || status === 'FAILURE'){finalChunk = body;// results 안에 APIResponseSchemaconsole.log('[final]',status,finalChunk.results.results);}else{console.log('[node]',body.ability_node_id,body.ability_node_name,body.results);}}}if(finalChunk?.results?.status === 'FAILURE'){console.error('실패:',finalChunk.results.failure_reason);}// 최종 chunk 없이 스트림이 끊긴 경우 — 재전송(재실행) 대신 재조회if(!finalChunk && requestId){constres = awaitfetch(`${host}/api/agent/my-agent/${requestId}/result`,{headers:{'X-API-KEY':'your-api-key'}});constresult = awaitres.json();// status 가 COMPLETED/FAILURE 가 될 때까지 폴링}
참고 (Python 클라이언트):httpx.AsyncClient.stream()의 aiter_lines()와 JSON 파싱으로 한 줄씩 처리하되, node: / response: / error: 접두 라인은 JSON 파싱 오류로 건너뛰면 됩니다. request_id 이벤트의 body(plain UUID)도 JSON이 아니므로 같은 방식으로 건너뛰어지며, 기존 클라이언트의 chunks[-1].get("results", {}).get("status") == "FAILURE" 같은 검사 패턴이 그대로 동작합니다. 재조회용 ID가 필요하면 직전 라인이 request_id:인지 추적하거나 응답 헤더 X-Agent-Request-Id를 사용합니다.
// external_request_id를 부여한 요청의 콜백 — 키 포함{"request_id":"a1b2c3d4-...","external_request_id":"order-12345","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}// external_request_id를 부여하지 않은 요청의 콜백 — 키 자체가 생략됨{"request_id":"a1b2c3d4-...","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}
// external_request_id를 부여한 요청의 콜백 — 키 포함{"request_id":"a1b2c3d4-...","external_request_id":"order-12345","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}// external_request_id를 부여하지 않은 요청의 콜백 — 키 자체가 생략됨{"request_id":"a1b2c3d4-...","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}
// external_request_id를 부여한 요청의 콜백 — 키 포함{"request_id":"a1b2c3d4-...","external_request_id":"order-12345","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}// external_request_id를 부여하지 않은 요청의 콜백 — 키 자체가 생략됨{"request_id":"a1b2c3d4-...","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}
외부 ID가 없는 요청은 external_request_id 키 자체를 콜백 페이로드에 포함하지 않습니다. 기존 방식 그대로 유지되므로 이전 연동 코드와도 호환됩니다.
흐름 3: 실시간 스트리밍 방식
참고:chat_room_id를 지정하지 않으면 채팅방이 자동으로 생성될 수 있습니다. 대화 이력을 유지하려면 항상 동일한 chat_room_id를 사용해야 합니다.
Ability API가 완성된 워크플로(어빌리티)를 한 번 실행하고 결과를 받는 방식이라면, Agent API는 채팅방 단위로 여러 차례 대화를 주고받으며 맥락을 유지하는 방식입니다. SSE 응답 구조도 달라서, Agent API는 최초 request_id 이벤트를 제외한 모든 이벤트가 {ability_node_id, ability_node_name, results} 형태로 한 번 더 감싸져 있고, 최종 상태는 results.status를 확인해야 합니다.
서버는 스트림 연결과 무관하게 에이전트 실행을 계속 완료하고 결과를 기록합니다. 연결이 끊겨 최종 이벤트를 받지 못했다면, 같은 요청을 재전송하지 않고 request_id(또는 external_request_id)로 상태·결과 조회 API를 폴링해 결과를 가져와야 합니다. 같은 요청을 재전송하면 에이전트가 다시 실행되어 대화 이력이 중복될 수 있습니다.