PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / admin / helpers / src / chat / user / ai.php

ai.php in VikBooking Hotel Booking Engine & PMS trunk, at admin/helpers/src/chat/user/ai.php

261 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2026 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * Chat AI user.
16 *
17 * @since 1.8.8
18 */
19 class VBOChatUserAi extends VBOChatUseraware implements VBOChatNotifiable
20 {
21 /**
22 * The unique thread ID.
23 *
24 * @var string|null
25 */
26 protected $threadId;
27
28 /**
29 * Class constructor.
30 *
31 * @param string|null $threadId
32 */
33 public function __construct($threadId)
34 {
35 $this->threadId = $threadId;
36 }
37
38 /**
39 * @inheritDoc
40 */
41 public function getID()
42 {
43 return -2;
44 }
45
46 /**
47 * @inheritDoc
48 */
49 public function getName()
50 {
51 return 'AI';
52 }
53
54 /**
55 * @inheritDoc
56 */
57 public function getAvatar()
58 {
59 return VCM_ADMIN_URI . 'assets/css/channels/ai-avatar.png';
60 }
61
62 /**
63 * @inheritDoc
64 *
65 * @see VBOChatUser
66 */
67 public function can(string $scope, ?VBOChatContext $context = null)
68 {
69 // enabled by default, only if the Channel Manager is installed with active AI support
70 if (!static::isSupported()) {
71 return false;
72 }
73
74 if (!$context) {
75 return true;
76 }
77
78 if ($scope === 'chat.send') {
79 // get context metadata
80 $metadata = $context->getMetadata();
81
82 // check whether the AI is still active for this conversation
83 return (bool) ($metadata['use_ai'] ?? true);
84 }
85
86 return true;
87 }
88
89 /**
90 * @inheritDoc
91 *
92 * @see VBOChatNotifiable
93 */
94 public function scheduleNotification(VBOChatMessage $message, VBOChatUser $user)
95 {
96 // ignore auto-replies in case the message wasn't wrote by a guest
97 if ($user->getID() != -1) {
98 return;
99 }
100
101 /** @var VCMChatContext */
102 $context = $message->getContext();
103
104 if (!$this->can('chat.send', $context)) {
105 // prevent automatic replies
106 return;
107 }
108
109 try {
110 // clone the global chat mediator
111 $chat = clone VBOFactory::getChatMediator();
112 // authenticate as AI
113 $chat->authenticate($this);
114
115 // obtain the last 30 messages
116 $messages = $chat->getMessages((new VBOChatSearch)->limit(30)->withContext($context));
117
118 // map the messages for AI compliance
119 $messages = array_map(function($message) {
120 if ($message->getSenderID() == -1) {
121 $role = 'user';
122 } else {
123 $role = 'assistant';
124 }
125
126 $data = [
127 'role' => $role,
128 'content' => $message->getMessage(),
129 'attachments' => [],
130 'user' => $message->getSenderName(),
131 ];
132
133 // include the attachments within the message data object
134 foreach ($message->getAttachments() as $attachment) {
135 $mime = $attachment->getMimeType();
136
137 $supportedMimeTypes = [
138 'image/png',
139 'image/jpg',
140 'image/jpeg',
141 'application/pdf',
142 ];
143
144 // in case the mime type of the attachment is not supported,
145 // do not pass it to the AI service
146 if (!in_array($mime, $supportedMimeTypes)) {
147 continue;
148 }
149
150 $data['attachments'][] = [
151 'name' => $attachment->getName(),
152 'base64' => $attachment->getBase64(),
153 ];
154 }
155
156 return $data;
157 }, $messages);
158
159 // ask AI assistant to elaborate an answer (reverse the messages to have the oldest first)
160 $answer = $this->generateAnswer(array_reverse($messages), $context);
161
162 /** @var VBOChatMessage */
163 $reply = $chat->createMessage([
164 'context' => $context->getAlias(),
165 'id_context' => $context->getID(),
166 'message' => $answer->result, /* use `$reply->text` to get rid of HTML tags */
167 ]);
168
169 // iterate all attachments one by one
170 foreach ($answer->attachments ?? [] as $attachment) {
171 $reply->addAttachment(new VBOChatAttachment([
172 'path' => JPath::clean(VCM_SITE_PATH . '/helpers/chat/attachments/docs/'),
173 'name' => $attachment->name,
174 'filename' => $attachment->filename,
175 'extension' => $attachment->extension,
176 'type' => $attachment->type,
177 ]));
178 }
179
180 // send the AI reply to the guest
181 $chat->send($reply);
182 } catch (Throwable $error) {
183 // make sure the VCM logger is supported
184 if (class_exists('VCMLogDriverJsonlines')) {
185 // create internal logger
186 $logger = new VCMLogDriverJsonlines(
187 // groups logs by day
188 VBO_MEDIA_PATH . '/logs/ai/chatbot/' . JHtml::fetch('date', 'now', 'Y-m-d') . '.php',
189 [
190 // auto-delete log files that were updated more than 4 weeks ago
191 'gc_threshold' => '-4 weeks',
192 ]
193 );
194
195 // log the faced error
196 $logger->error("An error occurred while the AI was trying to reply to the guest.\n\n> {$error}", [
197 'error' => $error->getMessage(),
198 ]);
199 }
200 }
201 }
202
203 /**
204 * Generates an answer according to the provided conversation.
205 *
206 * @param object[] $messages
207 * @param VBOChatContext $context
208 *
209 * @return object
210 */
211 protected function generateAnswer(array $messages, VBOChatContext $context)
212 {
213 try {
214 // ask AI assistant to elaborate an answer
215 $reply = (new VCMAiModelService)->assistant($messages, $this->threadId, 'chatbot');
216 } catch (Exception $error) {
217 if ($error->getCode() == 423) {
218 // we received a 423 error (Locked), meaning that the customer prefers to
219 // talk with a human and doesn't want to receive further AI messages
220 $context->setMetadata('use_ai', 0);
221
222 // inform the user that an administrator has been warned
223 $warning = JText::translate('VBO_CHAT_AI_LOCKED_REPLY');
224 } else if ($error->getCode() == 451) {
225 // we received a 451 error (Unavailable for legal reasons), meaning that AI detected
226 // SPAM or phishing attempts under this conversation
227 $context->setMetadata('banned', 1);
228
229 // inform the user that an administrator has been warned
230 $warning = JText::translate('VBO_CHAT_USER_BANNED_BY_AI');
231 } else {
232 // unknown error detected (timeout, hallucination, etc...), stop the AI to prevent loop errors
233 $context->setMetadata('use_ai', 0);
234
235 // inform the user that an administrator will reply soon
236 $warning = JText::translate('VBO_CHAT_AI_REPLY_DOWNTIME_ERROR');
237 }
238
239 // normalize answer
240 $reply = new stdClass;
241 $reply->result = $warning;
242 $reply->text = strip_tags($warning);
243 }
244
245 return $reply;
246 }
247
248 /**
249 * Checks whether the VCM install owns AI capabilities.
250 *
251 * @return bool
252 */
253 public static function isSupported()
254 {
255 // enabled by default, only if the Channel Manager is installed with active AI support
256 return class_exists('VikChannelManager')
257 && defined('VikChannelManagerConfig::AI')
258 && VikChannelManager::getChannel(VikChannelManagerConfig::AI);
259 }
260 }
261