Can't stream in php legacy

Hi, I created my assistant by feeding it JSON via the vector store, I gave it instructions and limited its scope of action.

The result is great on the playground, about 2 to 3 sec to answer.

I try to install it on my site, via a chatbot, it works but it takes 5 to 10 sec to answer me, which is very long.

I spent over 2 weeks trying dozens and dozens of scripts before coming back for help.

I can’t manage to stream the answer and make it appear as I go along, so I don’t have to wait 10sec.

Note that I don’t know compositor or node. I only work in “basic” php.

Thank you very much in advance, I’m close to my goal!

(I had to remove the urls to avoid being blocked by the forum)

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');

require_once 'config.php';

function sendSSE($data) {
    echo "data: " . json_encode($data) . "\n\n";
    ob_flush();
    flush();
}

if($_SERVER['REQUEST_METHOD'] === 'POST') {
    try {
        $input = json_decode(file_get_contents('php://input'), true);
        $question = $input['question'] ?? '';

        $curl = curl_init();
        
        // Configuration de base de cURL
        $headers = [
            'Authorization: Bearer ' . OPENAI_API_KEY,
            'Content-Type: application/json',
            'OpenAI-Beta: assistants=v2'
        ];
        
        // 1. Créer un thread
        curl_setopt_array($curl, [
            CURLOPT_URL => API V1 THREADS,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_POSTFIELDS => '{}'  // Corps vide mais nécessaire
        ]);

        $threadResponse = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        
        $threadData = json_decode($threadResponse, true);
        if (!$threadData || !isset($threadData['id'])) {
            throw new Exception('Erreur création thread: ' . $threadResponse);
        }
        
        $threadId = $threadData['id'];

        // 2. Ajouter le message
        curl_setopt_array($curl, [
            CURLOPT_URL => API V1 THREADS /threads/{$threadId}/messages,
            CURLOPT_POSTFIELDS => json_encode([
                'role' => 'user',
                'content' => $question
            ])
        ]);

        $messageResponse = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

        // 3. Lancer l'assistant
        curl_setopt_array($curl, [
            CURLOPT_URL => API V1 THREADS /threads/{$threadId}/runs,
            CURLOPT_POSTFIELDS => json_encode([
                'assistant_id' => ASSISTANT_ID
            ])
        ]);

        $runResponse = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        
        $runData = json_decode($runResponse, true);
        if (!$runData || !isset($runData['id'])) {
            throw new Exception('Erreur lancement assistant: ' . $runResponse);
        }
        
        $runId = $runData['id'];

        // 4. Vérifier l'état
        $attempts = 0;
        $maxAttempts = 30;  // Augmenté à 30 secondes

        do {
            curl_setopt_array($curl, [
                CURLOPT_URL => API V1 THREAD $threadId}/runs/{$runId},
                CURLOPT_CUSTOMREQUEST => 'GET',
                CURLOPT_POSTFIELDS => null
            ]);

            $statusResponse = curl_exec($curl);
            $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
            
            $statusData = json_decode($statusResponse, true);
            if (!$statusData || !isset($statusData['status'])) {
                throw new Exception('Réponse de status invalide');
            }

            if ($statusData['status'] === 'completed') {
                // 5. Récupérer les messages
                curl_setopt_array($curl, [
                    CURLOPT_URL => API V1 THREADS {$threadId}/messages,
                    CURLOPT_CUSTOMREQUEST => 'GET'
                ]);

                $messagesResponse = curl_exec($curl);
                $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
                
                $messagesData = json_decode($messagesResponse, true);
                if (isset($messagesData['data'][0]['content'][0]['text']['value'])) {
                    $response = $messagesData['data'][0]['content'][0]['text']['value'];
                    $words = explode(' ', $response);
                    foreach ($words as $word) {
                        sendSSE(['content' => $word . ' ']);
                        usleep(40000);
                    }
                    sendSSE(['done' => true]);
                    break;
                }
                throw new Exception('Format de réponse invalide');
            } elseif ($statusData['status'] === 'failed') {
                throw new Exception('Génération échouée: ' . ($statusData['last_error']['message'] ?? 'Raison inconnue'));
            } elseif ($statusData['status'] === 'error') {
                throw new Exception('Erreur pendant la génération: ' . ($statusData['last_error']['message'] ?? 'Raison inconnue'));
            }

            $attempts++;
            if ($attempts >= $maxAttempts) {
                throw new Exception('Timeout après 30 secondes');
            }

            usleep(500000); // 500ms entre chaque vérification
        } while (true);

    } catch (Exception $e) {
        sendSSE(['error' => 'Erreur: ' . $e->getMessage()]);
    } finally {
        if (isset($curl)) {
            curl_close($curl);
        }
    }
}

