BxDolAiTriggerMessage.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup UnaCore UNA Core
  7. * @{
  8. */
  9. class BxDolAiTriggerMessage extends BxDolAiTrigger
  10. {
  11. protected $_sType = 'message';
  12. /**
  13. * The agent's out-of-domain answer: not posted into the talk.
  14. */
  15. const NO_REPLY = 'NO_REPLY';
  16. protected function usesSenderChatHistory()
  17. {
  18. return true;
  19. }
  20. protected function appliesChatLimits()
  21. {
  22. return true;
  23. }
  24. /**
  25. * Messenger history is per talk (lot), not mixed with the sender's Studio chat.
  26. */
  27. protected function getCallChatHistoryParams($aAgent, $mixedParams)
  28. {
  29. $aParams = parent::getCallChatHistoryParams($aAgent, $mixedParams);
  30. $iLot = is_array($mixedParams) ? (int)($mixedParams['message_lot_id'] ?? 0) : 0;
  31. if ($iLot > 0)
  32. $aParams['chat_history_subindex'] = 'lot' . $iLot;
  33. return $aParams;
  34. }
  35. /**
  36. * limit_message is for chat widgets; in a talk the agent just goes quiet.
  37. */
  38. protected function getChatLimitReply($aAgent)
  39. {
  40. return null;
  41. }
  42. public function response($oAlert)
  43. {
  44. BxDolAiTrigger::getInstance($this->getType())->handle($oAlert);
  45. }
  46. public function handle($mixed = null)
  47. {
  48. $oAlert = $mixed;
  49. if (!$oAlert || 'bx_messenger' != $oAlert->sUnit || 'got_jot' != $oAlert->sAction)
  50. return false;
  51. $aJotInfo = isset($oAlert->aExtras['subobject_info']) && is_array($oAlert->aExtras['subobject_info'])
  52. ? $oAlert->aExtras['subobject_info']
  53. : [];
  54. return $this->processMessage(
  55. $oAlert->iSender,
  56. (int)($oAlert->aExtras['recipient_id'] ?? 0),
  57. (int)$oAlert->iObject,
  58. (int)($oAlert->aExtras['subobject_id'] ?? 0),
  59. $aJotInfo
  60. );
  61. }
  62. public function getAgentsByProfileId($iProfileId)
  63. {
  64. $oDb = new BxDolAiQuery();
  65. return $oDb->getAgentsByProfileId($iProfileId);
  66. }
  67. /**
  68. * Drains `$GLOBALS['glAgentsCallQueue']` once; jobs queued while a reply is
  69. * being posted (got_jot fires again) are picked up by the same loop, never re-run.
  70. */
  71. public function processCallQueue($bFinishRequest = true, $bExit = true)
  72. {
  73. if ($bFinishRequest) {
  74. if (function_exists('fastcgi_finish_request')) {
  75. fastcgi_finish_request();
  76. } else {
  77. @ob_end_flush();
  78. @flush();
  79. }
  80. }
  81. if (empty($GLOBALS['glAgentsCallQueueLock'])) {
  82. $GLOBALS['glAgentsCallQueueLock'] = true;
  83. try {
  84. while (!empty($GLOBALS['glAgentsCallQueue'])) {
  85. $aBatch = $GLOBALS['glAgentsCallQueue'];
  86. $GLOBALS['glAgentsCallQueue'] = [];
  87. foreach ($aBatch as $r) {
  88. $sType = $r['type'] ?? $this->getType();
  89. $mixedReply = BxDolAiTrigger::getInstance($sType)->call($r['agent'], $r['params']);
  90. if (!is_string($mixedReply) || $mixedReply === '')
  91. continue;
  92. $this->replyToMessage($r['agent'], $r['params'], $mixedReply);
  93. }
  94. }
  95. } finally {
  96. $GLOBALS['glAgentsCallQueueLock'] = false;
  97. }
  98. }
  99. if ($bExit)
  100. exit(0);
  101. }
  102. /**
  103. * Post the agent's reply (markdown) into the talk the message came from.
  104. *
  105. * @param array $aParams the call params (`sender_profile_id`, `message_lot_id`, `message_id`)
  106. */
  107. public function replyToMessage($aAgent, $aParams, $sReply)
  108. {
  109. if (is_string($aParams))
  110. $aParams = json_decode($aParams, true) ?: [];
  111. if (!is_array($aParams))
  112. $aParams = [];
  113. $oParsedown = new Parsedown();
  114. $oParsedown->setSafeMode(true);
  115. $sHtml = $oParsedown->text((string)$sReply);
  116. return $this->sendMessengerMessage(
  117. (int)($aAgent['profile_id'] ?? 0),
  118. (int)($aParams['sender_profile_id'] ?? 0),
  119. str_replace("\n", '', $sHtml),
  120. (int)($aParams['message_lot_id'] ?? 0),
  121. (int)($aParams['message_id'] ?? 0)
  122. );
  123. }
  124. /**
  125. * @param int $iLotId talk to post into; 0 = a private talk with $iRecipient (resolved from $iJotId when given)
  126. * @param int $iJotId the message being answered — used to find the talk and to stop agent-on-agent loops
  127. */
  128. public function sendMessengerMessage($iSender, $iRecipient, $sMsg, $iLotId = 0, $iJotId = 0)
  129. {
  130. $oMessengerModule = BxDolModule::getInstance('bx_messenger');
  131. if (!$oMessengerModule)
  132. return false;
  133. $iSender = (int)$iSender;
  134. $iRecipient = (int)$iRecipient;
  135. $iLotId = (int)$iLotId;
  136. $iJotId = (int)$iJotId;
  137. $sPlain = trim(html_entity_decode(strip_tags((string)$sMsg), ENT_QUOTES, 'UTF-8'));
  138. if ($sPlain === '' || strcasecmp($sPlain, self::NO_REPLY) === 0)
  139. return false;
  140. if (!$iLotId && $iJotId) {
  141. $aJot = $oMessengerModule->_oDb->getJotById($iJotId);
  142. $iLotId = (int)($aJot['lot_id'] ?? 0);
  143. }
  144. // No talk and no recipient: sendMessage would open a stray talk with participant 0.
  145. if (!$iLotId && !$iRecipient)
  146. return false;
  147. $aAutoReplyData = [
  148. 'message' => $sMsg,
  149. ];
  150. if ($iLotId)
  151. $aAutoReplyData['lot'] = $iLotId;
  152. else
  153. $aAutoReplyData['participants'] = [$iSender, $iRecipient];
  154. // Never pile agent jots on agent jots.
  155. if ($this->isAgentFlood($oMessengerModule, $iLotId, $iJotId))
  156. return false;
  157. $iSaveProfileId = $oMessengerModule->setProfileId($iSender);
  158. $a = $oMessengerModule->sendMessage($aAutoReplyData, $iLotId ? 0 : $iRecipient, $iSender);
  159. $oMessengerModule->setProfileId($iSaveProfileId);
  160. return $a;
  161. }
  162. protected function processMessage($iSender, $iRecipient, $iLotId, $iJotId, $aJotInfo)
  163. {
  164. $iSender = (int)$iSender;
  165. $iRecipient = (int)$iRecipient;
  166. $iLotId = (int)$iLotId;
  167. $iJotId = (int)$iJotId;
  168. if (!$iSender || !$iRecipient || $iSender == $iRecipient)
  169. return false;
  170. $oAi = $this->getAi();
  171. if (!$oAi)
  172. return false;
  173. // The alert may carry no jot info (or an empty one): read the jot.
  174. $oMessenger = BxDolModule::getInstance('bx_messenger');
  175. if ((empty($aJotInfo) || empty($aJotInfo['message'])) && $iJotId && $oMessenger)
  176. $aJotInfo = $oMessenger->_oDb->getJotById($iJotId) ?: $aJotInfo;
  177. if (!$iLotId && !empty($aJotInfo['lot_id']))
  178. $iLotId = (int)$aJotInfo['lot_id'];
  179. // Never answer an agent's own jot (the alert sender can be the human it replied to).
  180. $iJotAuthor = (int)($aJotInfo['user_id'] ?? 0);
  181. if ($oAi->isAgentProfile($iSender) || ($iJotAuthor && $oAi->isAgentProfile($iJotAuthor)))
  182. return false;
  183. $aAgents = $this->getAgentsByProfileId($iRecipient);
  184. if (!$aAgents)
  185. return false;
  186. $sText = trim((string)($aJotInfo['message'] ?? ''));
  187. if ($sText === '')
  188. return false;
  189. // got_jot fires once per participant and can nest: handle each jot+recipient once.
  190. $sQueueKey = $iLotId . ':' . $iJotId . ':' . $iRecipient;
  191. if (!isset($GLOBALS['glAgentsCallQueueKeys']))
  192. $GLOBALS['glAgentsCallQueueKeys'] = [];
  193. if (isset($GLOBALS['glAgentsCallQueueKeys'][$sQueueKey]))
  194. return false;
  195. $GLOBALS['glAgentsCallQueueKeys'][$sQueueKey] = true;
  196. if (!isset($GLOBALS['glAgentsCallQueue']) || !is_array($GLOBALS['glAgentsCallQueue']))
  197. $GLOBALS['glAgentsCallQueue'] = [];
  198. foreach ($aAgents as $a) {
  199. if (!$oAi->canInteract($a, $iSender))
  200. continue;
  201. if (!$a['message_profile_id'] || $iSender == $a['message_profile_id']) {
  202. $aParams = [
  203. 'trigger' => $this->getType(),
  204. 'sender_profile_id' => $iSender,
  205. 'recipient_profile_id' => $iRecipient,
  206. 'message_lot_id' => $iLotId,
  207. 'message_id' => $iJotId,
  208. 'message_text' => $sText,
  209. 'message_info' => $aJotInfo,
  210. ];
  211. if ($a['async']) {
  212. $this->enqueue($a, $aParams);
  213. } else {
  214. $GLOBALS['glAgentsCallQueue'][] = [
  215. 'type' => $this->getType(),
  216. 'agent' => $a,
  217. 'params' => $aParams,
  218. ];
  219. }
  220. }
  221. }
  222. if (!empty($GLOBALS['glAgentsCallQueue'])) {
  223. ignore_user_abort(true);
  224. set_time_limit(0);
  225. // TODO: need a way to force messenger to show new message from the server side
  226. // register_shutdown_function(function () {
  227. // bx_ai_process_agents_call_queue();
  228. // });
  229. $this->processCallQueue(false, false); // TODO: for a while do synchronically
  230. }
  231. return true;
  232. }
  233. /**
  234. * The jot being answered, or the last jot in the talk, was written by an agent.
  235. */
  236. protected function isAgentFlood($oMessengerModule, $iLotId, $iSourceJotId)
  237. {
  238. $oAi = $this->getAi();
  239. if (!$oAi)
  240. return false;
  241. $iLotId = (int)$iLotId;
  242. $iSourceJotId = (int)$iSourceJotId;
  243. if ($iSourceJotId) {
  244. $aSrc = $oMessengerModule->_oDb->getJotById($iSourceJotId);
  245. $iSrcAuthor = (int)($aSrc['user_id'] ?? 0);
  246. if ($iSrcAuthor && $oAi->isAgentProfile($iSrcAuthor))
  247. return true;
  248. }
  249. if (!$iLotId)
  250. return false;
  251. $CNF = &$oMessengerModule->_oConfig->CNF;
  252. $aLast = $oMessengerModule->_oDb->getRow("SELECT `{$CNF['FIELD_MESSAGE_AUTHOR']}` AS `user_id` FROM `{$CNF['TABLE_MESSAGES']}` WHERE `{$CNF['FIELD_MESSAGE_FK']}` = :lot AND `{$CNF['FIELD_MESSAGE_TRASH']}` = 0 ORDER BY `{$CNF['FIELD_MESSAGE_ID']}` DESC LIMIT 1", ['lot' => $iLotId]);
  253. $iLastAuthor = (int)($aLast['user_id'] ?? 0);
  254. return $iLastAuthor > 0 && $oAi->isAgentProfile($iLastAuthor);
  255. }
  256. }
  257. /** @} */