Assistant API + function calling

hi. i’m trying to implement function calling to Assistant API but i’m having problem calling the function since it’s on the client side. here’s my code example:

openAIconfig.js (server-side):

require('dotenv').config();
const OpenAI = require('openai');
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const winston = require('winston');
const app = express();

// Environment variables
const port = process.env.PORT || 3000;
const apiKey = process.env.API_KEY;

// OpenAI client setup
const openai = new OpenAI({ apiKey });

// Logger setup (using winston)
const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' }),
    ],
});

// Middleware for rate limiting
const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
    standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
    legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});

// Express server setup
app.use(cors());
app.use(bodyParser.json());
app.use(limiter); // Apply the rate limiter to all requests
const path = require('path');

// Serve static files from the 'public' directory
app.use(express.static(path.join(__dirname, 'public')));

// Function to handle conversation with GPT
async function askGPT(question, threadId) {
    try {
        if (!threadId) {
            const thread = await openai.beta.threads.create();
            threadId = thread.id;
        }

        await openai.beta.threads.messages.create(threadId, {
            role: "user",
            content: question
        });

        const run = await openai.beta.threads.runs.create(threadId, { assistant_id: 'asst_123' });

        let runStatus = await openai.beta.threads.runs.retrieve(threadId, run.id);

        while (runStatus.status !== 'completed' && runStatus.status !== 'requires_action') {
            console.log("Run is not yet completed, checking again in 2 seconds");
            await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for 2 seconds
            runStatus = await openai.beta.threads.runs.retrieve(threadId, run.id);
        }
    
        // Check if the run requires action
        if (runStatus.status === 'requires_action') {
            console.log("Requires Action");
            const requiredActions = runStatus.required_action.submit_tool_outputs.tool_calls;
            console.log(requiredActions);
            // Handle the required actions
        } else if (runStatus.status === 'completed') {
            // Handle the completed run
            const messagesResponse = await openai.beta.threads.messages.list(threadId);
            const aiMessages = messagesResponse.data.filter(msg => msg.run_id === run.id && msg.role === "assistant");
            return { response: aiMessages[aiMessages.length - 1].content[0].text.value, threadId };
        }

    } catch (error) {
        logger.error('Error in askGPT:', error);
        throw new Error('An error occurred while processing your request.');
    }
}

// Endpoint to handle chat requests
app.post('/ask', async (req, res) => {
    const { question, threadId } = req.body;
    if (typeof question !== 'string' || question.trim() === '') {
        return res.status(400).send('Invalid question');
    }

    try {
        const { response, newThreadId } = await askGPT(question, threadId);
        res.json({ answer: response, threadId: newThreadId });
    } catch (error) {
        logger.error('Error in /ask endpoint:', error);
        res.status(500).send('Error processing your request');
    }
});

// Start the server
app.listen(port, () => {
    logger.info(`Server running on http://localhost:${port}`);
});

script.js (client-side):

document.getElementById('open-modal').addEventListener('click', function() {
    document.getElementById('chat-modal').style.display = 'flex';
});

document.getElementById('close-modal').addEventListener('click', function() {
    document.getElementById('chat-modal').style.display = 'none';
});

document.getElementById('send-button').addEventListener('click', sendQuestion);
document.getElementById('new-thread-button').addEventListener('click', newThread);

function startTolstoyWidget() {
    window.tolstoyWidget.start();
}

function sendQuestion() {
    const userInput = document.getElementById('user-input').value;
    const chatOutput = document.getElementById('chat-output');
    const loadingMessage = document.getElementById('loading-message');

    if (userInput.trim() !== '') {
        chatOutput.innerHTML += `<div class="user">${userInput}</div>`;
        loadingMessage.style.display = 'block';

        fetch('/ask', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ question: userInput }),
        })
        .then(response => response.json())
        .then(data => {
            loadingMessage.style.display = 'none';
            const formattedResponse = formatResponse(data.answer);
            chatOutput.innerHTML += `<div class="ai">${formattedResponse}</div>`;
            chatOutput.scrollTop = chatOutput.scrollHeight;
        })
        .catch((error) => {
            console.error('Error:', error);
            loadingMessage.style.display = 'none';
        });

        document.getElementById('user-input').value = '';
    }
}

function formatResponse(response) {
    // Check for numbered lists
    if (/^\d+\./m.test(response)) {
        // Split by lines and filter out empty lines
        const lines = response.split('\n').filter(line => line.trim() !== '');
        // Convert lines into list items, checking for numbered list format
        const listItems = lines.map((line) => {
            if (/^\d+\./.test(line.trim())) {
                return `<li>${line.trim().substring(line.indexOf(' ') + 1)}</li>`;
            } else {
                return line; // Return line as-is if it doesn't match the list format
            }
        }).join('');
        // Wrap in <ol> tag if any list item is detected
        if (listItems.includes('<li>')) {
            return `<ol>${listItems}</ol>`;
        }
    }

    // Check for bullet points
    if (response.includes('\n- ')) {
        const items = response.split('\n- ').slice(1); // Split and remove the first empty element
        const listItems = items.map(item => `<li>${item.trim()}</li>`).join('');
        return `<ul>${listItems}</ul>`;
    }
    
    // Return the response as-is if no list patterns are detected
    return response;
}

function newThread() {
    document.getElementById('chat-output').innerHTML = '';
    fetch('/reset', { method: 'POST' })
    .catch((error) => {
        console.error('Error resetting conversation:', error);
    });
}

so the function i’m trying to run is the startTolstoyWidget(), but right now i’m not sure how can fire it.

i already defined the same function in open AI playground. now when the run status becomes requires_action, the next step would be completing the run by submitting tool outputs to run. but after that i’m not sure what will be the next step now.

i can understand the documentation but only if the function is in the server side, but if it’s already in the client side, i can’t wrap my head around it. any ideas? thank you!

After completing the run, the new message (answer) generated by GPT will be added to your message thread. To verify, you can retrieve the messages to see if the answer is there.

After that, you can repeat the process, starting again with the user’s question to the thread, create a run, provide the submitting tool output and so on.

The client side is the browser and in term of concepts, it works pretty much the same, you need to use your OpenAI client instance to retrieve/create thread, then append the messages, then create the run, submitting otuput…etc. the only different is you do it right in the browser, which poses security risk (suhc as your API key) as anybody can view your code in the browser.