Skip to content

[Bug]: Stream response dos not return the runs()->steps() at first message. #506

@beshoo

Description

@beshoo

Description

I am running an assistant. and i need to include the resource where the information has been found .
the problem this dos not work when you send the very first message!

private function streamResponse($client, $threadId, $runParameters ,$chat_title)
    {
        return response()->stream(function () use ($client, $threadId, $runParameters, $chat_title) {
            header('Content-Type: text/event-stream');
            header('Cache-Control: no-cache');
            header('Connection: keep-alive');

            $runResponse = $client->threads()->runs()->createStreamed($threadId, $runParameters);
            $currentMessageId = null;
            $buffer = '';
            $messageRunMap = [];
            $currentRunId = null;
            $currentSources = [];
            $citedFileIds = [];

            // Get all runs for this thread to build the file mapping
            $runs = $client->threads()->runs()->list($threadId);

            if (!empty($runs['data'])) {
                foreach ($runs['data'] as $run) {
                    $stepsResponse = $client->threads()->runs()->steps()->list(
                        threadId: $threadId,
                        runId: $run['id']
                    );

                    $messageRunMap[$run['id']] = $this->buildFileNameMap($stepsResponse->toArray());
                }
            }

            foreach ($runResponse as $response) {
                switch ($response->event) {
                    case 'thread.run.created':
                        $currentRunId = $response->response->id;
                        break;

                    case 'thread.message.delta':
                        if ($response->response->delta->content) {
                            foreach ($response->response->delta->content as $content) {
                                if ($content->type === 'text' && !empty($content->text->value)) {
                                    if (empty($currentMessageId)) {
                                        $currentMessageId = $response->response->id;
                                    }

                                    // Check for file citations in annotations
                                    if (!empty($content->text->annotations)) {
                                        foreach ($content->text->annotations as $annotation) {
                                            if ($annotation->type === 'file_citation' && !empty($annotation->fileCitation->fileId)) {
                                                $fileId = $annotation->fileCitation->fileId;
                                                $citedFileIds[] = $fileId;

                                                // Find the source name in messageRunMap
                                                foreach ($messageRunMap as $runSources) {
                                                    if (isset($runSources[$fileId])) {
                                                        $currentSources[] = $runSources[$fileId];
                                                        break;
                                                    }
                                                }
                                            }
                                        }
                                    }

                                    // Clean and add content to buffer
                                    $buffer .= $this->cleanCitationMarkers($content->text->value);

                                    // Split buffer into chunks at natural break points
                                    $chunks = preg_split('/(?<=[\.\!\?\s])/u', $buffer, -1, PREG_SPLIT_NO_EMPTY);

                                    if (count($chunks) >= 1) {
                                        $buffer = array_pop($chunks);

                                        foreach ($chunks as $chunk) {
                                            $chunk = trim($chunk);
                                            if (empty($chunk)) {
                                                continue;
                                            }

                                            // Check if this chunk starts with a number followed by a dot
                                            $startsWithNumber = preg_match('/^\d+\./', $chunk);

                                            // Add <br> before numbered items
                                            if ($startsWithNumber) {
                                                $chunk = "<br>" . $chunk;
                                            }

                                            // Add <br> after chunks ending with period (but not for numbered items)
                                            if (str_ends_with($chunk, '.')) {
                                                // Check if the chunk ends with a number followed by a dot (e.g., "1.")
                                                if (!preg_match('/\d+\.$/', $chunk)) {
                                                    $chunk .= "<br>";
                                                }
                                            }

                                     /*       if (empty($currentSources)) {
                                                $currentSources[] = "القانون السعودي";
                                                $currentSources[] = "القانون السعودي 2";
                                            }*/
                                            echo "data: " . json_encode([
                                                    'title' => $chat_title,
                                                    'content' => $chunk,
                                                    'sources' => array_values(array_unique($currentSources))
                                                ], JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR) . "\n\n";

                                            ob_flush();
                                            flush();
                                            usleep(50000);
                                        }
                                    }
                                }
                            }
                        }
                        break;

                    case 'thread.run.completed':
                        // Always send the final buffer content, even if empty
                        echo "data: " . json_encode([
                                'title' => $chat_title,
                                'content' => $buffer,
                                'sources' => array_values(array_unique($currentSources))
                            ], JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR) . "\n\n";
                        break;

                    case 'thread.run.requires_action':
                        // Handle tool calls if needed
                        break;

                    case 'thread.run.failed':
                        echo "data: " . json_encode([
                                'error' => 'Run failed'
                            ]) . "\n\n";
                        break;
                }
            }
        }, 200, [
            'Cache-Control' => 'no-cache',
            'Content-Type' => 'text/event-stream',
            'Connection' => 'keep-alive',
            'X-Accel-Buffering' => 'no'
        ]);
    }
    
    
    
    private function buildFileNameMap($steps)
    {
        static $cache = [];

        $cacheKey = md5(serialize($steps));
        if (isset($cache[$cacheKey])) {
            return $cache[$cacheKey];
        }

        $fileNameMap = [];

        if (isset($steps['data'])) {
            foreach ($steps['data'] as $step) {
                if ($step['type'] === 'tool_calls' &&
                    isset($step['step_details']['tool_calls'])) {
                    foreach ($step['step_details']['tool_calls'] as $toolCall) {
                        if (isset($toolCall['file_search']['results'])) {
                            foreach ($toolCall['file_search']['results'] as $result) {
                                if (isset($result['file_id']) && isset($result['file_name'])) {
                                    $fileNameMap[$result['file_id']] = str_replace(
                                        '_',
                                        ' ',
                                        basename($result['file_name'], '.txt')
                                    );
                                }
                            }
                        }
                    }
                }
            }
        }

        $cache[$cacheKey] = $fileNameMap;
        return $fileNameMap;
    }

Steps To Reproduce

Please read the code , you will understand the flow.

OpenAI PHP Client Version

latest one

PHP Version

8.3

Notes

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions