"use strict"; const fs = require("fs"); const path = require("path"); const http = require("http"); const https = require("https"); const { URL } = require("url"); let CONFIG_CACHE = null; class LocalAIApi { static createResponse(params, options) { return createResponse(params, options); } static request(pathValue, payload, options) { return request(pathValue, payload, options); } static fetchStatus(aiRequestId, options) { return fetchStatus(aiRequestId, options); } static awaitResponse(aiRequestId, options) { return awaitResponse(aiRequestId, options); } static extractText(response) { return extractText(response); } static decodeJsonFromResponse(response) { return decodeJsonFromResponse(response); } } async function createResponse(params, options = {}) { const payload = { ...(params || {}) }; if (!Array.isArray(payload.input) || payload.input.length === 0) { return { success: false, error: "input_missing", message: 'Parameter "input" is required and must be a non-empty array.', }; } const cfg = config(); if (!payload.model) { payload.model = cfg.defaultModel; } const initial = await request(options.path, payload, options); if (!initial.success) { return initial; } const data = initial.data; if (data && typeof data === "object" && data.ai_request_id) { const pollTimeout = Number(options.poll_timeout ?? 300); const pollInterval = Number(options.poll_interval ?? 5); return await awaitResponse(data.ai_request_id, { interval: pollInterval, timeout: pollTimeout, headers: options.headers, timeout_per_call: options.timeout, verify_tls: options.verify_tls, }); } return initial; } async function request(pathValue, payload = {}, options = {}) { const cfg = config(); const resolvedPath = pathValue || options.path || cfg.responsesPath; if (!resolvedPath) { return { success: false, error: "project_id_missing", message: "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.", }; } if (!cfg.projectUuid) { return { success: false, error: "project_uuid_missing", message: "PROJECT_UUID is not defined; aborting AI request.", }; } const bodyPayload = { ...(payload || {}) }; if (!bodyPayload.project_uuid) { bodyPayload.project_uuid = cfg.projectUuid; } const url = buildUrl(resolvedPath, cfg.baseUrl); const timeout = resolveTimeout(options.timeout, cfg.timeout); const verifyTls = resolveVerifyTls(options.verify_tls, cfg.verifyTls); const headers = { Accept: "application/json", "Content-Type": "application/json", [cfg.projectHeader]: cfg.projectUuid, }; if (Array.isArray(options.headers)) { for (const header of options.headers) { if (typeof header === "string" && header.includes(":")) { const [name, value] = header.split(":", 2); headers[name.trim()] = value.trim(); } } } const body = JSON.stringify(bodyPayload); return sendRequest(url, "POST", body, headers, timeout, verifyTls); } async function fetchStatus(aiRequestId, options = {}) { const cfg = config(); if (!cfg.projectUuid) { return { success: false, error: "project_uuid_missing", message: "PROJECT_UUID is not defined; aborting status check.", }; } const statusPath = resolveStatusPath(aiRequestId, cfg); const url = buildUrl(statusPath, cfg.baseUrl); const timeout = resolveTimeout(options.timeout, cfg.timeout); const verifyTls = resolveVerifyTls(options.verify_tls, cfg.verifyTls); const headers = { Accept: "application/json", [cfg.projectHeader]: cfg.projectUuid, }; if (Array.isArray(options.headers)) { for (const header of options.headers) { if (typeof header === "string" && header.includes(":")) { const [name, value] = header.split(":", 2); headers[name.trim()] = value.trim(); } } } return sendRequest(url, "GET", null, headers, timeout, verifyTls); } async function awaitResponse(aiRequestId, options = {}) { const timeout = Number(options.timeout ?? 300); const interval = Math.max(Number(options.interval ?? 5), 1); const deadline = Date.now() + Math.max(timeout, interval) * 1000; while (true) { const statusResp = await fetchStatus(aiRequestId, { headers: options.headers, timeout: options.timeout_per_call, verify_tls: options.verify_tls, }); if (statusResp.success) { const data = statusResp.data || {}; if (data && typeof data === "object") { if (data.status === "success") { return { success: true, status: 200, data: data.response || data, }; } if (data.status === "failed") { return { success: false, status: 500, error: String(data.error || "AI request failed"), data, }; } } } else { return statusResp; } if (Date.now() >= deadline) { return { success: false, error: "timeout", message: "Timed out waiting for AI response.", }; } await sleep(interval * 1000); } } function extractText(response) { const payload = response && typeof response === "object" ? response.data || response : null; if (!payload || typeof payload !== "object") { return ""; } if (Array.isArray(payload.output)) { let combined = ""; for (const item of payload.output) { if (!item || !Array.isArray(item.content)) { continue; } for (const block of item.content) { if ( block && typeof block === "object" && block.type === "output_text" && typeof block.text === "string" && block.text.length > 0 ) { combined += block.text; } } } if (combined) { return combined; } } if ( payload.choices && payload.choices[0] && payload.choices[0].message && typeof payload.choices[0].message.content === "string" ) { return payload.choices[0].message.content; } return ""; } function decodeJsonFromResponse(response) { const text = extractText(response); if (!text) { throw new Error("No text found in AI response."); } const parsed = parseJson(text); if (parsed.ok && parsed.value && typeof parsed.value === "object") { return parsed.value; } const stripped = stripJsonFence(text); if (stripped !== text) { const parsedStripped = parseJson(stripped); if (parsedStripped.ok && parsedStripped.value && typeof parsedStripped.value === "object") { return parsedStripped.value; } throw new Error(`JSON parse failed after stripping fences: ${parsedStripped.error}`); } throw new Error(`JSON parse failed: ${parsed.error}`); } function config() { if (CONFIG_CACHE) { return CONFIG_CACHE; } ensureEnvLoaded(); const baseUrl = process.env.AI_PROXY_BASE_URL || "https://flatlogic.com"; const projectId = process.env.PROJECT_ID || null; let responsesPath = process.env.AI_RESPONSES_PATH || null; if (!responsesPath && projectId) { responsesPath = `/projects/${projectId}/ai-request`; } const timeout = resolveTimeout(process.env.AI_TIMEOUT, 30); const verifyTls = resolveVerifyTls(process.env.AI_VERIFY_TLS, true); CONFIG_CACHE = { baseUrl, responsesPath, projectId, projectUuid: process.env.PROJECT_UUID || null, projectHeader: process.env.AI_PROJECT_HEADER || "project-uuid", defaultModel: process.env.AI_DEFAULT_MODEL || "gpt-5-mini", timeout, verifyTls, }; return CONFIG_CACHE; } function buildUrl(pathValue, baseUrl) { const trimmed = String(pathValue || "").trim(); if (trimmed === "") { return baseUrl; } if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { return trimmed; } if (trimmed.startsWith("/")) { return `${baseUrl}${trimmed}`; } return `${baseUrl}/${trimmed}`; } function resolveStatusPath(aiRequestId, cfg) { const basePath = (cfg.responsesPath || "").replace(/\/+$/, ""); if (!basePath) { return `/ai-request/${encodeURIComponent(String(aiRequestId))}/status`; } const normalized = basePath.endsWith("/ai-request") ? basePath : `${basePath}/ai-request`; return `${normalized}/${encodeURIComponent(String(aiRequestId))}/status`; } function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls) { return new Promise((resolve) => { let targetUrl; try { targetUrl = new URL(urlString); } catch (err) { resolve({ success: false, error: "invalid_url", message: err.message, }); return; } const isHttps = targetUrl.protocol === "https:"; const requestFn = isHttps ? https.request : http.request; const options = { protocol: targetUrl.protocol, hostname: targetUrl.hostname, port: targetUrl.port || (isHttps ? 443 : 80), path: `${targetUrl.pathname}${targetUrl.search}`, method: method.toUpperCase(), headers, timeout: Math.max(Number(timeoutSeconds || 30), 1) * 1000, }; if (isHttps) { options.rejectUnauthorized = Boolean(verifyTls); } const req = requestFn(options, (res) => { let responseBody = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { responseBody += chunk; }); res.on("end", () => { const status = res.statusCode || 0; const parsed = parseJson(responseBody); const payload = parsed.ok ? parsed.value : responseBody; if (status >= 200 && status < 300) { const result = { success: true, status, data: payload, }; if (!parsed.ok) { result.json_error = parsed.error; } resolve(result); return; } const errorMessage = parsed.ok && payload && typeof payload === "object" ? String(payload.error || payload.message || "AI proxy request failed") : String(responseBody || "AI proxy request failed"); resolve({ success: false, status, error: errorMessage, response: payload, json_error: parsed.ok ? undefined : parsed.error, }); }); }); req.on("timeout", () => { req.destroy(new Error("request_timeout")); }); req.on("error", (err) => { resolve({ success: false, error: "request_failed", message: err.message, }); }); if (body) { req.write(body); } req.end(); }); } function parseJson(value) { if (typeof value !== "string" || value.trim() === "") { return { ok: false, error: "empty_response" }; } try { return { ok: true, value: JSON.parse(value) }; } catch (err) { return { ok: false, error: err.message }; } } function stripJsonFence(text) { const trimmed = text.trim(); if (trimmed.startsWith("```json")) { return trimmed.replace(/^```json/, "").replace(/```$/, "").trim(); } if (trimmed.startsWith("```")) { return trimmed.replace(/^```/, "").replace(/```$/, "").trim(); } return text; } function resolveTimeout(value, fallback) { const parsed = Number.parseInt(String(value ?? fallback), 10); return Number.isNaN(parsed) ? Number(fallback) : parsed; } function resolveVerifyTls(value, fallback) { if (value === undefined || value === null) { return Boolean(fallback); } return String(value).toLowerCase() !== "false" && String(value) !== "0"; } function ensureEnvLoaded() { if (process.env.PROJECT_UUID && process.env.PROJECT_ID) { return; } const envPath = path.resolve(__dirname, "../../../../.env"); if (!fs.existsSync(envPath)) { return; } let content; try { content = fs.readFileSync(envPath, "utf8"); } catch (err) { throw new Error(`Failed to read executor .env: ${err.message}`); } for (const line of content.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) { continue; } const [rawKey, ...rest] = trimmed.split("="); const key = rawKey.trim(); if (!key) { continue; } const value = rest.join("=").trim().replace(/^['"]|['"]$/g, ""); if (!process.env[key]) { process.env[key] = value; } } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } module.exports = { LocalAIApi, createResponse, request, fetchStatus, awaitResponse, extractText, decodeJsonFromResponse, };