Преглед изворни кода

#5406 AI Agents revamp: chat history

Aleksandr Trofimov пре 1 месец
родитељ
комит
3a40b3f5d6
3 измењених фајлова са 131 додато и 2 уклоњено
  1. 80 0
      inc/classes/BxDolAI.php
  2. 16 1
      src/chat/index.tsx
  3. 35 1
      template/scripts/BxBaseServices.php

+ 80 - 0
inc/classes/BxDolAI.php

@@ -452,6 +452,86 @@ class BxDolAI extends BxDolFactory implements iBxDolSingleton
         return '';
     }
 
+    /**
+     * Transcript for TanStack `useChat` hydrate / `initialMessages`.
+     * Loads the same NeuronAI chat history the agent uses when streaming.
+     */
+    public function getChatHistoryUiMessages($iAgentId, $aParams = [])
+    {
+        $aParams['chat_history_subindex'] = $aParams['sender_profile_id'] ?? 0;
+
+        $aAgent = BxDolAiQuery::getAgentObject((int)$iAgentId);
+        if (!$aAgent || empty($aAgent['chat_history_context']))
+            return [];
+
+        $sThreadId = $aAgent['trigger'] . ':' . $aAgent['id'] . ':' . $aParams['chat_history_subindex'];
+        $sJson = $this->_oDb->getOne("SELECT `messages` FROM `sys_agents_chat_history` WHERE `thread_id` = :t", [
+            't' => $sThreadId,
+        ]);
+        $aStored = json_decode((string)$sJson, true);
+        if (!is_array($aStored) || $aStored === [])
+            return [];
+
+        try {
+            $o = self::getAgentInstance((int)$iAgentId, $aParams);
+        } catch (Exception $oException) {
+            bx_log('sys_agents', "Hydrate exception for agent {$iAgentId}: " . $oException->getMessage(), BX_LOG_ERR);
+            return [];
+        }
+
+        return $this->neuronMessagesToUiMessages($o->getChatHistory()->getMessages());
+    }
+
+    /**
+     * @param NeuronAI\Chat\Messages\Message[] $aMessages
+     * @return array<int, array<string, mixed>>
+     */
+    protected function neuronMessagesToUiMessages($aMessages)
+    {
+        $aResult = [];
+        $i = 0;
+
+        foreach ($aMessages as $oMessage) {
+            if (
+                $oMessage instanceof NeuronAI\Chat\Messages\ToolCallMessage
+                || $oMessage instanceof NeuronAI\Chat\Messages\ToolResultMessage
+            ) {
+                continue;
+            }
+
+            $sRole = $oMessage->getRole();
+            if ($sRole === 'model')
+                $sRole = 'assistant';
+            if ($sRole !== 'user' && $sRole !== 'assistant')
+                continue;
+
+            $aParts = [];
+            foreach ($oMessage->getContentBlocks() as $oBlock) {
+                if ($oBlock instanceof NeuronAI\Chat\Messages\ContentBlocks\ReasoningContent) {
+                    if ($oBlock->content !== '')
+                        $aParts[] = ['type' => 'thinking', 'content' => $oBlock->content];
+                    continue;
+                }
+
+                if ($oBlock instanceof NeuronAI\Chat\Messages\ContentBlocks\TextContent && $oBlock->content !== '')
+                    $aParts[] = ['type' => 'text', 'content' => $oBlock->content];
+            }
+
+            if (!$aParts)
+                continue;
+
+            $sId = $oMessage->getMetadata('__id');
+            $aResult[] = [
+                'id' => is_string($sId) && $sId !== '' ? $sId : ('msg_' . $i),
+                'role' => $sRole,
+                'parts' => $aParts,
+            ];
+            $i++;
+        }
+
+        return $aResult;
+    }
+
     public function streamAgentChat($iAgentId, $sPrompt, $aParams = [], $sThreadId = null)
     {
         $aParams['chat_history_subindex'] = $aParams['sender_profile_id'] ?? 0;

+ 16 - 1
src/chat/index.tsx

@@ -11,6 +11,8 @@ export interface ChatOptions {
     placeholder?: string;
     credentials?: RequestCredentials;
     formatting?: boolean;
+    initialMessages?: UIMessage[];
+    threadId?: string;
 }
 
 declare const sUrlRoot: string | undefined;
@@ -91,23 +93,36 @@ function getChatEndpoint(agentId: number | string, endpoint?: string): string
     return `${root}sys-ai-chat/${agentId}`;
 }
 
+function getChatThreadId(agentId: number | string, threadId?: string): string
+{
+    return threadId || `agent:${agentId}`;
+}
+
 function ChatView({
     agentId,
     endpoint,
     placeholder = 'Type a message…',
     credentials = 'same-origin',
     formatting = false,
+    initialMessages,
+    threadId,
 }: ChatOptions)
 {
     const [input, setInput] = useState('');
     const messagesRef = useRef<HTMLDivElement>(null);
+    const chatThreadId = getChatThreadId(agentId, threadId);
 
     const connection = useMemo(
         () => fetchServerSentEvents(getChatEndpoint(agentId, endpoint), { credentials }),
         [agentId, endpoint, credentials],
     );
 
-    const { messages, sendMessage, isLoading, error } = useChat({ connection });
+    const { messages, sendMessage, isLoading, error } = useChat({
+        connection,
+        persistence: true,
+        threadId: chatThreadId,
+        initialMessages: initialMessages ?? [],
+    });
 
     useEffect(() => {
         const list = messagesRef.current;

+ 35 - 1
template/scripts/BxBaseServices.php

@@ -1797,7 +1797,11 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
         // if (!bx_get_logged_profile_id())
         //    return echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized')]);
 
-        if ('POST' !== ($_SERVER['REQUEST_METHOD'] ?? ''))
+        $sMethod = $_SERVER['REQUEST_METHOD'] ?? '';
+        if ('GET' === $sMethod)
+            return $this->_aiChatHydrate($iAgentId);
+
+        if ('POST' !== $sMethod)
             return echoJson(['code' => 405, 'msg' => _t('_error occured')]);
 
         $iAgentId = (int)$iAgentId;
@@ -1826,6 +1830,36 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
         ], $aData['threadId'] ?? null);
     }
 
+    /**
+     * GET hydrate for TanStack `useChat({ persistence: true })`.
+     * Same route as the SSE POST; join/resume (`?offset=`) is not supported.
+     */
+    protected function _aiChatHydrate($iAgentId)
+    {
+        if (false !== bx_get('offset'))
+            return echoJson(['code' => 405, 'msg' => _t('_error occured')]);
+
+        $iAgentId = (int)$iAgentId;
+        if (!$iAgentId)
+            return echoJson(['code' => 400, 'msg' => _t('_sys_agents_agent_not_found')]);
+
+        $aAgent = BxDolAiQuery::getAgentObject($iAgentId);
+        if (!$aAgent || !$aAgent['active'])
+            return echoJson(['code' => 404, 'msg' => _t('_sys_agents_agent_not_found')]);
+
+        $oAi = BxDolAI::getInstance();
+        if (!$oAi)
+            return echoJson(['code' => 503, 'msg' => _t('_sys_agents_exception')]);
+
+        return echoJson([
+            'messages' => $oAi->getChatHistoryUiMessages($iAgentId, [
+                'sender_profile_id' => bx_get_logged_profile_id(),
+            ]),
+            'activeRun' => null,
+            'interrupts' => null,
+        ]);
+    }
+
     protected function _getOptionsAiAssistants()
     {
         $aResult = ['' => _t('_Select_one')];