diff --git a/docs/env.md b/docs/env.md new file mode 100644 index 0000000..c321af2 --- /dev/null +++ b/docs/env.md @@ -0,0 +1,39 @@ +# Variables de entorno (Botino) + +## Variables usadas (runtime) + +### Core + +- **`PORT`**: puerto del servidor (default `3000`). +- **`TENANT_KEY`**: key por defecto para resolver tenant (default `piaf`). +- **`DATABASE_URL`**: conexión Postgres. +- **`PG_POOL_MAX`**: tamaño del pool (default `10`). +- **`PG_IDLE_TIMEOUT_MS`**: idle timeout (default `30000`). +- **`PG_CONN_TIMEOUT_MS`**: connection timeout (default `5000`). +- **`APP_ENCRYPTION_KEY`**: clave para desencriptar credenciales Woo en Postgres (`tenant_ecommerce_config`). + +### OpenAI + +- **`OPENAI_API_KEY`** (o `OPENAI_APIKEY`): API key. +- **`OPENAI_MODEL`**: modelo (default `gpt-4o-mini`). + +### WooCommerce (solo fallback si falta config por tenant) + +- **`WOO_CONSUMER_KEY`**: consumer key (se usa solo si en `tenant_ecommerce_config` falta `consumer_key`). +- **`WOO_CONSUMER_SECRET`**: consumer secret (idem). + +## Debug por temas (nuevo) + +Todos aceptan `1/true/yes/on` para activar. + +- **`DEBUG_PERF`**: perf/timings (pipeline + webhook evolution). +- **`DEBUG_WOO_HTTP`**: requests/responses a Woo (status/timing/len). +- **`DEBUG_WOO_PRODUCTS`**: cache/búsquedas de productos Woo. +- **`DEBUG_LLM`**: requests/responses a OpenAI. +- **`DEBUG_EVOLUTION`**: logs del hook evolution (además de perf). +- **`DEBUG_DB`**: reservado para instrumentar queries DB (no está verboso aún). +- **`DEBUG_RESOLVE`**: debug de resolución/ambiguity (pipeline). + +## Notas importantes + +- En producción multi-tenant, Woo se toma de Postgres (`tenant_ecommerce_config`). Si restaurás WordPress y cambian keys/base_url, **tenés que actualizar esa tabla** para el tenant correspondiente; no alcanza con regenerar keys en WP si Botino sigue leyendo las viejas desde Postgres. diff --git a/querys.md b/querys.md new file mode 100644 index 0000000..d24a368 --- /dev/null +++ b/querys.md @@ -0,0 +1,81 @@ +-- INSERT INTO tenants (key, name) +-- VALUES ('piaf', 'Piaf') +-- RETURNING id; + +-- select * from tenants; + +-- truncate table tenants cascade; + +-- eb71b9a7-9ccf-430e-9b25-951a0c589c0f + +-- select * from tenant_ecommerce_config; +-- -- truncate table tenant_ecommerce_config cascade; +-- SELECT set_config('app.encryption_key', 'p1ficolosal3443aAaA', false); + +-- INSERT INTO tenant_ecommerce_config ( +-- tenant_id, +-- provider, +-- base_url, +-- credential_ref, +-- api_version, +-- timeout_ms, +-- enabled, +-- enc_consumer_key, +-- enc_consumer_secret +-- ) VALUES ( +-- 'eb71b9a7-9ccf-430e-9b25-951a0c589c0f'::uuid, +-- 'woo', +-- '', +-- 'secret://woo/piaf', +-- 'wc/v3', +-- 8000, +-- true, +-- pgp_sym_encrypt('ck_241b030c4addffcd5051dd048dccbd00601915e5', current_setting('app.encryption_key')), +-- pgp_sym_encrypt('cs_2fcae6ecc526b1f38f686a19132efa5ac83cb036', current_setting('app.encryption_key')) +-- ); + +-- -- Mover config al tenant correcto +-- UPDATE tenant_ecommerce_config +-- SET tenant_id = 'af906a3a-bd47-4202-8f68-6dac48d33570' +-- WHERE tenant_id = 'eb71b9a7-9ccf-430e-9b25-951a0c589c0f'; + +-- (Opcional) borrar el tenant duplicado en minúsculas si ya no lo usas +-- DELETE FROM tenants WHERE id = 'c32c7b51-ea17-470c-8cab-040e75bdca88'; + +-- INSERT INTO wa_identity_map ( +-- tenant_id, +-- wa_chat_id, +-- provider, +-- external_customer_id, +-- created_at, +-- updated_at +-- ) VALUES ( +-- 'eb71b9a7-9ccf-430e-9b25-951a0c589c0f', -- tu tenant_id +-- '<5491133230322@s.whatsapp.net>', +-- 'woo', +-- 28506, +-- now(), +-- now() +-- ) +-- ON CONFLICT (tenant_id, wa_chat_id) +-- DO UPDATE SET +-- provider = EXCLUDED.provider, +-- external_customer_id = EXCLUDED.external_customer_id, +-- updated_at = now(); + +-- select * from wa_identity_map; + +-- select * from woo_products_cache; +-- BEGIN; + +-- TRUNCATE TABLE +-- wa_messages, +-- conversation_runs, +-- wa_conversation_state, +-- wa_identity_map +-- RESTART IDENTITY +-- CASCADE; + +-- COMMIT; + +-- select * from wa_identity_map; diff --git a/src/handlers/evolution.js b/src/handlers/evolution.js index 4dd3ae9..661bd47 100644 --- a/src/handlers/evolution.js +++ b/src/handlers/evolution.js @@ -1,6 +1,7 @@ import crypto from "crypto"; import { parseEvolutionWebhook } from "../services/evolutionParser.js"; import { resolveTenantId, processMessage } from "../services/pipeline.js"; +import { debug as dbg } from "../services/debug.js"; export async function handleEvolutionWebhook(body) { const t0 = Date.now(); @@ -9,12 +10,14 @@ export async function handleEvolutionWebhook(body) { return { status: 200, payload: { ok: true, ignored: parsed.reason } }; } - console.log("[perf] evolution.webhook.start", { - tenant_key: parsed.tenant_key || null, - chat_id: parsed.chat_id, - message_id: parsed.message_id || null, - ts: parsed.ts || null, - }); + if (dbg.perf || dbg.evolution) { + console.log("[perf] evolution.webhook.start", { + tenant_key: parsed.tenant_key || null, + chat_id: parsed.chat_id, + message_id: parsed.message_id || null, + ts: parsed.ts || null, + }); + } const tenantId = await resolveTenantId({ chat_id: parsed.chat_id, @@ -33,13 +36,15 @@ export async function handleEvolutionWebhook(body) { meta: { pushName: parsed.from_name, ts: parsed.ts, instance: parsed.tenant_key, source: parsed.source }, }); - console.log("[perf] evolution.webhook.end", { - tenantId, - chat_id: parsed.chat_id, - message_id: parsed.message_id || null, - run_id: pm?.run_id || null, - webhook_ms: Date.now() - t0, - }); + if (dbg.perf || dbg.evolution) { + console.log("[perf] evolution.webhook.end", { + tenantId, + chat_id: parsed.chat_id, + message_id: parsed.message_id || null, + run_id: pm?.run_id || null, + webhook_ms: Date.now() - t0, + }); + } return { status: 200, payload: { ok: true } }; } diff --git a/src/services/debug.js b/src/services/debug.js new file mode 100644 index 0000000..8144155 --- /dev/null +++ b/src/services/debug.js @@ -0,0 +1,42 @@ +function envIsOn(v) { + const s = String(v || "").trim().toLowerCase(); + return s === "1" || s === "true" || s === "yes" || s === "on"; +} + +function envIsOff(v) { + const s = String(v || "").trim().toLowerCase(); + return s === "0" || s === "false" || s === "no" || s === "off"; +} + +/** + * Debug flags (por temas) + * + * - DEBUG_PERF: performance/latencias + * - DEBUG_WOO_HTTP: requests/responses a Woo (status, timings, tamaño) + * - DEBUG_WOO_PRODUCTS: caching/queries de productos Woo + * - DEBUG_LLM: requests/responses a OpenAI + * - DEBUG_EVOLUTION: hook evolution + parse + * - DEBUG_DB: queries/latencias DB (si se instrumenta) + * - DEBUG_RESOLVE: debug de resolución/ambiguity (pipeline) + */ +export const debug = { + perf: envIsOn(process.env.DEBUG_PERF), + + wooHttp: envIsOn(process.env.DEBUG_WOO_HTTP), + + wooProducts: envIsOn(process.env.DEBUG_WOO_PRODUCTS), + + llm: envIsOn(process.env.DEBUG_LLM), + + evolution: envIsOn(process.env.DEBUG_EVOLUTION), + + db: envIsOn(process.env.DEBUG_DB), + + resolve: envIsOn(process.env.DEBUG_RESOLVE), +}; + +export function debugOn(flagName) { + return Boolean(debug?.[flagName]); +} + + diff --git a/src/services/openai.js b/src/services/openai.js index 0cca04e..cbc3d51 100644 --- a/src/services/openai.js +++ b/src/services/openai.js @@ -1,5 +1,6 @@ import OpenAI from "openai"; import { z } from "zod"; +import { debug as dbg } from "./debug.js"; let _client = null; let _clientKey = null; @@ -93,7 +94,7 @@ function extractJsonObject(text) { async function jsonCompletion({ system, user, model }) { const openai = getClient(); const chosenModel = model || process.env.OPENAI_MODEL || "gpt-4o-mini"; - const debug = String(process.env.LLM_DEBUG || "") === "1"; + const debug = dbg.llm; if (debug) console.log("[llm] openai.request", { model: chosenModel }); const resp = await openai.chat.completions.create({ diff --git a/src/services/pipeline.js b/src/services/pipeline.js index 16c8364..b7abf58 100644 --- a/src/services/pipeline.js +++ b/src/services/pipeline.js @@ -14,6 +14,7 @@ import { sseSend } from "./sse.js"; import { createWooCustomer, getWooCustomerById } from "./woo.js"; import { llmExtract, llmPlan } from "./openai.js"; import { searchProducts } from "./wooProducts.js"; +import { debug as dbg } from "./debug.js"; function nowIso() { @@ -895,7 +896,7 @@ export async function processMessage({ await touchConversationState({ tenant_id: tenantId, wa_chat_id: chat_id }); mark("start"); - const stageDebug = String(process.env.PIPELINE_DEBUG || "") === "1"; + const stageDebug = dbg.perf; const prev = await getConversationState(tenantId, chat_id); mark("after_getConversationState"); const isStale = @@ -939,7 +940,7 @@ export async function processMessage({ // Reducer de contexto: consolidar slots y evitar “olvidos” mark("before_reduceContext"); - const resolveDebug = String(process.env.RESOLVE_DEBUG || "") === "1"; + const resolveDebug = dbg.resolve; const { extracted, resolvedBasket, unresolved, products_context } = await extractProducts({ tenantId, text, diff --git a/src/services/woo.js b/src/services/woo.js index f38676b..20085d0 100644 --- a/src/services/woo.js +++ b/src/services/woo.js @@ -1,5 +1,6 @@ import crypto from "crypto"; import { getDecryptedTenantEcommerceConfig } from "../db/repo.js"; +import { debug } from "./debug.js"; // --- Simple in-memory lock to serialize work per key (e.g. wa_chat_id) --- const locks = new Map(); @@ -76,9 +77,9 @@ async function fetchWoo({ url, method = "GET", body = null, timeout = 20000, hea body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); - console.log("woo headers in", Date.now() - t0, "ms", res.status); + if (debug.wooHttp) console.log("woo headers in", Date.now() - t0, "ms", res.status); const text = await res.text(); - console.log("woo body in", Date.now() - t0, "ms", "len", text.length); + if (debug.wooHttp) console.log("woo body in", Date.now() - t0, "ms", "len", text.length); let parsed; try { parsed = text ? JSON.parse(text) : null; @@ -89,7 +90,7 @@ async function fetchWoo({ url, method = "GET", body = null, timeout = 20000, hea const err = new Error(`Woo HTTP ${res.status}`); err.status = res.status; err.body = parsed; - err.url = url; + err.url = redactWooUrl(url); err.method = method; throw err; } @@ -97,7 +98,10 @@ async function fetchWoo({ url, method = "GET", body = null, timeout = 20000, hea } catch (e) { const err = new Error(`Woo request failed after ${Date.now() - t0}ms: ${e.message}`); err.cause = e; - err.url = url; + // Propagar status/body para que el caller pueda decidir retries/auth fallback + err.status = e?.status || null; + err.body = e?.body || null; + err.url = redactWooUrl(url); err.method = method; throw err; } finally { @@ -105,6 +109,17 @@ async function fetchWoo({ url, method = "GET", body = null, timeout = 20000, hea } } +function redactWooUrl(url) { + try { + const u = new URL(url); + if (u.searchParams.has("consumer_key")) u.searchParams.set("consumer_key", "REDACTED"); + if (u.searchParams.has("consumer_secret")) u.searchParams.set("consumer_secret", "REDACTED"); + return u.toString(); + } catch { + return url; + } +} + async function searchWooCustomerByEmail({ base, consumerKey, consumerSecret, email, timeout }) { const url = `${base}/customers?email=${encodeURIComponent(email)}`; const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString("base64"); @@ -364,12 +379,16 @@ export async function getWooCustomerById({ tenantId, id }) { })(); const base = cfg.base_url.replace(/\/+$/, ""); - const url = `${base}/customers/${id}?consumer_key=${encodeURIComponent( - consumerKey - )}&consumer_secret=${encodeURIComponent(consumerSecret)}`; + const url = `${base}/customers/${encodeURIComponent(id)}`; + const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString("base64"); try { - const data = await fetchWoo({ url, method: "GET", timeout: cfg.timeout_ms }); + const data = await fetchWoo({ + url, + method: "GET", + timeout: cfg.timeout_ms, + headers: { Authorization: `Basic ${auth}` }, + }); return data; } catch (err) { if (err.status === 404) return null; diff --git a/src/services/wooProducts.js b/src/services/wooProducts.js index 18679ac..67e42f7 100644 --- a/src/services/wooProducts.js +++ b/src/services/wooProducts.js @@ -1,4 +1,5 @@ import { getDecryptedTenantEcommerceConfig, getWooProductCacheById, searchWooProductCache, upsertWooProductCache } from "../db/repo.js"; +import { debug as dbg } from "./debug.js"; async function fetchWoo({ url, method = "GET", body = null, timeout = 8000, headers = {} }) { const controller = new AbortController(); @@ -117,7 +118,7 @@ export async function searchProducts({ maxAgeMs = 24 * 60 * 60 * 1000, forceWoo = false, }) { - const debug = String(process.env.WOO_PRODUCTS_DEBUG || "") === "1"; + const debug = dbg.wooProducts; const lim = Math.max(1, Math.min(50, parseInt(limit, 10) || 10)); const query = String(q || "").trim(); if (!query) return { items: [], source: "none" };