PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 All 69 releases
← All changes | app/Services/Integrations/FluentBot/FluentBotHelper.php +91 -22 2.2.0 → 2.4.5 View file →
@@ -15,10 +15,24 @@
15 15 'default' => '/responses',
16 16 'ticket_reply' => '/chat/fs',
17 17 ];
18 18
19 - public function generateStreamResponse($prompt, $ticket, $productId, $conversationId = null, $selectedConversations = null, $includeTicketContent = true, $seedMessages = null)
19 + /**
20 + * Resolve the FluentBot API base URL. Defaults to production; overridable
21 + * for local/staging via the `FLUENTBOT_API_BASE_URL` constant (wp-config)
22 + * or the `fluent_support/fluentbot_api_base_url` filter.
23 + */
24 + private function apiBaseUrl(): string
20 25 {
26 + if (defined('FLUENTBOT_API_BASE_URL') && FLUENTBOT_API_BASE_URL) {
27 + return rtrim((string) FLUENTBOT_API_BASE_URL, '/');
28 + }
29 +
30 + return rtrim((string) apply_filters('fluent_support/fluentbot_api_base_url', static::BASE_URL), '/');
31 + }
32 +
33 + public function generateStreamResponse($prompt, $ticket, $productId, $conversationId = null, $selectedConversations = null, $includeTicketContent = true, $seedMessages = null, $webSearch = false, $temperature = 0)
34 + {
21 35 $prompt = apply_filters('fluent_support/generate_response', $prompt, $ticket);
22 36
23 37 $ticketMessages = [];
24 38 if ($selectedConversations !== null) {
@@ -31,8 +45,10 @@
31 45 'source' => 'fluent_support',
32 46 'prompt' => $prompt,
33 47 'stream' => true,
34 48 'chat_id' => $conversationId ?? null,
49 + 'enable_web_search' => (bool) $webSearch,
50 + 'temperature' => (float) $temperature,
35 51 ];
36 52
37 53 if (!empty($ticketMessages)) {
38 54 $payload['ticket_conversation'] = $ticketMessages;
@@ -165,9 +181,9 @@
165 181 }
166 182
167 183 private function makeAPICall(array $payload, string $prompt, int $ticketId, string $type = 'default', $productId = null )
168 184 {
169 - $apiUrl = static::BASE_URL . static::ENDPOINTS[$type];
185 + $apiUrl = $this->apiBaseUrl() . static::ENDPOINTS[$type];
170 186
171 187 $credentials = $this->resolveApiCredentials($productId);
172 188
173 189 if (is_wp_error($credentials)) {
@@ -176,9 +192,9 @@
176 192
177 193 // Use bot_id instead of botId for the new API
178 194 $payload['bot_id'] = $credentials['botId'];
179 195
180 - $api = new FluentBotAPI($apiUrl);
196 + $api = new FluentBotAPI($apiUrl, $credentials['apiKey']);
181 197 $result = $api->makeRequest($ticketId, $prompt, $payload);
182 198
183 199 // For ticket_reply endpoint, return the full result with chat_id
184 200 // For other endpoints, return just the content for backward compatibility
@@ -192,13 +208,16 @@
192 208 }
193 209
194 210 private function makeStreamAPICall(array $payload, string $prompt, int $ticketId, string $type = 'default', $productId = null)
195 211 {
196 - $apiUrl = static::BASE_URL . static::ENDPOINTS[$type];
212 + $apiUrl = $this->apiBaseUrl() . static::ENDPOINTS[$type];
197 213
198 214 $credentials = $this->resolveApiCredentials($productId);
199 215
200 216 if (is_wp_error($credentials)) {
217 + // Emit as an SSE `error` event (not a bare `data:` frame) so the client
218 + // shows a proper error state instead of rendering the JSON as AI text.
219 + echo "event: error\n";
201 220 echo "data: " . json_encode(['error' => $credentials->get_error_message()]) . "\n\n";
202 221 return;
203 222 }
204 223
@@ -204,9 +223,9 @@
204 223
205 224 // Use bot_id instead of botId for the new API
206 225 $payload['bot_id'] = $credentials['botId'];
207 226
208 - $api = new FluentBotAPI($apiUrl);
227 + $api = new FluentBotAPI($apiUrl, $credentials['apiKey']);
209 228 $api->makeStreamRequest($ticketId, $prompt, $payload);
210 229 }
211 230
212 231 public function getChatMessages($chatId, $productId = null, $cursor = null)
@@ -217,9 +236,9 @@
217 236 return $credentials;
218 237 }
219 238
220 239 $botId = $credentials['botId'];
221 - $url = static::BASE_URL . '/bots/' . $botId . '/chats/' . $chatId . '/messages';
240 + $url = $this->apiBaseUrl() . '/bots/' . $botId . '/chats/' . $chatId . '/messages';
222 241
223 242 if ($cursor) {
224 243 $url .= '?cursor=' . urlencode($cursor);
225 244 }
@@ -224,9 +243,9 @@
224 243 $url .= '?cursor=' . urlencode($cursor);
225 244 }
226 245
227 246 $response = wp_remote_get($url, [
228 - 'headers' => ['Content-Type' => 'application/json'],
247 + 'headers' => $this->requestHeaders($credentials['apiKey']),
229 248 'timeout' => 30,
230 249 ]);
231 250
232 251 if (is_wp_error($response)) {
@@ -244,8 +263,30 @@
244 263
245 264 return $body;
246 265 }
247 266
267 + /**
268 + * Reconnect to an in-flight turn and stream its buffered SSE straight to the
269 + * browser. Writes directly to the output stream, so it returns nothing on
270 + * success; a credentials failure is emitted as an SSE `error` frame because the
271 + * caller has already sent SSE headers by this point.
272 + */
273 + public function resumeChatStream($chatId, $productId = null)
274 + {
275 + $credentials = $this->resolveApiCredentials($productId);
276 +
277 + if (is_wp_error($credentials)) {
278 + echo "event: error\n";
279 + echo "data: " . wp_json_encode(['error' => $credentials->get_error_message()]) . "\n\n";
280 + flush();
281 + return;
282 + }
283 +
284 + $url = $this->apiBaseUrl() . '/bots/' . $credentials['botId'] . '/chats/' . $chatId . '/stream';
285 +
286 + (new FluentBotAPI($url, $credentials['apiKey']))->makeResumeStreamRequest();
287 + }
288 +
248 289 public function createFeedback($messageId, $reaction, $comment, $productId = null, $chatId = null)
249 290 {
250 291 $credentials = $this->resolveApiCredentials($productId);
251 292
@@ -264,10 +305,10 @@
264 305 if (!empty($chatId)) {
265 306 $payload['chat_id'] = $chatId;
266 307 }
267 308
268 - $response = wp_remote_post(static::BASE_URL . '/feedbacks', [
269 - 'headers' => ['Content-Type' => 'application/json'],
309 + $response = wp_remote_post($this->apiBaseUrl() . '/feedbacks', [
310 + 'headers' => $this->requestHeaders($credentials['apiKey']),
270 311 'body' => wp_json_encode($payload),
271 312 'timeout' => 15,
272 313 ]);
273 314
@@ -301,11 +342,11 @@
301 342 if (!empty($chatId)) {
302 343 $payload['chat_id'] = $chatId;
303 344 }
304 345
305 - $response = wp_remote_request(static::BASE_URL . '/feedbacks/' . $feedbackId, [
346 + $response = wp_remote_request($this->apiBaseUrl() . '/feedbacks/' . $feedbackId, [
306 347 'method' => 'DELETE',
307 - 'headers' => ['Content-Type' => 'application/json'],
348 + 'headers' => $this->requestHeaders($credentials['apiKey']),
308 349 'body' => wp_json_encode($payload),
309 350 'timeout' => 15,
310 351 ]);
311 352
@@ -371,13 +412,41 @@
371 412 __('Bot ID is not set for this product.', 'fluent-support')
372 413 );
373 414 }
374 415
416 + // The FluentBot API is team-scoped: a single API key authenticates every
417 + // bot in the team, so one general key covers both the general and any
418 + // product-specific bot. It is now required — the API rejects anonymous
419 + // calls — so surface a clear config error instead of a raw 401.
420 + $apiKey = trim((string)($config['generalApiKey'] ?? ''));
421 + if ($apiKey === '') {
422 + return new \WP_Error(
423 + 'missing_api_key',
424 + __('FluentBot API key is not set. Add it in the FluentBot integration settings.', 'fluent-support')
425 + );
426 + }
427 +
375 428 return [
376 - 'botId' => $botId
429 + 'botId' => $botId,
430 + 'apiKey' => $apiKey,
377 431 ];
378 432 }
379 433
434 + /**
435 + * Build the outbound request headers, attaching the team API key as a
436 + * Bearer token so upstream can authenticate + team-scope the call.
437 + */
438 + private function requestHeaders(string $apiKey): array
439 + {
440 + $headers = ['Content-Type' => 'application/json'];
441 +
442 + if ($apiKey !== '') {
443 + $headers['Authorization'] = 'Bearer ' . $apiKey;
444 + }
445 +
446 + return $headers;
447 + }
448 +
380 449 private function getTicketMessages($ticket, $includeTicketContent = true): array
381 450 {
382 451 $messages = [];
383 452 $ticketArray = $ticket->toArray();
@@ -466,29 +535,29 @@
466 535 private function getModifyResponsePresets(): array
467 536 {
468 537 $presets = [
469 538 [
470 - 'label' => 'Improve Writing',
539 + 'label' => __('Improve Writing', 'fluent-support'),
471 540 'text' => 'shorten',
472 541 'description' => 'Use AI to refine the text by removing unnecessary words and making it more concise while retaining the original meaning and key information.'
473 542 ],
474 543 [
475 - 'label' => 'Fix Spelling & Grammar',
544 + 'label' => __('Fix Spelling & Grammar', 'fluent-support'),
476 545 'text' => 'lengthen',
477 546 'description' => 'Apply AI to correct any spelling and grammatical errors in the text, ensuring it is free of mistakes and reads professionally.'
478 547 ],
479 548 [
480 - 'label' => 'Make Shorter',
549 + 'label' => __('Make Shorter', 'fluent-support'),
481 550 'text' => 'friendly',
482 551 'description' => 'AI will modify the text to make it shorter and more casual, making it suitable for informal or friendly communication.'
483 552 ],
484 553 [
485 - 'label' => 'Make Longer',
554 + 'label' => __('Make Longer', 'fluent-support'),
486 555 'text' => 'professional',
487 556 'description' => 'Enhance the text by adding more details and using refined language to make it more formal and detailed, appropriate for professional settings.'
488 557 ],
489 558 [
490 - 'label' => 'Simplify Language',
559 + 'label' => __('Simplify Language', 'fluent-support'),
491 560 'text' => 'simplify',
492 561 'description' => 'Utilize AI to simplify complex phrases and terminology, making the text easier to read and understand for a general audience.'
493 562 ]
494 563 ];
@@ -499,29 +568,29 @@
499 568 private function getCreateResponsePresets(): array
500 569 {
501 570 $presets = [
502 571 [
503 - 'label' => 'Request More Information',
572 + 'label' => __('Request More Information', 'fluent-support'),
504 573 'text' => 'requestInfo',
505 574 'description' => 'Ask the customer to provide additional details or clarification about the issue they reported. This helps in gathering more information to resolve the issue effectively.'
506 575 ],
507 576 [
508 - 'label' => 'Acknowledge Issue',
577 + 'label' => __('Acknowledge Issue', 'fluent-support'),
509 578 'text' => 'acknowledgeIssue',
510 579 'description' => 'Confirm receipt of the customer\'s issue and reassure them that it is being investigated. This demonstrates that their concern is being taken seriously.'
511 580 ],
512 581 [
513 - 'label' => 'Provide Solution',
582 + 'label' => __('Provide Solution', 'fluent-support'),
514 583 'text' => 'provideSolution',
515 584 'description' => 'Offer a comprehensive solution or resolution to the problem described by the customer. This should address their concerns and provide actionable steps to resolve the issue.'
516 585 ],
517 586 [
518 - 'label' => 'Follow Up',
587 + 'label' => __('Follow Up', 'fluent-support'),
519 588 'text' => 'followUp',
520 589 'description' => 'Reach out to the customer after a solution has been provided to ensure that their issue has been resolved to their satisfaction. This helps in confirming the resolution and maintaining good customer relations.'
521 590 ],
522 591 [
523 - 'label' => 'Close Ticket',
592 + 'label' => __('Close Ticket', 'fluent-support'),
524 593 'text' => 'closeTicket',
525 594 'description' => 'Notify the customer that their ticket will be closed as the issue has been resolved. Ensure that all their concerns are addressed before closing the ticket.'
526 595 ]
527 596 ];