"use strict"; /** * Вызов агента Alem «ИИ-агент КФУ» (открытый API). * * Базовый адрес: https://alem.ai.telecom.kz/app/api/v1/open-api * (переменная окружения ALEM_BASE_URL уже содержит путь до /app, * то есть ALEM_BASE_URL = https://alem.ai.telecom.kz/app — * скрипт дописывает /api/v1/open-api/...). * * Шаг 1. Создать сессию: * POST $ALEM_BASE_URL/api/v1/open-api/conversation * Headers: Content-Type: application/json * Authorization: Bearer $ALEM_API_KEY * Body: { "template_id": "$ALEM_TEMPLATE_ID", "title": "...", "user_id": "kfu_app" } * Ответ: { "data": { "session_id": "" } } * * Шаг 2. Отправить сообщение в сессию (ответ идёт потоком SSE): * POST $ALEM_BASE_URL/api/v1/open-api/conversation//send * Headers: Content-Type: application/json * Authorization: Bearer $ALEM_API_KEY * Body: { "content": "<текст запроса>", "user_id": "kfu_app", "model_api_key": "$ALEM_API_KEY" } * * Ответ — поток Server-Sent Events, каждая строка вида "data: ". * Итог — в последнем событии message_result: * { "detail": { "full_content": "<полный текст ответа агента>" } } * (см. extractSseText). Дальше текст разбирается на JSON (extractJsonFromText). * * Таймаут — 10 минут (ALEM_TIMEOUT_MS). Ключи приходят из окружения, * в код и в git не пишутся. */ const ALEM_TIMEOUT_MS = 600000; const ALEM_TIMEOUT_TEXT = "Alem timeout: no response within " + Math.round(ALEM_TIMEOUT_MS / 60000) + " min."; function fetchWithTimeout(url, options, timeoutMs) { const controller = new AbortController(); const timer = setTimeout(function () { controller.abort(); }, timeoutMs); return fetch(url, Object.assign({ signal: controller.signal }, options)).catch(function (err) { clearTimeout(timer); if (controller.signal.aborted) { const e = new Error(ALEM_TIMEOUT_TEXT); e.code = "ALEM_TIMEOUT"; throw e; } throw err; }).finally(function () { clearTimeout(timer); }); } function createAlemSession() { const url = (process.env.ALEM_BASE_URL || "").replace(/\/+$/, "") + "/api/v1/open-api/conversation"; return fetchWithTimeout(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer " + process.env.ALEM_API_KEY }, body: JSON.stringify({ template_id: process.env.ALEM_TEMPLATE_ID, title: "KFU-agent-call", user_id: "kfu_app" }) }).then(function (resp) { if (!resp.ok) { const e = new Error("Alem create HTTP " + resp.status + (resp.statusText ? " " + resp.statusText : "")); e.code = "ALEM_HTTP_" + resp.status; throw e; } return resp.json().then(function (body) { const sid = body && body.data && body.data.session_id; if (!sid) throw new Error("Alem no session_id: " + JSON.stringify(body).slice(0, 200)); return sid; }); }); } function extractSseText(raw) { var answer = null; var lines = String(raw).split(String.fromCharCode(10)); for (var i = 0; i < lines.length; i++) { var s = lines[i].trim(); if (!s.startsWith(String.fromCharCode(100,97,116,97,58))) continue; var payload = s.slice(5).trim(); if (!payload || payload === String.fromCharCode(91,68,79,78,69,93)) continue; try { var ev = JSON.parse(payload); if (ev && ev.detail && typeof ev.detail.full_content === String.fromCharCode(115,116,114,105,110,103) && ev.detail.full_content) { answer = ev.detail.full_content; } } catch (e) { } } return answer || String.fromCharCode(78,101,116,32,101,116,32,111,116,118,101,116); } function askAlem(sessionId, message) { var url = (process.env.ALEM_BASE_URL || "").replace(/\/+$/, "") + sendPath(sessionId); return fetchWithTimeout(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + process.env.ALEM_API_KEY }, body: JSON.stringify({ content: message, user_id: 'kfu_app', model_api_key: process.env.ALEM_API_KEY }) }, ALEM_TIMEOUT_MS).then(function (resp) { if (!resp.ok) { const e = new Error("Alem send HTTP " + resp.status + (resp.statusText ? " " + resp.statusText : "")); e.code = "ALEM_HTTP_" + resp.status; throw e; } const reader = resp.body.getReader(); const dec = new TextDecoder(); let raw = ""; let finished = false; function settle(fn) { if (!finished) { finished = true; try { reader.cancel(); } catch (e) { } fn(extractSseText(raw)); } } function pump() { reader.read().then(function (r) { if (r.done) { settle(resolve); return; } raw += dec.decode(r.value, { stream: true }); pump(); }).catch(function (err) { if (!finished) { finished = true; reject(err.code === "ALEM_TIMEOUT" ? err : new Error("Alem stream read error: " + String((err && err.message) || err))); } }); } return new Promise(function (resolve, reject) { pump(); }); }); } function sendPath(id) { return '/api/v1/open-api/conversation/' + id + '/send'; } function extractJsonFromText(text) { var s = String(text || ''); var fenceStart = s.indexOf('```'); if (fenceStart !== -1) { var inner = s.slice(fenceStart + 3); var langEnd = inner.indexOf('```'); if (langEnd !== -1) inner = inner.slice(langEnd + 3); var jsonStart = inner.indexOf('{'); var jsonEnd = inner.lastIndexOf('}'); if (jsonStart !== -1 && jsonEnd > jsonStart) { try { return JSON.parse(inner.slice(jsonStart, jsonEnd + 1)); } catch (e) { } } } var first = s.indexOf('{'); var last = s.lastIndexOf('}'); if (first !== -1 && last > first) { try { return JSON.parse(s.slice(first, last + 1)); } catch (e) { } } return null; } function callKfuAgent(company, period, fileText) { var prompt = [ 'Расчёт коэффициентов финансовой устойчивости (КФУ).', 'Компания: ' + company, 'Отчётный период: ' + period, 'Данные отчёта:', fileText, '', 'Верни ТОЛЬКО один JSON-объект без пояснений со строгими полями:', '{company, period, units, debt, ebitda_ltm, interest_ltm, equity, k1, k2, k3, zone_k1, zone_k2, zone_k3, zone_company, flags, sources, comment}.', 'Числа — числом (без слов и валютного знака), zone_* — одно из: зелёная, жёлтая, красная, без оценки.', 'flags — массив строк (пустой, если замечаний нет), sources — объект-источник цифр, comment — краткое замечание (или пустая строка).' ].join("\n"); const base = (process.env.ALEM_BASE_URL || "").replace(/\/+$/, ""); if (!base || !process.env.ALEM_API_KEY || !process.env.ALEM_TEMPLATE_ID) { const e = new Error("Агент Alem не настроен: отсутствуют ALEM_BASE_URL / ALEM_API_KEY / ALEM_TEMPLATE_ID."); e.code = "ALEM_NOT_CONFIGURED"; return Promise.reject(e); } return createAlemSession().then(function (sid) { return askAlem(sid, prompt).then(function (text) { var data = extractJsonFromText(text); if (!data) { const e = new Error('Ответ агента не JSON. Первые 300 символов: ' + String(text || '').replace(/\s+/g, ' ').trim().slice(0, 300)); e.code = "ALEM_NOT_JSON"; throw e; } return data; }); }); } module.exports = { callKfuAgent, extractSseText, extractJsonFromText };