gorka@iand.dev
CrispOpenAIJavaScriptNode.jsExpress.js

Crisp Chatbot

Introduction

This project integrates Crisp’s messaging platform (REST API and RTM API) with OpenAI’s GPT to provide an enhanced chatbot experience. It leverages Node.js for backend operations, handling incoming messages, selecting the user language, responding based on user input, and managing conversation states.

Table of Contents

Features

Dynamic Response Generation

async function getOpenAIResponse(userMessage, country, language, website_id, session_id, email) {
    const heliconeApiKey = process.env.HELICONE_API_KEY;
    const readableWebsiteId = websiteIdToReadableName[website_id]

    try {
        const systemMessageContent = getSystemMessageContent(website_id);
        const systemMessageInfoUserContent = `Language: ${language} (This is the language u need to respond)\nCountry: ${country}`;

        const humanMessageContent = `${userMessage}`;

        const systemMessage = new SystemMessage(systemMessageContent);
        const systemMessageInfoUser = new SystemMessage(systemMessageInfoUserContent);
        const humanMessage = new HumanMessage(humanMessageContent);

        const promptValue = ChatPromptTemplate.fromMessages([
            systemMessage,
            systemMessageInfoUser,
            humanMessage
        ]);
        const chatModel = new ChatOpenAI({
            openAIApiKey: process.env.OPENAI_API_KEY,
            temperature: 0.2,
            modelName: "gpt-3.5-turbo-0125",
            configuration: {
                baseURL: "https://oai.hconeai.com/v1",
                baseOptions: {
                    headers: {
                        "Helicone-Auth": "Bearer " + heliconeApiKey,
                        "Helicone-Property-Session": readableWebsiteId,
                        "Helicone-Property-WebsiteID": website_id,
                        "Helicone-Property-SessionID": session_id,
                        "Helicone-User-Id": email,
                    },
                },
            },
        });

        const chain = promptValue.pipe(chatModel);
        const result = await chain.invoke({});

        return result.content;

    } catch (error) {
        log(`Error in OpenAI response: ${error.toString()}`, 'error');
        return "Got an error, Try again. :(";
    }
}

Event Handling

Implements robust event handling for incoming and outgoing messages, including:

CrispClient.on("message:send", async function (message) {
    const convInfo = await getConversationInfo(message.website_id, message.session_id);
    if (convInfo) {
        const { geolocation, chatbot_status, language, email } = convInfo;
        if (chatbot_status !== "pause") {
            if (!language) {
                if (!email) {
                    await sendMessageEmail(CrispClient, message);
                    await sendMessageLanguage(CrispClient, message);
                } else {
                    await sendMessageLanguage(CrispClient, message);
                }
            } else {
                await startComposing(message.website_id, message.session_id);
                await sendMessageInConversation(CrispClient, message, getOpenAIResponse, geolocation.country, language, email);
                await stopComposing(message.website_id, message.session_id);
            }

        } else {
            log(`Chatbot is paused. Website ID: ${message.website_id} Session ID: ${message.session_id}`, 'info');
        }
    }
});
CrispClient.on("message:updated", async function (message) {
    // Handler functions for different message content IDs
    const handlers = {
        "conversation": async (message) => {
            const resolved_status = message.content.choices[0].selected;
            const resolved_name = message.content.choices[0].value;
            const human_status = message.content.choices[1].selected;
            const human_name = message.content.choices[1].value;

            if (resolved_status) {
                await sendMessageResolver(message.website_id, message.session_id, resolved_name);
                await addSegmentEndedByAI(message.website_id, message.session_id);
                await resolveConversation(message.website_id, message.session_id);
            } else if (human_status) {
                await sendMessageHuman(message.website_id, message.session_id, human_name);
                await addSegmentWaitingForHuman(message.website_id, message.session_id);
                await pauseChatbot(message.website_id, message.session_id);
                await pendingConversation(message.website_id, message.session_id);
            }
        },
        "language": async (message) => {
            const languageChoices = message.content.choices.map((choice, index) => ({
                selected: choice.selected,
                value: ["catala", "castellano", "english", "francais"][index],
            }));

            for (let choice of languageChoices) {
                if (choice.selected) {
                    const convInfo = await getConversationInfo(message.website_id, message.session_id);
                    if (convInfo) {
                        await setLanguage(message.website_id, message.session_id, choice.value);
                        await setLocale(message.website_id, message.session_id, choice.value);
                        await removeMessage(message.website_id, message.session_id, message.fingerprint);
                        await startComposing(message.website_id, message.session_id);
                        await sendMessageBeforeLanguage(CrispClient, message, getOpenAIResponseWithout, choice.value);
                        await stopComposing(message.website_id, message.session_id);
                        break;
                    }
                }
            }
        },
        "email-field": async (message) => {
            const email = message.content.value;
            await setEmail(message.website_id, message.session_id, email);
            await removeMessage(message.website_id, message.session_id, message.fingerprint);
        }
        //todo Add more handlers for other conditions here
    };

    // Execute the handler for the current message content ID if it exists
    if (handlers[message.content.id]) {
        await handlers[message.content.id](message);
    } else {
        // Handle unknown message.content.id or default case
    }
});