I’ve added a “typing” effect, but alas it does “typing”, only after receiving the whole response, so not very useful.

$(document).ready(function() {
    let currentEventSource = null;
    let currentBotMessage = null;

	function appendMessage(message, isUser) {
		const messageDiv = $('<div>').addClass('message-container ' + (isUser ? 'user-container' : 'bot-container'));
		const messageContent = $('<div>')
			.addClass('message ' + (isUser ? 'user-msg' : 'bot-msg'));
		
		if (isUser) {
			messageContent.text(message);  // Pour les messages utilisateur, on garde text()
		} else {
			messageContent.html(message);  // Pour le bot, on utilise html()
		}
		
		messageDiv.append(messageContent);
		
		if (isUser) {
			currentBotMessage = null;
			$("#chatbox").append(messageDiv);
		} else if (currentBotMessage === null) {
			currentBotMessage = messageContent;
			$("#chatbox").append(messageDiv);
		}
		
		$("#chatbox").scrollTop($("#chatbox")[0].scrollHeight);
	}

    function sendMessage() {
        const question = $("#userInput").val().trim();
        if (question === "") return;

        // Désactiver l'entrée et le bouton
        $("#userInput").val("").prop("disabled", true);
        $("#sendBtn").prop("disabled", true);
        $("#typingIndicator").show();

        // Afficher le message de l'utilisateur
        appendMessage(question, true);

        // Fermer l'EventSource précédent s'il existe
        if (currentEventSource) {
            currentEventSource.close();
        }

        // Créer une nouvelle requête fetch avec streaming
        fetch('chatbot.php', {  // C'est ce fichier qui va gérer notre requête
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ question: question })
        })
        .then(response => {
			console.log('Réponse reçue:', response);

            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';

            function processText({ done, value }) {
                if (done) {
                    $("#typingIndicator").hide();
                    $("#userInput").prop("disabled", false);
                    $("#sendBtn").prop("disabled", false);
                    $("#userInput").focus();
                    return;
                }

                buffer += decoder.decode(value, { stream: true });
                const lines = buffer.split('\n');
                buffer = lines.pop();

                lines.forEach(line => {
                    if (line.startsWith('data: ')) {
                        try {
                            const data = JSON.parse(line.slice(5));
							if (data.content) {
								if (currentBotMessage) {
									const currentText = currentBotMessage.html() || '';
									currentBotMessage.html(currentText + data.content);
								} else {
									appendMessage(data.content, false);
								}
								$("#chatbox").scrollTop($("#chatbox")[0].scrollHeight);
							}
                        } catch (e) {
                            console.error('Error parsing SSE data:', e);
                        }
                    }
                });

                return reader.read().then(processText);
            }

            return reader.read().then(processText);
        })
        .catch(error => {
            console.error('Error:', error);
            $("#typingIndicator").hide();
            $("#userInput").prop("disabled", false);
            $("#sendBtn").prop("disabled", false);
            appendMessage("Désolé, une erreur s'est produite.", false);
        });
    }

    $("#sendBtn").click(sendMessage);

    $("#userInput").keypress(function(event) {
        if (event.which === 13 && !$("#sendBtn").prop("disabled")) {
            event.preventDefault();
            sendMessage();
        }
    });
});