no more debu legacy
This commit is contained in:
39
docs/env.md
Normal file
39
docs/env.md
Normal 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
81
querys.md
Normal 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;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { parseEvolutionWebhook } from "../services/evolutionParser.js";
|
import { parseEvolutionWebhook } from "../services/evolutionParser.js";
|
||||||
import { resolveTenantId, processMessage } from "../services/pipeline.js";
|
import { resolveTenantId, processMessage } from "../services/pipeline.js";
|
||||||
|
import { debug as dbg } from "../services/debug.js";
|
||||||
|
|
||||||
export async function handleEvolutionWebhook(body) {
|
export async function handleEvolutionWebhook(body) {
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
@@ -9,12 +10,14 @@ export async function handleEvolutionWebhook(body) {
|
|||||||
return { status: 200, payload: { ok: true, ignored: parsed.reason } };
|
return { status: 200, payload: { ok: true, ignored: parsed.reason } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dbg.perf || dbg.evolution) {
|
||||||
console.log("[perf] evolution.webhook.start", {
|
console.log("[perf] evolution.webhook.start", {
|
||||||
tenant_key: parsed.tenant_key || null,
|
tenant_key: parsed.tenant_key || null,
|
||||||
chat_id: parsed.chat_id,
|
chat_id: parsed.chat_id,
|
||||||
message_id: parsed.message_id || null,
|
message_id: parsed.message_id || null,
|
||||||
ts: parsed.ts || null,
|
ts: parsed.ts || null,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const tenantId = await resolveTenantId({
|
const tenantId = await resolveTenantId({
|
||||||
chat_id: parsed.chat_id,
|
chat_id: parsed.chat_id,
|
||||||
@@ -33,6 +36,7 @@ export async function handleEvolutionWebhook(body) {
|
|||||||
meta: { pushName: parsed.from_name, ts: parsed.ts, instance: parsed.tenant_key, source: parsed.source },
|
meta: { pushName: parsed.from_name, ts: parsed.ts, instance: parsed.tenant_key, source: parsed.source },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (dbg.perf || dbg.evolution) {
|
||||||
console.log("[perf] evolution.webhook.end", {
|
console.log("[perf] evolution.webhook.end", {
|
||||||
tenantId,
|
tenantId,
|
||||||
chat_id: parsed.chat_id,
|
chat_id: parsed.chat_id,
|
||||||
@@ -40,6 +44,7 @@ export async function handleEvolutionWebhook(body) {
|
|||||||
run_id: pm?.run_id || null,
|
run_id: pm?.run_id || null,
|
||||||
webhook_ms: Date.now() - t0,
|
webhook_ms: Date.now() - t0,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { status: 200, payload: { ok: true } };
|
return { status: 200, payload: { ok: true } };
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/services/debug.js
Normal file
42
src/services/debug.js
Normal 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { debug as dbg } from "./debug.js";
|
||||||
|
|
||||||
let _client = null;
|
let _client = null;
|
||||||
let _clientKey = null;
|
let _clientKey = null;
|
||||||
@@ -93,7 +94,7 @@ function extractJsonObject(text) {
|
|||||||
async function jsonCompletion({ system, user, model }) {
|
async function jsonCompletion({ system, user, model }) {
|
||||||
const openai = getClient();
|
const openai = getClient();
|
||||||
const chosenModel = model || process.env.OPENAI_MODEL || "gpt-4o-mini";
|
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 });
|
if (debug) console.log("[llm] openai.request", { model: chosenModel });
|
||||||
|
|
||||||
const resp = await openai.chat.completions.create({
|
const resp = await openai.chat.completions.create({
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { sseSend } from "./sse.js";
|
|||||||
import { createWooCustomer, getWooCustomerById } from "./woo.js";
|
import { createWooCustomer, getWooCustomerById } from "./woo.js";
|
||||||
import { llmExtract, llmPlan } from "./openai.js";
|
import { llmExtract, llmPlan } from "./openai.js";
|
||||||
import { searchProducts } from "./wooProducts.js";
|
import { searchProducts } from "./wooProducts.js";
|
||||||
|
import { debug as dbg } from "./debug.js";
|
||||||
|
|
||||||
|
|
||||||
function nowIso() {
|
function nowIso() {
|
||||||
@@ -895,7 +896,7 @@ export async function processMessage({
|
|||||||
await touchConversationState({ tenant_id: tenantId, wa_chat_id: chat_id });
|
await touchConversationState({ tenant_id: tenantId, wa_chat_id: chat_id });
|
||||||
|
|
||||||
mark("start");
|
mark("start");
|
||||||
const stageDebug = String(process.env.PIPELINE_DEBUG || "") === "1";
|
const stageDebug = dbg.perf;
|
||||||
const prev = await getConversationState(tenantId, chat_id);
|
const prev = await getConversationState(tenantId, chat_id);
|
||||||
mark("after_getConversationState");
|
mark("after_getConversationState");
|
||||||
const isStale =
|
const isStale =
|
||||||
@@ -939,7 +940,7 @@ export async function processMessage({
|
|||||||
|
|
||||||
// Reducer de contexto: consolidar slots y evitar “olvidos”
|
// Reducer de contexto: consolidar slots y evitar “olvidos”
|
||||||
mark("before_reduceContext");
|
mark("before_reduceContext");
|
||||||
const resolveDebug = String(process.env.RESOLVE_DEBUG || "") === "1";
|
const resolveDebug = dbg.resolve;
|
||||||
const { extracted, resolvedBasket, unresolved, products_context } = await extractProducts({
|
const { extracted, resolvedBasket, unresolved, products_context } = await extractProducts({
|
||||||
tenantId,
|
tenantId,
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { getDecryptedTenantEcommerceConfig } from "../db/repo.js";
|
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) ---
|
// --- Simple in-memory lock to serialize work per key (e.g. wa_chat_id) ---
|
||||||
const locks = new Map();
|
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,
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
signal: controller.signal,
|
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();
|
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;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
parsed = text ? JSON.parse(text) : null;
|
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}`);
|
const err = new Error(`Woo HTTP ${res.status}`);
|
||||||
err.status = res.status;
|
err.status = res.status;
|
||||||
err.body = parsed;
|
err.body = parsed;
|
||||||
err.url = url;
|
err.url = redactWooUrl(url);
|
||||||
err.method = method;
|
err.method = method;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@@ -97,7 +98,10 @@ async function fetchWoo({ url, method = "GET", body = null, timeout = 20000, hea
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = new Error(`Woo request failed after ${Date.now() - t0}ms: ${e.message}`);
|
const err = new Error(`Woo request failed after ${Date.now() - t0}ms: ${e.message}`);
|
||||||
err.cause = e;
|
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;
|
err.method = method;
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} 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 }) {
|
async function searchWooCustomerByEmail({ base, consumerKey, consumerSecret, email, timeout }) {
|
||||||
const url = `${base}/customers?email=${encodeURIComponent(email)}`;
|
const url = `${base}/customers?email=${encodeURIComponent(email)}`;
|
||||||
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString("base64");
|
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 base = cfg.base_url.replace(/\/+$/, "");
|
||||||
const url = `${base}/customers/${id}?consumer_key=${encodeURIComponent(
|
const url = `${base}/customers/${encodeURIComponent(id)}`;
|
||||||
consumerKey
|
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString("base64");
|
||||||
)}&consumer_secret=${encodeURIComponent(consumerSecret)}`;
|
|
||||||
|
|
||||||
try {
|
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;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.status === 404) return null;
|
if (err.status === 404) return null;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getDecryptedTenantEcommerceConfig, getWooProductCacheById, searchWooProductCache, upsertWooProductCache } from "../db/repo.js";
|
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 = {} }) {
|
async function fetchWoo({ url, method = "GET", body = null, timeout = 8000, headers = {} }) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@@ -117,7 +118,7 @@ export async function searchProducts({
|
|||||||
maxAgeMs = 24 * 60 * 60 * 1000,
|
maxAgeMs = 24 * 60 * 60 * 1000,
|
||||||
forceWoo = false,
|
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 lim = Math.max(1, Math.min(50, parseInt(limit, 10) || 10));
|
||||||
const query = String(q || "").trim();
|
const query = String(q || "").trim();
|
||||||
if (!query) return { items: [], source: "none" };
|
if (!query) return { items: [], source: "none" };
|
||||||
|
|||||||
Reference in New Issue
Block a user