codigoencasa / builderbot

🤖 Crear Chatbot WhatsApp en minutos. Únete a este proyecto OpenSource
https://www.builderbot.app
MIT License
2.44k stars 833 forks source link

[🐛]Al usar base de datos postgres no ingresa al flujo de respuesta esperada #1102

Open Kira1414 opened 1 month ago

Kira1414 commented 1 month ago

¿Que versión estas usando?

v2

¿Sobre que afecta?

Flujo de palabras (Flow)

Describe tu problema

Hola,

De antemano pido disculpas si lo estoy haciendo mal, es la primera vez que hago esto.

No sé si solo pasa en linux o solo sea un caso atipico pero sucede que al encadenar respuestas y almacenar el comportamiento de la respuesta no sigue el flujo, por ejemplo:

const registerFlow = addKeyword<Provider, Database>(['register', utils.setEvent('REGISTER_FLOW')]) .addAnswer(What is your name?, { capture: true }, async (ctx, { state }) => { await state.update({ name: ctx.body }) }) .addAnswer('What is your age?', { capture: true }, async (ctx, { state }) => { await state.update({ age: ctx.body }) }) .addAction(async (_, { flowDynamic, state }) => { await flowDynamic(${state.get('name')}, thanks for your information!: Your age: ${state.get('age')}) })

En el anterior código, cuando el usuario escribe register, el bot responde correctamente preguntando What is your name?, pero al usuario responder con el nombre, ahí se detiene el flujo.

Descubrí que el problema estaba en el archivo base-ts-baileys-postgres/node_modules/@builderbot/database-postgres/dist/index.cjs, mas exactamente en el método save porque en él al armar el array n, se está pasando un parametro t.phone y al parecer este no existe, el que existe es el parámetro t.from

Reproducir error

No response

Información Adicional

No response

emilianoduran commented 1 month ago

Tuve el mismo problema.

SantiagoGaonaC commented 1 month ago

Tengo exactamente el mismo problema Al parecer es acá

https://github.com/codigoencasa/builderbot/blob/27d700d6432f7d592399a499754b7abb94f68c31/packages/database-postgres/src/types.ts#L1-L8

Pero lo que se espera en la tabla es:

https://github.com/codigoencasa/builderbot/blob/27d700d6432f7d592399a499754b7abb94f68c31/packages/database-postgres/src/postgresAdapter.ts#L114-L125

Un ejemplo donde lo inserta:

https://github.com/codigoencasa/builderbot/blob/27d700d6432f7d592399a499754b7abb94f68c31/packages/database-postgres/src/postgresAdapter.ts#L49-L51

Al parecer el error vino de la PR: #1094 en este momento se seguía manteniendo en el dominio phone pero lo que hicieron fue cambiarlo a from, a día de hoy la tabla se sigue construyendo con el campo phone @leifermendez Sería cambiarlo a from el campo en la creación de la base de datos, pero tal vez puede romperle a algunos

SantiagoGaonaC commented 1 month ago

Dejo el código corregido del adapter, cambios en las lineas 52 y 62 ctx.phone a ctx.from, ya que viene como tal from y no viene un phone, algo así sería el json que entra con el type HistoryEntry a las funciones:

📝 Saving history entry: {
  ref: 'ans_8a771232-7f84-4077-85f3-7182c4e2128b',
  keyword: 'key_5a23aa89-395e-4c39-12af-cdf791a4cb03',
  answer: '__call_action__',
  options: {
    media: undefined,
    buttons: [],
    capture: false,
    delay: 0,
    idle: undefined,
    ref: undefined,
    nested: [],
    keyword: {},
    callback: true
  },
  refSerialize: 'bc0a8804f59ee93780e7b4fe65e347af',
  from: 'NUMERODECELULAR(borrado para el ejemplo)´
}

No viene un campo phone como tal

Interface:

export type HistoryEntry = {
    ref: string
    keyword?: string
    answer: string
    refSerialize: string
    phone?: string
    options?: Record<string, any>
    from: string
}

Adapter:


import { MemoryDB } from "@builderbot/bot";
import pg from "pg";

import type { Contact, Credential, HistoryEntry } from "./types";

class PostgreSQLAdapter extends MemoryDB {
    db: any;
    listHistory: HistoryEntry[] = [];
    credentials: Credential = { host: "localhost", user: "", database: "", password: null, port: 5432 };

    constructor(_credentials: Credential) {
        super();
        this.credentials = _credentials;
        this.init().then();
    }

    async init(): Promise<boolean | undefined> {
        try {
            const { Pool } = pg;
            const pool = new Pool(this.credentials);
            const db = await pool.connect();
            this.db = db;
            console.log("🆗 Successful DB Connection");
            this.checkTableExistsAndSP();
            return true;
        } catch (error) {
            console.log("Error", error);
            throw error;
        }
    }

    async getPrevByNumber(from: string): Promise<HistoryEntry | undefined> {
        const query = "SELECT * FROM public.history WHERE phone = $1 ORDER BY created_at DESC LIMIT 1";
        try {
            const result = await this.db.query(query, [from]);
            const row = result.rows[0];

            if (row) {
                row["refSerialize"] = row.refserialize;
                delete row.refserialize;
            }

            return row;
        } catch (error) {
            console.error("Error getting previous entry by number:", error);
            throw error;
        }
    }

