diff --git a/backend-node/src/services/multiAgentExecutionEngine.ts b/backend-node/src/services/multiAgentExecutionEngine.ts index ee74d54f..6e7012fe 100644 --- a/backend-node/src/services/multiAgentExecutionEngine.ts +++ b/backend-node/src/services/multiAgentExecutionEngine.ts @@ -264,10 +264,25 @@ export class MultiAgentExecutionEngine { try { const data = await this.executeConnector(connector); connectorResults.push({ connector: connector.name, type: connector.type, data }); - connectorContext += `\n[데이터 소스: ${connector.name} (${connector.type})]:\n${JSON.stringify(data).substring(0, 2000)}\n`; + // REST API 결과는 raw response 본문을 별도 라벨로 inject (JSON.stringify 한번 더 X) + if (data?.type === "rest_api" && typeof data.response === "string") { + connectorContext += + `\n========== 데이터 소스: ${connector.name} ==========\n` + + `유형: REST API ${data.method} ${data.url}\n` + + `상태: ${data.status} (${data.duration_ms}ms)${data.truncated ? " [응답이 truncate 됨]" : ""}\n` + + `--- 응답 본문 시작 ---\n${data.response}\n--- 응답 본문 끝 ---\n`; + } else if (data?.error) { + connectorContext += `\n========== 데이터 소스: ${connector.name} ==========\n` + + `유형: ${connector.type}\n` + + `상태: 조회 실패 — ${data.error}\n`; + } else { + connectorContext += `\n========== 데이터 소스: ${connector.name} ==========\n` + + `유형: ${connector.type}\n` + + `데이터: ${JSON.stringify(data).substring(0, 4000)}\n`; + } } catch (e: any) { connectorResults.push({ connector: connector.name, type: connector.type, error: e.message }); - connectorContext += `\n[데이터 소스: ${connector.name}]: 조회 실패 - ${e.message}\n`; + connectorContext += `\n========== 데이터 소스: ${connector.name} ==========\n조회 실패 - ${e.message}\n`; } } @@ -298,11 +313,24 @@ export class MultiAgentExecutionEngine { } // LLM 호출 + // connector 결과가 있을 때는 강한 가드레일을 system prompt 에 추가: + // - 답변에 사용하는 모든 수치/사실은 반드시 [데이터 소스] 블록 안에서 인용해야 함 + // - 데이터에 없으면 "데이터 없음" 으로 명시 (hallucination 금지) + const dataGuard = connectorContext + ? `\n[데이터 사용 규칙]\n` + + `1. 아래 "데이터 소스" 블록의 응답 본문(--- 응답 본문 시작 --- ~ 끝 ---)을 끝까지 정독한 뒤 답한다. JSON 안의 모든 키를 빠짐없이 확인할 것.\n` + + `2. 응답 본문에 사용자가 요청한 값이 있으면 그 값을 자신 있게 인용해 답한다 (예: rates.JPY = 0.10809).\n` + + `3. 응답 본문에 그 값이 없거나, 상태가 200이 아니거나, 응답이 HTML 페이지( = { + Accept: "application/json, text/plain, */*", + ...(conn.default_headers && typeof conn.default_headers === "object" ? conn.default_headers : {}), + }; + // auth_type / auth_config 처리 (basic / bearer / api_key) + if (conn.auth_type === "bearer" && conn.auth_config?.token) { + headers["Authorization"] = `Bearer ${conn.auth_config.token}`; + } else if (conn.auth_type === "api_key" && conn.auth_config?.key) { + const headerName = conn.auth_config.header_name || "X-API-Key"; + headers[headerName] = conn.auth_config.key; + } else if (conn.auth_type === "basic" && conn.auth_config?.username) { + const cred = Buffer.from(`${conn.auth_config.username}:${conn.auth_config.password || ""}`).toString("base64"); + headers["Authorization"] = `Basic ${cred}`; + } + + const reqOpts: any = { + method, + url: fullUrl, + headers, + timeout: (conn.timeout || 15) * 1000, + validateStatus: () => true, + }; + if ((method === "POST" || method === "PUT" || method === "PATCH") && conn.default_request_body) { + reqOpts.data = conn.default_request_body; + } + + const t0 = Date.now(); + const res = await axios(reqOpts); + const dur = Date.now() - t0; + + // 응답 데이터를 prompt 안전 길이로 잘라서 반환 + let body: any = res.data; + let bodyStr = ""; + if (typeof body === "string") { + bodyStr = body.substring(0, 12000); + } else { + try { bodyStr = JSON.stringify(body, null, 2).substring(0, 12000); } + catch { bodyStr = String(body).substring(0, 12000); } + } + + return { + type: "rest_api", + name: conn.connection_name, + url: fullUrl, + method, + status: res.status, + duration_ms: dur, + response: bodyStr, + truncated: bodyStr.length >= 12000, + }; + } catch (err: any) { + return { + type: "rest_api", + name: conn.connection_name, + url: (conn.base_url || "") + (conn.endpoint_path || ""), + error: err.message, + status: err.response?.status, + }; + } } if (connector.type === "file" && connector.path) {