Getting the session info

async function getConversationInfo(websiteId, sessionId) {
    const auth = Buffer.from(process.env.CRISP_IDENTIFIER + ":" + process.env.CRISP_KEY).toString('base64');
    const url = `https://api.crisp.chat/v1/website/${websiteId}/conversation/${sessionId}`;

    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Basic ${auth}`,
        'X-Crisp-Tier': 'plugin'
    };

    try {
        const response = await axios.get(url, { headers: headers });
        // console.log("Conversation info:", response.data.data.meta);
        const data = response.data.data.meta;
        const locales = data.device.locales;
        const email = data.email;
        const geolocation = data.device.geolocation;

        const chatMetaData = data.data;
        const chatbot_status = chatMetaData.chatbot;
        const language = chatMetaData.language;
        return {
            geolocation,
            chatbot_status,
            language,
            email,
            locales
        };
    } catch (error) {
        log(`Error getting conversation info: ${error.toString()} Website ID: ${websiteId} Session ID: ${sessionId}`, 'error');
        return null;
    }
}

Multi-language Support

    CrispClient.website.sendMessageInConversation(
        message.website_id, message.session_id,
        {
            type: "picker",
            from: "operator",
            origin: "urn:gorka.vilar:xatgpt-1:0",
            user: {
                nickname,
                avatar
            },
            content: {
                id: "language",
                text: "Select your language:",
                required: true,
                choices: [
                    {
                        value: "catala",
                        icon: "🇦🇩",
                        label: "Català",
                        selected: false,
                    },
                    {
                        value: "castellano",
                        icon: "🇪🇸",
                        label: "Castellano",
                        selected: false,
                    },
                    {
                        value: "english",
                        icon: "🇬🇧",
                        label: "English",
                        selected: false,
                    },
                    {
                        value: "francais",
                        icon: "🇫🇷",
                        label: "Français",
                        selected: false,
                    }
                ]
            }
        })

Setting up the metadata

async function updateConversationMeta(websiteId, sessionId, data, logMessage) {
    const auth = Buffer.from(`${process.env.CRISP_IDENTIFIER}:${process.env.CRISP_KEY}`).toString('base64');
    const url = `https://api.crisp.chat/v1/website/${websiteId}/conversation/${sessionId}/meta`;

    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Basic ${auth}`,
        'X-Crisp-Tier': 'plugin'
    };

    try {
        await axios.patch(url, data, { headers: headers });
        log(`${logMessage} ${JSON.stringify(data)} Website ID: ${websiteId} Session ID: ${sessionId}`, 'info');
    } catch (error) {
        log(`Error updating conversation meta: ${error.toString()} Website ID: ${websiteId} Session ID: ${sessionId}`, 'error');
    }

}
async function setLanguage(websiteId, sessionId, language) {
    const data = {
        data: {
            language: language,
        }
    };

    await updateConversationMeta(websiteId, sessionId, data, "Language set to:");
}

Get in touch

Email me at gorka@iand.dev gorka@iand.dev link