no more debu legacy

This commit is contained in:
Lucas Tettamanti
2026-01-06 23:42:12 -03:00
parent 8bb21b4edb
commit ce96df9e30
8 changed files with 214 additions and 25 deletions

39
docs/env.md Normal file
View File

@@ -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.

81
querys.md Normal file
View File

@@ -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',
-- '<https://piaf.floda.dev/wp-json/wc/v3>',
-- '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;

View File

@@ -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 } };
}

42
src/services/debug.js Normal file
View File

@@ -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]);
}

View File

@@ -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({

View File

@@ -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,

View File

@@ -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;

View File

@@ -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" };