← All changes
|
app/Services/Integrations/FluentBot/FluentBotAPI.php
+9
-22
2.4.0
→
2.4.5
View file →
| @@ -54,9 +54,8 @@ | ||
| 54 | 54 | $tokenUsage = $responseBody['token_usage'] ?? []; |
| 55 | 55 | $totalTokens = ($tokenUsage['input_tokens'] ?? 0) + ($tokenUsage['output_tokens'] ?? 0); |
| 56 | 56 | do_action('fluent_support/ai_response_success', $ticketId, $prompt, $totalTokens, "FluentBot"); |
| 57 | 57 | |
| 58 | - // Return both content and chat_id if available | |
| 59 | 58 | return [ |
| 60 | 59 | 'content' => $content, |
| 61 | 60 | 'chat_id' => $responseBody['chat_id'] ?? null |
| 62 | 61 | ]; |
| @@ -65,10 +64,9 @@ | ||
| 65 | 64 | public function makeStreamRequest(int $ticketId, $prompt, array $args = []) |
| 66 | 65 | { |
| 67 | 66 | $timeout = apply_filters('fs_ai_request_timeout', 120); |
| 68 | 67 | |
| 69 | - // Use cURL for streaming | |
| 70 | - // Note: Using cURL here because WordPress HTTP API doesn't support streaming SSE responses | |
| 68 | + // Raw cURL because the WP HTTP API can't stream SSE. | |
| 71 | 69 | // phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_init |
| 72 | 70 | // phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_setopt |
| 73 | 71 | // phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_exec |
| 74 | 72 | // phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_close |
| @@ -90,9 +88,8 @@ | ||
| 90 | 88 | $streamTokens = 0; |
| 91 | 89 | |
| 92 | 90 | curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$buffer, &$conversationId, &$streamTokens) { |
| 93 | 91 | foreach ($this->drainSseChunk($data, $buffer) as $parsed) { |
| 94 | - // Store chat_id for later use | |
| 95 | 92 | if ($parsed['event'] === 'chat_id' && !empty($parsed['data'])) { |
| 96 | 93 | $conversationId = $parsed['data'][0]; |
| 97 | 94 | } elseif ($parsed['event'] === 'token_usage' && !empty($parsed['data'])) { |
| 98 | 95 | $usage = json_decode($parsed['data'][0], true); |
| @@ -143,9 +140,8 @@ | ||
| 143 | 140 | private function drainSseChunk(string $data, string &$buffer): array |
| 144 | 141 | { |
| 145 | 142 | $buffer .= $data; |
| 146 | 143 | |
| 147 | - // Process complete SSE events from the AI API | |
| 148 | 144 | $events = explode("\n\n", $buffer); |
| 149 | 145 | $buffer = array_pop($events); // Keep incomplete event in buffer |
| 150 | 146 | |
| 151 | 147 | $parsed = []; |
| @@ -154,9 +150,9 @@ | ||
| 154 | 150 | if (!trim($event)) { |
| 155 | 151 | continue; |
| 156 | 152 | } |
| 157 | 153 | |
| 158 | - // SSE comment/keepalive (starts ":"): forward raw so proxies keep seeing bytes, no idle-timeout. | |
| 154 | + // Forward keepalive comments as-is so proxies don't hit an idle timeout. | |
| 159 | 155 | if (strpos(ltrim($event), ':') === 0) { |
| 160 | 156 | echo $event . "\n\n"; |
| 161 | 157 | flush(); |
| 162 | 158 | continue; |
| @@ -176,9 +172,8 @@ | ||
| 176 | 172 | $eventDataLines[] = (string)substr($line, 6); |
| 177 | 173 | } |
| 178 | 174 | } |
| 179 | 175 | |
| 180 | - // Forward the event to the browser with proper formatting | |
| 181 | 176 | if (!$eventType) { |
| 182 | 177 | continue; |
| 183 | 178 | } |
| 184 | 179 | |
| @@ -183,14 +178,12 @@ | ||
| 183 | 178 | } |
| 184 | 179 | |
| 185 | 180 | echo "event: ".esc_html($eventType)."\n"; |
| 186 | 181 | |
| 187 | - // Include ID if present | |
| 188 | 182 | if ($eventId !== '') { |
| 189 | 183 | echo "id: ".esc_html($eventId)."\n"; |
| 190 | 184 | } |
| 191 | 185 | |
| 192 | - // Handle multiple data lines properly | |
| 193 | 186 | if (!empty($eventDataLines)) { |
| 194 | 187 | foreach ($eventDataLines as $dataLine) { |
| 195 | 188 | // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- raw SSE payload from trusted bot endpoint; rendered output sanitized client-side |
| 196 | 189 | echo "data: ".$dataLine."\n"; |
| @@ -208,16 +201,14 @@ | ||
| 208 | 201 | return $parsed; |
| 209 | 202 | } |
| 210 | 203 | |
| 211 | 204 | /** |
| 212 | - * Reconnect to an in-flight turn: replay its buffered SSE and tail it live. Same wire | |
| 213 | - * format as makeStreamRequest. Upstream ends on `__done__` or an `idle` frame; a total | |
| 214 | - * cap still applies — see the timeout block below. | |
| 205 | + * Reconnect to an in-flight turn: replay its buffered SSE and tail it live. | |
| 206 | + * Same wire format as makeStreamRequest. Upstream ends on `__done__` or an `idle` frame. | |
| 215 | 207 | */ |
| 216 | 208 | public function makeResumeStreamRequest() |
| 217 | 209 | { |
| 218 | - // Use cURL for streaming | |
| 219 | - // Note: Using cURL here because WordPress HTTP API doesn't support streaming SSE responses | |
| 210 | + // Raw cURL because the WP HTTP API can't stream SSE. | |
| 220 | 211 | // phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_init |
| 221 | 212 | // phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_setopt |
| 222 | 213 | // phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_exec |
| 223 | 214 | // phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_close |
| @@ -235,10 +226,9 @@ | ||
| 235 | 226 | |
| 236 | 227 | $buffer = ''; |
| 237 | 228 | |
| 238 | 229 | curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) use (&$buffer) { |
| 239 | - // Stop as soon as the agent navigates away — nothing here needs to | |
| 240 | - // outlive the client, the turn itself is persisted upstream. | |
| 230 | + // Stop when the agent leaves. The turn itself is persisted upstream. | |
| 241 | 231 | if (connection_aborted()) { |
| 242 | 232 | return 0; |
| 243 | 233 | } |
| 244 | 234 | |
| @@ -246,16 +236,13 @@ | ||
| 246 | 236 | |
| 247 | 237 | return strlen($data); |
| 248 | 238 | }); |
| 249 | 239 | |
| 250 | - // Bounded on purpose: connection_aborted() can lag (abort travels browser -> | |
| 251 | - // proxy -> fluent-bot, Apache buffers writes), so a refresh mid-turn can leave | |
| 252 | - // the old tail pinning a worker. The cap makes that self-limiting — the client | |
| 253 | - // treats a cut tail as a body ended without __done__ and refetches. | |
| 240 | + // connection_aborted() can lag behind proxies and buffering, so cap the tail | |
| 241 | + // or a refresh mid-turn can pin a worker. The client refetches a cut tail. | |
| 254 | 242 | curl_setopt($ch, CURLOPT_TIMEOUT, apply_filters('fs_ai_resume_timeout', 120)); |
| 255 | 243 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15); |
| 256 | - // Abort sooner when genuinely stalled: the upstream sends ': keepalive' | |
| 257 | - // during quiet gaps, so a healthy tail always beats this window. | |
| 244 | + // Upstream sends keepalives during quiet gaps, so a stall here means it's dead. | |
| 258 | 245 | curl_setopt($ch, CURLOPT_LOW_SPEED_LIMIT, 1); |
| 259 | 246 | curl_setopt($ch, CURLOPT_LOW_SPEED_TIME, apply_filters('fs_ai_resume_stall_timeout', 60)); |
| 260 | 247 | curl_setopt($ch, CURLOPT_TCP_KEEPALIVE, 1); |
| 261 | 248 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); |