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!