| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- <?php defined('BX_DOL') or die('hack attempt');
- /**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup UnaCore UNA Core
- * @{
- */
- class BxDolAiTriggerMessage extends BxDolAiTrigger
- {
- protected $_sType = 'message';
- /**
- * The agent's out-of-domain answer: not posted into the talk.
- */
- const NO_REPLY = 'NO_REPLY';
- protected function usesSenderChatHistory()
- {
- return true;
- }
- protected function appliesChatLimits()
- {
- return true;
- }
- /**
- * Messenger history is per talk (lot), not mixed with the sender's Studio chat.
- */
- protected function getCallChatHistoryParams($aAgent, $mixedParams)
- {
- $aParams = parent::getCallChatHistoryParams($aAgent, $mixedParams);
- $iLot = is_array($mixedParams) ? (int)($mixedParams['message_lot_id'] ?? 0) : 0;
- if ($iLot > 0)
- $aParams['chat_history_subindex'] = 'lot' . $iLot;
- return $aParams;
- }
- /**
- * limit_message is for chat widgets; in a talk the agent just goes quiet.
- */
- protected function getChatLimitReply($aAgent)
- {
- return null;
- }
- public function response($oAlert)
- {
- BxDolAiTrigger::getInstance($this->getType())->handle($oAlert);
- }
- public function handle($mixed = null)
- {
- $oAlert = $mixed;
- if (!$oAlert || 'bx_messenger' != $oAlert->sUnit || 'got_jot' != $oAlert->sAction)
- return false;
- $aJotInfo = isset($oAlert->aExtras['subobject_info']) && is_array($oAlert->aExtras['subobject_info'])
- ? $oAlert->aExtras['subobject_info']
- : [];
- return $this->processMessage(
- $oAlert->iSender,
- (int)($oAlert->aExtras['recipient_id'] ?? 0),
- (int)$oAlert->iObject,
- (int)($oAlert->aExtras['subobject_id'] ?? 0),
- $aJotInfo
- );
- }
- public function getAgentsByProfileId($iProfileId)
- {
- $oDb = new BxDolAiQuery();
- return $oDb->getAgentsByProfileId($iProfileId);
- }
- /**
- * Drains `$GLOBALS['glAgentsCallQueue']` once; jobs queued while a reply is
- * being posted (got_jot fires again) are picked up by the same loop, never re-run.
- */
- public function processCallQueue($bFinishRequest = true, $bExit = true)
- {
- if ($bFinishRequest) {
- if (function_exists('fastcgi_finish_request')) {
- fastcgi_finish_request();
- } else {
- @ob_end_flush();
- @flush();
- }
- }
- if (empty($GLOBALS['glAgentsCallQueueLock'])) {
- $GLOBALS['glAgentsCallQueueLock'] = true;
- try {
- while (!empty($GLOBALS['glAgentsCallQueue'])) {
- $aBatch = $GLOBALS['glAgentsCallQueue'];
- $GLOBALS['glAgentsCallQueue'] = [];
- foreach ($aBatch as $r) {
- $sType = $r['type'] ?? $this->getType();
- $mixedReply = BxDolAiTrigger::getInstance($sType)->call($r['agent'], $r['params']);
- if (!is_string($mixedReply) || $mixedReply === '')
- continue;
- $this->replyToMessage($r['agent'], $r['params'], $mixedReply);
- }
- }
- } finally {
- $GLOBALS['glAgentsCallQueueLock'] = false;
- }
- }
- if ($bExit)
- exit(0);
- }
- /**
- * Post the agent's reply (markdown) into the talk the message came from.
- *
- * @param array $aParams the call params (`sender_profile_id`, `message_lot_id`, `message_id`)
- */
- public function replyToMessage($aAgent, $aParams, $sReply)
- {
- if (is_string($aParams))
- $aParams = json_decode($aParams, true) ?: [];
- if (!is_array($aParams))
- $aParams = [];
- $oParsedown = new Parsedown();
- $oParsedown->setSafeMode(true);
- $sHtml = $oParsedown->text((string)$sReply);
- return $this->sendMessengerMessage(
- (int)($aAgent['profile_id'] ?? 0),
- (int)($aParams['sender_profile_id'] ?? 0),
- str_replace("\n", '', $sHtml),
- (int)($aParams['message_lot_id'] ?? 0),
- (int)($aParams['message_id'] ?? 0)
- );
- }
- /**
- * @param int $iLotId talk to post into; 0 = a private talk with $iRecipient (resolved from $iJotId when given)
- * @param int $iJotId the message being answered — used to find the talk and to stop agent-on-agent loops
- */
- public function sendMessengerMessage($iSender, $iRecipient, $sMsg, $iLotId = 0, $iJotId = 0)
- {
- $oMessengerModule = BxDolModule::getInstance('bx_messenger');
- if (!$oMessengerModule)
- return false;
- $iSender = (int)$iSender;
- $iRecipient = (int)$iRecipient;
- $iLotId = (int)$iLotId;
- $iJotId = (int)$iJotId;
- $sPlain = trim(html_entity_decode(strip_tags((string)$sMsg), ENT_QUOTES, 'UTF-8'));
- if ($sPlain === '' || strcasecmp($sPlain, self::NO_REPLY) === 0)
- return false;
- if (!$iLotId && $iJotId) {
- $aJot = $oMessengerModule->_oDb->getJotById($iJotId);
- $iLotId = (int)($aJot['lot_id'] ?? 0);
- }
- // No talk and no recipient: sendMessage would open a stray talk with participant 0.
- if (!$iLotId && !$iRecipient)
- return false;
- $aAutoReplyData = [
- 'message' => $sMsg,
- ];
- if ($iLotId)
- $aAutoReplyData['lot'] = $iLotId;
- else
- $aAutoReplyData['participants'] = [$iSender, $iRecipient];
- // Never pile agent jots on agent jots.
- if ($this->isAgentFlood($oMessengerModule, $iLotId, $iJotId))
- return false;
- $iSaveProfileId = $oMessengerModule->setProfileId($iSender);
- $a = $oMessengerModule->sendMessage($aAutoReplyData, $iLotId ? 0 : $iRecipient, $iSender);
- $oMessengerModule->setProfileId($iSaveProfileId);
- return $a;
- }
- protected function processMessage($iSender, $iRecipient, $iLotId, $iJotId, $aJotInfo)
- {
- $iSender = (int)$iSender;
- $iRecipient = (int)$iRecipient;
- $iLotId = (int)$iLotId;
- $iJotId = (int)$iJotId;
- if (!$iSender || !$iRecipient || $iSender == $iRecipient)
- return false;
- $oAi = $this->getAi();
- if (!$oAi)
- return false;
- // The alert may carry no jot info (or an empty one): read the jot.
- $oMessenger = BxDolModule::getInstance('bx_messenger');
- if ((empty($aJotInfo) || empty($aJotInfo['message'])) && $iJotId && $oMessenger)
- $aJotInfo = $oMessenger->_oDb->getJotById($iJotId) ?: $aJotInfo;
- if (!$iLotId && !empty($aJotInfo['lot_id']))
- $iLotId = (int)$aJotInfo['lot_id'];
- // Never answer an agent's own jot (the alert sender can be the human it replied to).
- $iJotAuthor = (int)($aJotInfo['user_id'] ?? 0);
- if ($oAi->isAgentProfile($iSender) || ($iJotAuthor && $oAi->isAgentProfile($iJotAuthor)))
- return false;
- $aAgents = $this->getAgentsByProfileId($iRecipient);
- if (!$aAgents)
- return false;
- $sText = trim((string)($aJotInfo['message'] ?? ''));
- if ($sText === '')
- return false;
- // got_jot fires once per participant and can nest: handle each jot+recipient once.
- $sQueueKey = $iLotId . ':' . $iJotId . ':' . $iRecipient;
- if (!isset($GLOBALS['glAgentsCallQueueKeys']))
- $GLOBALS['glAgentsCallQueueKeys'] = [];
- if (isset($GLOBALS['glAgentsCallQueueKeys'][$sQueueKey]))
- return false;
- $GLOBALS['glAgentsCallQueueKeys'][$sQueueKey] = true;
- if (!isset($GLOBALS['glAgentsCallQueue']) || !is_array($GLOBALS['glAgentsCallQueue']))
- $GLOBALS['glAgentsCallQueue'] = [];
- foreach ($aAgents as $a) {
- if (!$oAi->canInteract($a, $iSender))
- continue;
- if (!$a['message_profile_id'] || $iSender == $a['message_profile_id']) {
- $aParams = [
- 'trigger' => $this->getType(),
- 'sender_profile_id' => $iSender,
- 'recipient_profile_id' => $iRecipient,
- 'message_lot_id' => $iLotId,
- 'message_id' => $iJotId,
- 'message_text' => $sText,
- 'message_info' => $aJotInfo,
- ];
- if ($a['async']) {
- $this->enqueue($a, $aParams);
- } else {
- $GLOBALS['glAgentsCallQueue'][] = [
- 'type' => $this->getType(),
- 'agent' => $a,
- 'params' => $aParams,
- ];
- }
- }
- }
- if (!empty($GLOBALS['glAgentsCallQueue'])) {
- ignore_user_abort(true);
- set_time_limit(0);
- // TODO: need a way to force messenger to show new message from the server side
- // register_shutdown_function(function () {
- // bx_ai_process_agents_call_queue();
- // });
- $this->processCallQueue(false, false); // TODO: for a while do synchronically
- }
- return true;
- }
- /**
- * The jot being answered, or the last jot in the talk, was written by an agent.
- */
- protected function isAgentFlood($oMessengerModule, $iLotId, $iSourceJotId)
- {
- $oAi = $this->getAi();
- if (!$oAi)
- return false;
- $iLotId = (int)$iLotId;
- $iSourceJotId = (int)$iSourceJotId;
- if ($iSourceJotId) {
- $aSrc = $oMessengerModule->_oDb->getJotById($iSourceJotId);
- $iSrcAuthor = (int)($aSrc['user_id'] ?? 0);
- if ($iSrcAuthor && $oAi->isAgentProfile($iSrcAuthor))
- return true;
- }
- if (!$iLotId)
- return false;
- $CNF = &$oMessengerModule->_oConfig->CNF;
- $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]);
- $iLastAuthor = (int)($aLast['user_id'] ?? 0);
- return $iLastAuthor > 0 && $oAi->isAgentProfile($iLastAuthor);
- }
- }
- /** @} */
|