    async save(ctx: HistoryEntry): Promise<void> {
        console.log("📝 Saving history entry:", ctx);
        const values = [ctx.ref, ctx.keyword, ctx.answer, ctx.refSerialize, ctx.from, JSON.stringify(ctx.options)];
        const query = "SELECT save_or_update_history_and_contact($1, $2, $3, $4, $5, $6)";

        try {
            await this.db.query(query, values);
        } catch (error) {
            console.error("Error registering history entry:", error);
            throw error;
        }
        this.listHistory.push(ctx);
    }

    async getContact(ctx: HistoryEntry): Promise<Contact | undefined> {
        const from = ctx.from;
        const query = "SELECT * FROM public.contact WHERE phone = $1 LIMIT 1";
        try {
            const result = await this.db.query(query, [from]);
            return result.rows[0];
        } catch (error) {
            if (error instanceof Error) {
                console.error("Error getting contact by number:", error.message);
            }
            throw error;
        }
    }

    async saveContact(ctx: any): Promise<void> {
        // action: u (Actualiza el valor de ctx.values), a (Agrega). Agrega por defecto.
        const _contact = await this.getContact(ctx);
        let jsValues = {};

        if ((ctx?.action ?? "a") === "a") {
            jsValues = { ..._contact?.values, ...(ctx?.values ?? {}) };
        } else {
            jsValues = ctx?.values ?? {};
        }

        const values = [ctx.from, JSON.stringify(jsValues)];
        const query = "SELECT save_or_update_contact($1, $2)";

        try {
            await this.db.query(query, values);
        } catch (error) {
            console.error("🚫 Error saving or updating contact:", error);
            throw error;
        }
    }

    async checkTableExistsAndSP(): Promise<void> {
        const contact = `
            CREATE TABLE IF NOT EXISTS contact (
                id SERIAL PRIMARY KEY,
                phone VARCHAR(255) DEFAULT NULL,
                created_at TIMESTAMP DEFAULT current_timestamp,
                updated_in TIMESTAMP,
                last_interaction TIMESTAMP,
                values JSONB
            )`;
        try {
            await this.db.query(contact);
        } catch (error) {
            console.error("🚫 Error creating the contact table:", error);
            throw error;
        }

        const history = `
            CREATE TABLE IF NOT EXISTS history (
                id SERIAL PRIMARY KEY,
                ref VARCHAR(255) NOT NULL,
                keyword VARCHAR(255),
                answer TEXT NOT NULL,
                refSerialize TEXT NOT NULL,
                phone VARCHAR(255) DEFAULT NULL,
                options JSONB,
                created_at TIMESTAMP DEFAULT current_timestamp,
                updated_in TIMESTAMP,
                contact_id INTEGER REFERENCES contact(id)
            )`;
        try {
            await this.db.query(history);
        } catch (error) {
            console.error("🚫 Error creating the history table:", error);
            throw error;
        }

        await this.createSP();
    }

    async createSP(): Promise<void> {
        const sp_suc = `
        CREATE OR REPLACE FUNCTION save_or_update_contact(
            in_phone VARCHAR(255),
            in_values JSONB
        )
        RETURNS VOID AS
        $$
        DECLARE
            contact_cursor refcursor := 'cur_contact';
            contact_id INT;
        BEGIN
            SELECT id INTO contact_id FROM contact WHERE phone = in_phone;

            IF contact_id IS NULL THEN
                INSERT INTO contact (phone, "values")
                VALUES (in_phone, in_values);
            ELSE
                UPDATE contact SET "values" = in_values, updated_in = current_timestamp
                WHERE id = contact_id;
            END IF;
        END;
        $$ LANGUAGE plpgsql;`;

        try {
            await this.db.query(sp_suc);
        } catch (error) {
            console.error("🚫 Error creating the stored procedure for contact:", error);
            throw error;
        }

        const sp_suhc = `
        CREATE OR REPLACE FUNCTION save_or_update_history_and_contact(
            in_ref VARCHAR(255),
            in_keyword VARCHAR(255),
            in_answer TEXT,
            in_refserialize TEXT,
            in_phone VARCHAR(255),
            in_options JSONB
        )
        RETURNS VOID AS
        $$
        DECLARE
            _contact_id INT;
        BEGIN
            SELECT id INTO _contact_id FROM contact WHERE phone = in_phone;

            IF _contact_id IS NULL THEN
                INSERT INTO contact (phone)
                VALUES (in_phone)
                RETURNING id INTO _contact_id;
            ELSE
                UPDATE contact SET last_interaction = current_timestamp WHERE id = _contact_id;
            END IF;

            INSERT INTO history (ref, keyword, answer, refserialize, phone, options, contact_id, created_at)
            VALUES (in_ref, in_keyword, in_answer, in_refserialize, in_phone, in_options, _contact_id, current_timestamp);

        END;
        $$ LANGUAGE plpgsql;`;

        try {
            await this.db.query(sp_suhc);
        } catch (error) {
            console.error("🚫 Error creating the stored procedure for history:", error);
            throw error;
        }
    }
}

export { PostgreSQLAdapter };
``