Просмотр исходного кода

Merge branch 'master' into studio-design-fixes-2

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Andrey Yasko 1 месяц назад
Родитель
Сommit
5cd2d28f2f
92 измененных файлов с 3596 добавлено и 7724 удалено
  1. 137 4
      .github/workflows/ci.yml
  2. 0 53
      agents.php
  3. 1 11
      build.xml
  4. 0 1037
      inc/classes/BxDolAI.php
  5. 0 361
      inc/classes/BxDolAIAssistant.php
  6. 0 151
      inc/classes/BxDolAIModel.php
  7. 43 7
      inc/classes/BxDolAIModelFactory.php
  8. 0 150
      inc/classes/BxDolAIModelGpt35.php
  9. 0 521
      inc/classes/BxDolAIModelGpt40.php
  10. 0 142
      inc/classes/BxDolAIProvider.php
  11. 0 133
      inc/classes/BxDolAIProviderShopifyAdmin.php
  12. 0 1117
      inc/classes/BxDolAIQuery.php
  13. 70 0
      inc/classes/BxDolAIToolChatArtifactSave.php
  14. 40 0
      inc/classes/BxDolAIToolChatGoalReached.php
  15. 1 1
      inc/classes/BxDolAIToolFactory.php
  16. 1 1
      inc/classes/BxDolAIVectorStoreFactory.php
  17. 1533 0
      inc/classes/BxDolAi.php
  18. 11 5
      inc/classes/BxDolAiAgent.php
  19. 1 1
      inc/classes/BxDolAiAlertResponse.php
  20. 39 1
      inc/classes/BxDolAiChatHistory.php
  21. 524 0
      inc/classes/BxDolAiQuery.php
  22. 1 13
      inc/classes/BxDolAlerts.php
  23. 1 1
      inc/classes/BxDolCronPruning.php
  24. 1 16
      inc/classes/BxDolSearch.php
  25. 2 2
      inc/classes/BxDolSearchExtended.php
  26. 1 1
      inc/utils.inc.php
  27. 0 0
      install/sql/system.sql
  28. 30 161
      modules/boonex/english/data/langs/system/en.xml
  29. 1 1
      modules/boonex/notifications/classes/BxNtfsTemplate.php
  30. 1 1
      modules/boonex/notifications/install/sql/enable.sql
  31. 33 164
      modules/boonex/russian/data/langs/system/ru.xml
  32. 4 3
      package.json
  33. 88 6
      scripts/Jenkinsfile
  34. 39 0
      scripts/railway/Dockerfile
  35. 128 0
      scripts/railway/entrypoint.sh
  36. 7 0
      scripts/railway/railway.toml
  37. 0 0
      scripts/tailwind.max.css
  38. 1 1
      studio/agents.php
  39. 4 14
      studio/classes/BxDolStudioAgents.php
  40. 1 7
      studio/classes/BxDolStudioAgentsAgents.php
  41. 0 40
      studio/classes/BxDolStudioAgentsAssistants.php
  42. 0 61
      studio/classes/BxDolStudioAgentsAsstChats.php
  43. 0 323
      studio/classes/BxDolStudioAgentsAsstChatsCmts.php
  44. 0 47
      studio/classes/BxDolStudioAgentsAsstFiles.php
  45. 0 28
      studio/classes/BxDolStudioAgentsAutomators.php
  46. 0 201
      studio/classes/BxDolStudioAgentsAutomatorsCmts.php
  47. 0 83
      studio/classes/BxDolStudioAgentsCmts.php
  48. 0 28
      studio/classes/BxDolStudioAgentsHelpers.php
  49. 7 5
      studio/classes/BxDolStudioAgentsInstruments.php
  50. 0 30
      studio/classes/BxDolStudioAgentsProviders.php
  51. 1 1
      studio/classes/BxDolStudioAgentsQuery.php
  52. 2 2
      studio/classes/BxDolStudioGridAgents.php
  53. 1 9
      studio/classes/BxDolStudioPage.php
  54. 292 9
      studio/js/agents.js
  55. 18 18
      studio/template/agents_agents_form.html
  56. 0 10
      studio/template/agents_assistant_codes.html
  57. 0 19
      studio/template/agents_assistant_info.html
  58. 0 11
      studio/template/agents_automator_form_assistants.html
  59. 0 11
      studio/template/agents_automator_form_helpers.html
  60. 0 11
      studio/template/agents_automator_form_providers.html
  61. 0 26
      studio/template/agents_automator_info.html
  62. 0 10
      studio/template/agents_comment_content.html
  63. 18 1
      studio/template/agents_popup_chat.html
  64. 125 2
      studio/template/css/agents.css
  65. 4 252
      studio/template/scripts/BxBaseStudioAgents.php
  66. 264 33
      studio/template/scripts/BxBaseStudioAgentsAgents.php
  67. 0 318
      studio/template/scripts/BxBaseStudioAgentsAssistants.php
  68. 0 291
      studio/template/scripts/BxBaseStudioAgentsAsstChats.php
  69. 0 184
      studio/template/scripts/BxBaseStudioAgentsAsstFiles.php
  70. 0 790
      studio/template/scripts/BxBaseStudioAgentsAutomators.php
  71. 0 243
      studio/template/scripts/BxBaseStudioAgentsHelpers.php
  72. 1 1
      studio/template/scripts/BxBaseStudioAgentsLogs.php
  73. 2 2
      studio/template/scripts/BxBaseStudioAgentsModels.php
  74. 0 291
      studio/template/scripts/BxBaseStudioAgentsProviders.php
  75. 2 2
      studio/template/scripts/BxBaseStudioAgentsVectorStore.php
  76. 1 1
      studio/template/scripts/BxBaseStudioAgentsVectorStoreData.php
  77. 1 1
      studio/template/scripts/BxBaseStudioGridAgents.php
  78. 3 10
      studio/template/scripts/BxBaseStudioPage.php
  79. 0 19
      studio/template/scripts_templ/BxTemplStudioAgentsAssistants.php
  80. 0 19
      studio/template/scripts_templ/BxTemplStudioAgentsAsstChats.php
  81. 0 19
      studio/template/scripts_templ/BxTemplStudioAgentsAsstFiles.php
  82. 0 19
      studio/template/scripts_templ/BxTemplStudioAgentsAutomators.php
  83. 0 19
      studio/template/scripts_templ/BxTemplStudioAgentsHelpers.php
  84. 0 19
      studio/template/scripts_templ/BxTemplStudioAgentsProviders.php
  85. 0 5
      template/agents_comment_actions_assistant.html
  86. 0 5
      template/agents_comment_actions_automator.html
  87. 0 10
      template/agents_comment_content.html
  88. 0 14
      template/agents_comment_loading.html
  89. 1 1
      template/scripts/BxBaseSearch.php
  90. 11 6
      template/scripts/BxBaseSearchExtended.php
  91. 83 63
      template/scripts/BxBaseServices.php
  92. 15 14
      yarn.lock

+ 137 - 4
.github/workflows/ci.yml

@@ -5,12 +5,15 @@ on:
     branches: [ "master" ]
   pull_request:
     branches: [ "master" ]
+    types: [opened, synchronize, reopened, closed]
 
 permissions:
   contents: read
+  pull-requests: write
 
 jobs:
   test:
+    if: github.event_name == 'push' || github.event.action != 'closed'
     runs-on: ubuntu-latest
 
     services:
@@ -38,7 +41,7 @@ jobs:
         php-version: '8.3'
         extensions: intl, curl, gd, mbstring, zip, exif, pdo_mysql
         ini-values: short_open_tag=On, memory_limit=256M
-        tools: composer
+        tools: composer, phing, phpunit:12
 
     - name: Setup Node
       uses: actions/setup-node@v4
@@ -47,7 +50,7 @@ jobs:
         cache: yarn
 
     - name: Install PHP dependencies
-      run: composer install --prefer-dist --no-progress
+      run: composer install --prefer-dist --no-progress --no-dev
 
     - name: Install JS dependencies
       run: yarn install --frozen-lockfile
@@ -55,7 +58,7 @@ jobs:
     - name: Prepare writable paths
       run: |
         mkdir -p cache cache_public logs tmp storage
-        chmod -R a+rwX inc cache cache_public logs tmp storage
+        chmod a+rwX inc cache cache_public logs tmp storage
         test -f plugins/ffmpeg/ffmpeg.exe && chmod 0755 plugins/ffmpeg/ffmpeg.exe
 
     - name: Install UNA
@@ -71,4 +74,134 @@ jobs:
         --server_php_self=/install/index.php
 
     - name: Run PHPUnit
-      run: composer test
+      run: phpunit -c tests/phpunit.xml
+
+    - name: Build package
+      run: phing package -Dbuild=${{ github.run_number }}
+
+    - name: Upload package artifact
+      uses: actions/upload-artifact@v4
+      with:
+        name: una-package
+        path: packages/*.zip
+        if-no-files-found: error
+
+  deploy:
+    name: Deploy PR to Railway
+    needs: test
+    if: github.event_name == 'pull_request' && github.event.action != 'closed'
+    runs-on: ubuntu-latest
+    env:
+      RAILWAY_API_TOKEN: ${{ secrets.RAILWAY_API_TOKEN }}
+      RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
+      RAILWAY_BASE_ENVIRONMENT: ${{ secrets.RAILWAY_ENVIRONMENT }}
+      RAILWAY_PR_ENVIRONMENT: pr-${{ github.event.pull_request.number }}
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: actions/download-artifact@v4
+      with:
+        name: una-package
+        path: packages
+
+    - name: Prepare artefact
+      run: |
+        mkdir -p deploy
+        unzip -q packages/*.zip -d /tmp/una-pkg
+        pkg_dir=$(find /tmp/una-pkg -mindepth 1 -maxdepth 1 -type d | head -n 1)
+        cp -a "$pkg_dir"/. deploy/
+        cp scripts/railway/Dockerfile scripts/railway/entrypoint.sh scripts/railway/railway.toml deploy/
+        chmod +x deploy/entrypoint.sh
+
+    - name: Install Railway CLI
+      run: |
+        curl -fsSL https://railway.com/install.sh | sh
+        echo "$HOME/.railway/bin" >> "$GITHUB_PATH"
+
+    - name: Provision PR environment
+      run: |
+        if [ -z "$RAILWAY_API_TOKEN" ] || [ -z "$RAILWAY_PROJECT_ID" ]; then
+          echo "Set secrets RAILWAY_API_TOKEN (account token) and RAILWAY_PROJECT_ID" >&2
+          exit 1
+        fi
+        base="${RAILWAY_BASE_ENVIRONMENT:-production}"
+        railway link --project "$RAILWAY_PROJECT_ID" --environment "$base" --json
+        railway environment new "$RAILWAY_PR_ENVIRONMENT" || true
+        railway link --project "$RAILWAY_PROJECT_ID" --environment "$RAILWAY_PR_ENVIRONMENT" --json
+        railway add --image mariadb:11 --service mariadb --json \
+          --variables MARIADB_ROOT_PASSWORD=una \
+          --variables MARIADB_DATABASE=una \
+          --variables MARIADB_USER=una \
+          --variables MARIADB_PASSWORD=una \
+          || true
+        railway add --service una --json || true
+        railway variable set --service una --environment "$RAILWAY_PR_ENVIRONMENT" --skip-deploys \
+          MYSQLHOST=mariadb.railway.internal \
+          MYSQLPORT=3306 \
+          MYSQLDATABASE=una \
+          MYSQLUSER=una \
+          MYSQLPASSWORD=una \
+          || true
+        railway domain --service una --environment "$RAILWAY_PR_ENVIRONMENT" || true
+
+    - name: Deploy artefact
+      run: railway up ./deploy --path-as-root --no-gitignore --ci --service una --environment "$RAILWAY_PR_ENVIRONMENT" --project "$RAILWAY_PROJECT_ID"
+
+    - name: Comment preview URL on PR
+      env:
+        GH_TOKEN: ${{ github.token }}
+        PR_NUMBER: ${{ github.event.pull_request.number }}
+      run: |
+        json=$(railway domain list --service una --environment "$RAILWAY_PR_ENVIRONMENT" --json)
+        host=$(printf '%s\n' "$json" | jq -r '.domains[] | select(.type == "service") | .domain' | head -n1)
+        if [ -z "$host" ]; then
+          host=$(printf '%s\n' "$json" | jq -r '.domains[0].domain // empty')
+        fi
+        if [ -z "$host" ]; then
+          echo "No Railway domain found for service una" >&2
+          echo "$json" >&2
+          exit 1
+        fi
+        case "$host" in
+          https://*|http://*) url="$host" ;;
+          *) url="https://$host" ;;
+        esac
+        echo "Preview URL: $url"
+
+        marker="<!-- railway-preview -->"
+        body=$(printf '%s\n**Preview**: %s\n\nRailway environment: `%s`' "$marker" "$url" "$RAILWAY_PR_ENVIRONMENT")
+
+        comment_id=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \
+          --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | contains(\"${marker}\"))) | .id" \
+          | head -n1)
+
+        if [ -n "$comment_id" ]; then
+          gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" -f body="$body"
+        else
+          gh pr comment "$PR_NUMBER" --body "$body"
+        fi
+
+  teardown:
+    name: Remove PR Railway environment
+    if: github.event_name == 'pull_request' && github.event.action == 'closed'
+    runs-on: ubuntu-latest
+    env:
+      RAILWAY_API_TOKEN: ${{ secrets.RAILWAY_API_TOKEN }}
+      RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
+      RAILWAY_BASE_ENVIRONMENT: ${{ secrets.RAILWAY_ENVIRONMENT }}
+      RAILWAY_PR_ENVIRONMENT: pr-${{ github.event.pull_request.number }}
+    steps:
+    - name: Install Railway CLI
+      run: |
+        curl -fsSL https://railway.com/install.sh | sh
+        echo "$HOME/.railway/bin" >> "$GITHUB_PATH"
+
+    - name: Delete PR environment
+      run: |
+        if [ -z "$RAILWAY_API_TOKEN" ] || [ -z "$RAILWAY_PROJECT_ID" ]; then
+          echo "Set secrets RAILWAY_API_TOKEN (account token) and RAILWAY_PROJECT_ID" >&2
+          exit 1
+        fi
+        base="${RAILWAY_BASE_ENVIRONMENT:-production}"
+        railway link --project "$RAILWAY_PROJECT_ID" --environment "$base" --json
+        railway environment delete "$RAILWAY_PR_ENVIRONMENT" -y

+ 0 - 53
agents.php

@@ -1,53 +0,0 @@
-<?php
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaCore UNA Core
- * @{
- */
-
-require_once('./inc/header.inc.php');
-require_once(BX_DIRECTORY_PATH_INC . "utils.inc.php");
-
-bx_import('BxDolLanguages');
-
-$sTool = bx_process_input(bx_get('t'));
-
-/**
- * Work with Providers
- */
-if(($iProviderId = bx_get('p')) !== false) {
-    $iProviderId = bx_process_input($iProviderId, BX_DATA_INT);
-    if(!$iProviderId)
-        exit;
-
-    $oProvider = BxDolAIProvider::getObjectInstance($iProviderId);
-    if(!$oProvider)
-        exit;
-
-    if(($sAction = bx_get('a')) !== false) {
-        $sAction = 'processAction' . bx_gen_method_name(bx_process_input($sAction));
-        if(method_exists($oProvider, $sAction))
-            $oProvider->$sAction();
-    }
-    else {
-        $mixedResponce = $oProvider->call('products/7433953116300.json', ['fields' => 'id,title,handle,body_html,tags,variants'], 'get');
-        print_r($mixedResponce);
-    }
-}
-
-/**
- * Work with Assistants
- */
-if($sTool == 'asst' && ($iId = bx_get('id')) !== false) {
-    $oAssistant = BxDolAIAssistant::getObjectInstance((int)$iId);
-
-    if(($sAction = bx_get('a')) !== false) {
-        $sAction = 'processAction' . bx_gen_method_name(bx_process_input($sAction));
-        if(method_exists($oAssistant, $sAction))
-            $oAssistant->$sAction();
-    }
-}
-
-/** @} */

+ 1 - 11
build.xml

@@ -82,21 +82,11 @@
         <exclude name="samples/**" />
         <exclude name="scripts/**" />
         <exclude name="tests/**" />
-        <exclude name="plugins/phpunit/**" />
-        <exclude name="plugins/phpmd/**" />
-        <exclude name="plugins/pdepend/**" />
-        <exclude name="plugins/sebastian/**" />
-        <exclude name="plugins/phar-io/**" />
-        <exclude name="plugins/myclabs/**" />
-        <exclude name="plugins/theseer/**" />
-        <exclude name="plugins/staabm/**" />
-        <exclude name="plugins/bin/phpunit" />
-        <exclude name="plugins/bin/phpmd" />
-        <exclude name="plugins/bin/pdepend" />
         <exclude name="packages/**" />
         <exclude name="upgrade/**" />
         <exclude name="node_modules/**" />
         <exclude name="bower_components/**" />
+        <exclude name="modules/boonex/*/updates/**" />
     </fileset>
 
     <fileset dir="${project.basedir}" defaultexcludes="true" id="doc_files">

+ 0 - 1037
inc/classes/BxDolAI.php

@@ -1,1037 +0,0 @@
-<?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
- * @{
- */
-
-define('BX_DOL_AI_ASSISTANT', 'assistant');
-define('BX_DOL_AI_AUTOMATOR_EVENT', 'event');
-define('BX_DOL_AI_AUTOMATOR_SCHEDULER', 'scheduler');
-define('BX_DOL_AI_AUTOMATOR_WEBHOOK', 'webhook');
-
-define('BX_DOL_AI_AUTOMATOR_STATUS_AUTO', 'auto');
-define('BX_DOL_AI_AUTOMATOR_STATUS_MANUAL', 'manual');
-define('BX_DOL_AI_AUTOMATOR_STATUS_READY', 'ready');
-
-class BxDolAI extends BxDolFactory implements iBxDolSingleton
-{
-    protected $_oDb;
-    protected $_iProfileId;
-    
-    protected $_aExcludeAlertUnits;
-
-    protected $_sCmtsAutomators;
-    protected $_sCmtsAssistantsChats;
-
-    protected $_bWriteLog;
-
-    protected function __construct()
-    {
-        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
-            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
-
-        parent::__construct();
-
-        $this->_oDb = new BxDolAIQuery();
-
-        $this->_iProfileId = (int)getParam('sys_profile_bot'); 
-
-        $this->_aExcludeAlertUnits = [
-            'system', 'module_template_method_call'
-        ];
-
-        $this->_sCmtsAutomators = 'sys_agents_automators';
-        $this->_sCmtsAssistantsChats = 'sys_agents_assistants_chats';
-
-        $this->_bWriteLog = true;
-    }
-
-    /**
-     * Prevent cloning the instance
-     */
-    public function __clone()
-    {
-        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
-            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
-    }
-
-    /**
-     * Get singleton instance of the class
-     */
-    public static function getInstance()
-    {
-        if (!isset($GLOBALS['bxDolClasses'][__CLASS__])) {
-            $GLOBALS['bxDolClasses'][__CLASS__] = BxDolDb::getInstance()->isTableExists('sys_agents_agents') ? new BxDolAI() : null;
-        }
-
-        return $GLOBALS['bxDolClasses'][__CLASS__];
-    }
-
-    public static function getAgentInstance(int $iId, array $aParams = []): NeuronAI\Agent\Agent
-    {
-        if (isset($GLOBALS['bxDolClasses'][__CLASS__ . '_Agent_' . $iId]))
-            return $GLOBALS['bxDolClasses'][__CLASS__ . '_Agent_' . $iId];
-
-        $a = BxDolAIQuery::getAgentObject($iId);
-        if (!$a) {
-            $s = "Agent with id {$iId} not found";
-            bx_log('sys_agents', $s, BX_LOG_ERR);
-            throw new Exception($s);
-        }
-        if (!$a['active']) {
-            $s = "Agent with id {$iId} isn't active";
-            bx_log('sys_agents', $s, BX_LOG_ERR);
-            throw new Exception($s);
-        }
-        if (!$a['prompt_system'] || !$a['model_id']) {
-            $s = "Agent with id {$iId} can't be used because it haven't prompt or AI model";
-            bx_log('sys_agents', $s, BX_LOG_ERR);
-            throw new Exception($s);
-        }
-
-        $o = BxDolAiAgent::make($a, $aParams);
-
-        if ((int)$a['tools_max_run'] > 0)
-            $o->toolMaxRuns($a['tools_max_run']);
-
-        if ($a['vector_store_id']) {
-            $aVectorStore = BxDolAIQuery::getVectorStoreObject($a['vector_store_id']);
-            if ($aVectorStore && $aVectorStore['embedding_provider_id']) {
-                $oEmbedder = self::getAiEmbeddingsProviderInstance($aVectorStore['embedding_provider_id']);
-                $o->setEmbeddingsProvider($oEmbedder);
-            }
-        }
-
-        $GLOBALS['bxDolClasses'][__CLASS__ . '_Agent_' . $iId] = $o;
-
-        return $o;
-    }
-
-    public static function getAiProviderInstance(int $iId):NeuronAI\Providers\AIProviderInterface
-    {
-        return BxDolAIModelFactory::getModelInstance($iId);
-    }   
-
-    public static function getAiEmbeddingsProviderInstance(int $iId):NeuronAI\RAG\Embeddings\EmbeddingsProviderInterface
-    {
-        return BxDolAIModelFactory::getModelInstance($iId);
-    }
-
-    public static function callHelper($mixedHelper, $sMessage)
-    {
-        $oAI = BxDolAI::getInstance();
-        if (is_numeric($mixedHelper))
-            $aHelper = $oAI->getHelperById($mixedHelper);
-        else
-             $aHelper = $oAI->getHelperByName($mixedHelper);
-        $oAIModel = $oAI->getModelObject($aHelper['model_id']);
-        return $oAIModel->getResponseText($aHelper['prompt'], $sMessage);
-    }
-
-    public static function pruning()
-    {
-        BxDolAIAssistant::pruning();
-    }
-
-    public static function getDefaultApiKey()
-    {
-        return getParam('sys_agents_api_key');
-    }
-
-    public static function getDefaultModel()
-    {
-        return (int)getParam('sys_agents_model');
-    }
-
-    public static function getAssistantForStudio()
-    {
-        return ($iId = (int)getParam('sys_agents_studio_assistant')) != 0 ? $iId : 0;
-    }
-
-    public static function getAssistantForLiveSearch()
-    {
-        return ($iId = (int)getParam('sys_agents_live_search_assistant')) != 0 ? $iId : 0;
-    }
-
-    public static function getAssistantForAskBlock()
-    {
-        return ($iId = (int)getParam('sys_agents_ask_block_assistant')) != 0 ? $iId : 0;
-    }
-
-    public function getProfileId()
-    {
-        return $this->_iProfileId;
-    }
-
-    public function getModels($aParams = [])
-    {
-        $aParamsDb = ['sample' => 'all_pairs'];
-        if(isset($aParams['active']))
-            $aParamsDb['active'] = $aParams['active'] === true ? 1 : 0;
-        if(isset($aParams['capabilities']))
-            $aParamsDb['capabilities'] = $aParams['capabilities'];
-
-        return $aModel = $this->_oDb->getModelsBy($aParamsDb);
-    }
-
-    public function getModel($iId)
-    {
-        $aModel = $this->_oDb->getModelsBy(['sample' => 'id', 'id' => $iId]);
-        if(!empty($aModel['params']))
-            $aModel['params'] = json_decode($aModel['params'], true);
-
-        return $aModel;
-    }
-
-    public function getModelObject($iId)
-    {
-        if(!$iId)
-            $iId = $this->getDefaultModel();
-        if(!$iId)
-            return false;
-
-        return BxDolAIModel::getObjectInstance($iId);
-    }
-    
-    public function getProviderObject($iId)
-    {
-        if(!$iId)
-            return false;
-
-        return BxDolAIProvider::getObjectInstance($iId);
-    }   
-
-    public function getAssistants($aParams = [])
-    {
-        $aParamsDb = ['sample' => 'all_pairs'];
-        if(isset($aParams['active']))
-            $aParamsDb['active'] = $aParams['active'] === true ? 1 : 0;
-        if(isset($aParams['hidden']))
-            $aParamsDb['hidden'] = $aParams['hidden'] === true ? 1 : 0;
-
-        return $aModel = $this->_oDb->getAssistantsBy($aParamsDb);
-    }
-
-    public function getAssistantById($iId)
-    {
-        return $this->_oDb->getAssistantsBy(['sample' => 'id', 'id' => $iId]);
-    }
-
-    public function getAssistantByName($sName)
-    {
-        return $this->_oDb->getAssistantsBy(['sample' => 'name', 'name' => $sName]);
-    }
-
-    public function getAssistantChatById($iId)
-    {
-        return $this->_oDb->getChatsBy(['sample' => 'id', 'id' => $iId]);
-    }
-
-    public function getAssistantChatsTransient($iLifetime = 0)
-    {
-        return $this->_oDb->getChatsBy(['sample' => 'type', 'type' => BX_DOL_AI_ASST_TYPE_TRANSIENT, 'lifetime' => $iLifetime]);
-    }
-
-    public function updateAssistantChatById($iId, $aSet)
-    {
-        return $this->_oDb->updateChats($aSet, ['id' => $iId]);
-    }
-
-    public function getAssistantChatCmts()
-    {
-        return $this->_sCmtsAssistantsChats;
-    }
-
-    public function getAssistantChatCmtsObject($iId, $oTemplate = false)
-    {
-        $oCmts = BxDolCmts::getObjectInstance($this->_sCmtsAssistantsChats, (int)$iId, true, $oTemplate);
-        if(!$oCmts || !$oCmts->isEnabled())
-            return false;
-
-        return $oCmts;
-    }
-
-    public function getHelperById($iId)
-    {
-        return $this->_oDb->getHelpersBy(['sample' => 'id', 'id' => $iId]);
-    }
-    
-    public function getHelperByName($sName)
-    {
-        return $this->_oDb->getHelpersBy(['sample' => 'name', 'name' => $sName]);
-    }
-
-    public function getAutomator($iId, $bFullInfo = false)
-    {
-        $aAutomator = $this->_oDb->getAutomatorsBy(['sample' => 'id' . ($bFullInfo ? '_full' : ''), 'id' => $iId]);
-        if(!empty($aAutomator['params']))
-            $aAutomator['params'] = json_decode($aAutomator['params'], true);
-        if($bFullInfo && !empty($aAutomator['model_params']))
-            $aAutomator['model_params'] = json_decode($aAutomator['model_params'], true);
-
-        return $aAutomator;
-    }
-
-    public function getAutomatorInstruction($sType, $mixedParams = false)
-    {
-        $mixedResult = '';
-
-        switch($sType) {
-            case 'profile':
-                $mixedResult = "\n ProfileId for system actions = " . $mixedParams;
-                break;
-
-            case 'providers':
-                $aProviders = $this->_oDb->getProvidersBy(['sample' => 'ids', 'ids' => $mixedParams]);
-                if(!empty($aProviders) && is_array($aProviders)) {
-                    $mixedResult = "\n Proividers list = [";
-                    foreach($aProviders as $aProvider)
-                        $mixedResult .= "\n {'ProviderName' => '" . $aProvider['name'] . "',  'ProviderType' => '" . $aProvider['type_name'] . "'}";
-                    $mixedResult .= "\n ]";
-                }
-                break;
-
-            case 'helpers':
-                $aHelpers = $this->_oDb->getHelpersBy(['sample' => 'ids', 'ids' => $mixedParams]);
-                if(!empty($aHelpers) && is_array($aHelpers)) {
-                    $mixedResult = "\n Helpers list = [";
-                    foreach($aHelpers as $aHelper)
-                        $mixedResult .= "\n {'" . $aHelper['name'] . "', 'HelperDescription' => '" . $aHelper['description'] . "'}";
-                    $mixedResult .= "\n ]";
-                }
-                break;
-
-            case 'assistants':
-                $aAssistants = $this->_oDb->getAssistantsBy(['sample' => 'ids', 'ids' => $mixedParams]);
-                if(!empty($aAssistants) && is_array($aAssistants)) {
-                    $mixedResult = "\n Assistants list = [";
-                    foreach($aAssistants as $aAssistant)
-                        $mixedResult .= "\n {'" . $aAssistant['name'] . "', 'AssistantDescription' => '" . $aAssistant['description'] . "'}";
-                    $mixedResult .= "\n ]";
-                }
-                break;
-        }
-
-        return $mixedResult;
-    }
-
-    public function getAutomatorCmts()
-    {
-        return $this->_sCmtsAutomators;
-    }
-
-    public function getAutomatorCmtsObject($iId, $oTemplate = false)
-    {
-        $oCmts = BxDolCmts::getObjectInstance($this->_sCmtsAutomators, (int)$iId, true, $oTemplate);
-        if(!$oCmts || !$oCmts->isEnabled())
-            return false;
-
-        return $oCmts;
-    }
-
-    public function hasAutomators($sType, $bActive = null)
-    {
-        $aParams = [
-            'sample' => 'type', 
-            'type' => $sType
-        ];
-        if($bActive !== null)
-            $aParams['active'] = $bActive;
-
-        return ($aAutomators = $this->_oDb->getAutomatorsBy($aParams)) && is_array($aAutomators);
-    }
-
-    public function getAutomatorsEvent($sUnit, $sAction)
-    {
-        if(in_array($sUnit, $this->_aExcludeAlertUnits))
-            return [];
-
-        return $this->_oDb->getAutomatorsBy([
-            'sample' => 'events', 
-            'alert_unit' => $sUnit,
-            'alert_action' => $sAction,
-            'active' => true
-        ]);
-    }
-
-    public function getAutomatorsScheduler()
-    {
-        $aAutomators = $this->_oDb->getAutomatorsBy(['sample' => 'schedulers', 'active' => true]);
-        foreach($aAutomators as &$aAutomator)
-            if(!empty($aAutomator['params']))
-                $aAutomator['params'] = json_decode($aAutomator['params'], true);
-
-        return $aAutomators;
-    }
-
-    public function getAutomatorsWebhook($iProviderId)
-    {
-        $aAutomators = $this->_oDb->getAutomatorsBy(['sample' => 'webhooks', 'provider_id' => $iProviderId, 'active' => true]);
-        foreach($aAutomators as &$aAutomator)
-            if(!empty($aAutomator['params']))
-                $aAutomator['params'] = json_decode($aAutomator['params'], true);
-
-        return $aAutomators;
-    }
-
-    public function callAgent($sType, $aAgent, $mixedParams = [])
-    {
-        if ($mixedParams)
-            $sParams = is_string($mixedParams) ? $mixedParams : json_encode($mixedParams);
-        else
-            $sParams = 'START';
-
-        // update sample data
-        $a = ['webhook' => 'webhook_sample'];
-        if (isset($a[$sType]) && empty($aAgent[$a[$sType]])) {
-            $this->_oDb->updateAgentField($aAgent['id'], $a[$sType], $sParams);
-        }
-
-        // set additional params
-        $aParams = [];
-        if ('message' == $sType || 'form-input' == $sType || 'alert' == $sType) {
-            $aParams = ['chat_history_subindex' => (int)($mixedParams['sender_profile_id'] ?? 0)];
-        }
-
-        // call agent
-        $mixed = '';
-        try {                        
-            $o = self::getAgentInstance($aAgent['id'], $aParams);
-            if (!$o)
-                return false;
-
-            $oMessage = $o->chat(new NeuronAI\Chat\Messages\UserMessage($sParams))->getMessage();
-
-            $mixed = $oMessage->getContent();
-
-        } catch (Exception $exception) {            
-            bx_log('sys_agents', "Exception in '{$aAgent['name']}' agent: " . $exception->getMessage() . " INPUT:" . $sParams, BX_LOG_ERR);
-            $mixed = _t('_sys_agents_exception');
-        }
-
-        return $mixed;
-    }
-
-    /**
-     * Whether the current (or given) member may interact with the agent
-     * via chat, messenger, or form-input. Empty acl_levels means Nobody.
-     * Site admins / Studio operators always may, so they can debug.
-     */
-    public function canInteract($aAgent, $iProfileId = false, $bAllowOperators = true)
-    {
-        if (!is_array($aAgent) || empty($aAgent['id']))
-            return false;
-
-        if ($bAllowOperators && isAdmin())
-            return true;
-
-        $iLevels = (int)($aAgent['acl_levels'] ?? 0);
-        if (!$iLevels)
-            return false;
-
-        return (bool)BxDolAcl::getInstance()->isMemberLevelInSet($iLevels, $iProfileId);
-    }
-
-    /**
-     * Direct HTTP chat (`sys-ai-chat`). Operators may talk to any trigger;
-     * everyone else only `manual` and `message` agents they can interact with.
-     */
-    public function canChatDirectly($aAgent, $iProfileId = false)
-    {
-        if (!$this->canInteract($aAgent, $iProfileId))
-            return false;
-
-        if (isAdmin())
-            return true;
-
-        return in_array($aAgent['trigger'] ?? '', ['manual', 'message'], true);
-    }
-
-    public function extractChatPromptFromRequest($aData)
-    {
-        if (!is_array($aData))
-            return '';
-
-        if (!empty($aData['prompt']) && is_string($aData['prompt']))
-            return trim($aData['prompt']);
-
-        if (empty($aData['messages']) || !is_array($aData['messages']))
-            return '';
-
-        for ($i = count($aData['messages']) - 1; $i >= 0; $i--) {
-            $aMessage = $aData['messages'][$i];
-            if (!is_array($aMessage))
-                continue;
-
-            if (($aMessage['role'] ?? '') !== 'user')
-                continue;
-
-            if (isset($aMessage['content']) && is_string($aMessage['content']))
-                return trim($aMessage['content']);
-
-            if (!empty($aMessage['parts']) && is_array($aMessage['parts'])) {
-                $aText = [];
-                foreach ($aMessage['parts'] as $aPart) {
-                    if (is_array($aPart) && ($aPart['type'] ?? '') === 'text' && isset($aPart['content']))
-                        $aText[] = $aPart['content'];
-                }
-                $s = trim(implode("\n", $aText));
-                if ($s !== '')
-                    return $s;
-            }
-        }
-
-        return '';
-    }
-
-    /**
-     * Guests: UNA session id. Members: profile id.
-     * Guest chats remember agent ids in session so login can adopt those threads.
-     */
-    public function resolveChatHistoryParams($iAgentId = 0)
-    {
-        $iAgentId = (int)$iAgentId;
-        $iProfileId = (int)bx_get_logged_profile_id();
-        $oSession = BxDolSession::getInstance();
-        $sSessionKey = 'ai_chat_agent_ids';
-
-        if ($iProfileId) {
-            $aIds = $oSession->getValue($sSessionKey);
-            if (is_array($aIds) && $aIds) {
-                $sSessionId = (string)$oSession->getId();
-                if ($iAgentId)
-                    $aIds[] = $iAgentId;
-                foreach (array_unique(array_map('intval', $aIds)) as $iStoredAgentId) {
-                    if (!$iStoredAgentId || $sSessionId === '')
-                        continue;
-                    $aAgent = BxDolAiQuery::getAgentObject($iStoredAgentId);
-                    if ($aAgent)
-                        $this->_oDb->adoptGuestChatHistory($aAgent, $sSessionId, $iProfileId);
-                }
-                $oSession->unsetValue($sSessionKey);
-            }
-
-            return [
-                'sender_profile_id' => $iProfileId,
-                'chat_history_subindex' => (string)$iProfileId,
-            ];
-        }
-
-        if ($iAgentId) {
-            $aIds = $oSession->getValue($sSessionKey);
-            $aIds = array_map('intval', is_array($aIds) ? $aIds : []);
-            if (!in_array($iAgentId, $aIds, true)) {
-                $aIds[] = $iAgentId;
-                $oSession->setValue($sSessionKey, $aIds);
-            }
-        }
-
-        return [
-            'sender_profile_id' => 0,
-            'chat_history_subindex' => (string)$oSession->getId(),
-        ];
-    }
-
-    /**
-     * `{trigger}:{agentId}:{contextPid}:{userSubindex}`.
-     * Context is omitted when 0 so existing site-wide threads still load.
-     * User suffix (session id or profile id) stays last so adopt can replace it.
-     */
-    public static function chatHistoryThreadId($aAgent, $aParams = [])
-    {
-        $s = ($aAgent['trigger'] ?? '') . ':' . ($aAgent['id'] ?? '');
-        $iContextPid = (int)($aParams['chat_history_context_pid'] ?? 0);
-        if ($iContextPid > 0)
-            $s .= ':' . $iContextPid;
-        if (isset($aParams['chat_history_subindex']) && $aParams['chat_history_subindex'] !== '')
-            $s .= ':' . (string)$aParams['chat_history_subindex'];
-        return $s;
-    }
-
-    /**
-     * Context from chat HTTP (`?context=`). Missing/0 = site-wide. Invalid/denied = false (403).
-     * Posting in a group is not the same as viewing it; chat requires post by default.
-     * @return int|false
-     */
-    public function resolveChatHistoryContextPid($bRequirePost = true)
-    {
-        $mixed = bx_get('context');
-        if ($mixed === false)
-            return 0;
-
-        $iPid = (int)$mixed;
-        if ($iPid <= 0)
-            return 0;
-
-        $oProfile = BxDolProfile::getInstance($iPid);
-        if (!$oProfile || !bx_srv('system', 'is_module_context', [$oProfile->getModule()]))
-            return false;
-
-        if (!$this->isAllowedContextChat($oProfile, $bRequirePost))
-            return false;
-
-        return $iPid;
-    }
-
-    /**
-     * Context of the current page (AI agent block).
-     * `bx_get_page_info()` reads the URL; App page JSON merges `i`/`id` into $_GET instead.
-     * @param bool $bRequirePost also require posting into the context (membership / post ACL)
-     */
-    public function resolveChatHistoryContextPidFromPage($bRequirePost = false)
-    {
-        $aInfo = bx_get_page_info();
-        if ($aInfo && !empty($aInfo['context_profile_id']))
-            return $this->filterContextPid((int)$aInfo['context_profile_id'], $bRequirePost);
-
-        $sUri = bx_get('i');
-        $iContentId = (int)bx_process_input(bx_get('id'), BX_DATA_INT);
-        if ($sUri && $iContentId > 0) {
-            $oPage = BxDolPage::getObjectInstanceByURI($sUri);
-            $sModule = $oPage ? (string)$oPage->getModule() : '';
-            if ($sModule !== '' && bx_srv('system', 'is_module_context', [$sModule])) {
-                $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $sModule);
-                if ($oProfile && ($iPid = $this->filterContextPid((int)$oProfile->id(), $bRequirePost)))
-                    return $iPid;
-            }
-        }
-
-        $iPid = (int)bx_process_input(bx_get('profile_id'), BX_DATA_INT);
-        if ($iPid > 0 && ($iPid = $this->filterContextPid($iPid, $bRequirePost)))
-            return $iPid;
-
-        return 0;
-    }
-
-    public function filterContextPid($iPid, $bRequirePost = false)
-    {
-        $iPid = (int)$iPid;
-        if ($iPid <= 0)
-            return 0;
-
-        $oProfile = BxDolProfile::getInstance($iPid);
-        if (!$oProfile || !bx_srv('system', 'is_module_context', [$oProfile->getModule()]))
-            return 0;
-
-        if (!$this->isAllowedContextChat($oProfile, $bRequirePost))
-            return 0;
-
-        return $iPid;
-    }
-
-    protected function isAllowedContextChat($oProfile, $bRequirePost = false)
-    {
-        if ($oProfile->checkAllowedProfileView() !== CHECK_ACTION_RESULT_ALLOWED)
-            return false;
-
-        if ($bRequirePost && $oProfile->checkAllowedPostInProfile() !== CHECK_ACTION_RESULT_ALLOWED)
-            return false;
-
-        return true;
-    }
-
-    /**
-     * Transcript for TanStack `useChat` hydrate / `initialMessages`.
-     * Loads the same NeuronAI chat history the agent uses when streaming.
-     */
-    public function getChatHistoryUiMessages($iAgentId, $aParams = [])
-    {
-        if (!isset($aParams['chat_history_subindex']))
-            $aParams = array_merge($this->resolveChatHistoryParams($iAgentId), $aParams);
-
-        $aAgent = BxDolAiQuery::getAgentObject((int)$iAgentId);
-        if (!$aAgent || empty($aAgent['chat_history_context']))
-            return [];
-
-        $sThreadId = self::chatHistoryThreadId($aAgent, $aParams);
-        $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)
-    {
-        if (!isset($aParams['chat_history_subindex']))
-            $aParams = array_merge($this->resolveChatHistoryParams($iAgentId), $aParams);
-
-        $oAdapter = new NeuronAI\Chat\Messages\Stream\Adapters\AGUIAdapter($sThreadId);
-
-        foreach ($oAdapter->getHeaders() as $sName => $sValue)
-            header($sName . ': ' . $sValue);
-
-        if (function_exists('apache_setenv'))
-            @apache_setenv('no-gzip', '1');
-        @ini_set('zlib.output_compression', '0');
-        @ini_set('implicit_flush', '1');
-        while (ob_get_level())
-            ob_end_flush();
-
-        try {
-            $o = self::getAgentInstance((int)$iAgentId, $aParams);
-            $oHandler = $o->stream(new NeuronAI\Chat\Messages\UserMessage($sPrompt));
-
-            foreach ($oHandler->events($oAdapter) as $sEvent) {
-                echo $sEvent;
-                if (ob_get_level())
-                    ob_flush();
-                flush();
-            }
-        } catch (Exception $oException) {
-            bx_log('sys_agents', "Stream exception for agent {$iAgentId}: " . $oException->getMessage() . " INPUT:" . $sPrompt, BX_LOG_ERR);
-            echo 'data: ' . json_encode(['type' => 'RUN_ERROR', 'message' => _t('_sys_agents_exception')]) . "\n\n";
-            flush();
-        }
-
-        exit;
-    }
-
-    public function sendMessengerMessage ($iSender, $iRecipient, $sMsg) 
-    {        
-        $oMessengerModule = BxDolModule::getInstance('bx_messenger');
-
-        $aAutoReplyData = [
-            'message' => $sMsg,
-            'participants' => [$iSender, $iRecipient],
-        ];
-
-        $iSaveProfileId = $oMessengerModule->setProfileId($iSender);
-        $a = $oMessengerModule->sendMessage($aAutoReplyData, $iRecipient, $iSender);
-        $oMessengerModule->setProfileId($iSaveProfileId);
-
-        return $a;
-    }
-
-    public function getAgentsByAlertUnitAndAction($sUnit, $sAction)
-    {
-        $aAgents = [];
-        $a = $this->_oDb->getAgentsWithAlert();
-        foreach ($a as $r) {
-            $aAlert = explode(':', $r['alert']); // TODO: remake to concantenate $sUnit and $sAction and then compare
-            if (count($aAlert) == 2 && $aAlert[0] == $sUnit && $aAlert[1] == $sAction)
-                $aAgents[] = $r;
-        }
-
-        return $aAgents;
-    }
-
-    public function getAgentsBy($aParams)
-    {
-        return $this->_oDb->getAgentsBy($aParams);
-    }
-
-    public function getAgentsByProfileId($iProfileId)
-    {
-        return $this->_oDb->getAgentsByProfileId($iProfileId);
-    }
-    
-    public function getAgentsByFormObject($sFormObject)
-    {
-        return $this->_oDb->getAgentsByFormObject($sFormObject);
-    }
-
-    public function getAgentsByTriggerType($sTrigger)
-    {
-        return $this->_oDb->getAgentsByTriggerType($sTrigger);
-    }
-
-    public function getAgentById($iId)
-    {
-        return $this->_oDb->getAgentById($iId);
-    }
-
-    public function getAgentByTriggerWebhookKey($sKey)
-    {
-        return $this->_oDb->getAgentByTriggerWebhookKey($sKey);
-    }
-
-    public function callAutomator($sType, $aParams = [])
-    {
-        $sMethod = '_callAutomator' . bx_gen_method_name($sType);
-        if(!method_exists($this, $sMethod))
-            return false;
-
-        return $this->$sMethod($aParams);
-    }
-
-    protected function _callAutomatorEvent($aParams = [])
-    {
-        if(!isset($aParams['automator'], $aParams['alert']) || !is_a($aParams['alert'], 'BxDolAlerts'))
-            return false;
-        
-        $oAlert = &$aParams['alert'];
-
-        $this->evalCode($aParams['automator'], ['alert' => $oAlert]);
-    }
-
-    protected function _callAutomatorScheduler($aParams = [])
-    {
-        if(!isset($aParams['automator']))
-            return false;
-        
-        $this->evalCode($aParams['automator']);
-    }
-
-    protected function _callAutomatorWebhook($aParams = [])
-    {
-        if(!isset($aParams['automator']))
-            return false;
-
-        $this->evalCode($aParams['automator']);
-    }
-
-    public function evalCode($aAutomator, $aParams = [])
-    {
-        try {
-            $this->_evalCode($aAutomator, $aParams);
-        }
-        catch (Exception $oException) {
-            $this->log($oException->getFile() . ':' . $oException->getLine() . ' ' . $oException->getMessage());
-        }
-        catch (Error $oError) {
-            $this->log($oError->getFile() . ':' . $oError->getLine() . ' ' . $oError->getMessage());
-        }
-    }
-
-    public function emulCode($aAutomator, $aParams = [])
-    {
-        ob_start();
-
-        try {
-            $this->_evalCode($aAutomator, $aParams);
-        }
-        catch (Exception $oException) {
-            return $oException->getMessage();
-        }
-        catch (Error $oError) {
-            return $oError->getMessage();
-        }
-        finally {
-            $sOutput = ob_get_clean();
-
-            if(!empty($sOutput))
-                return $sOutput;
-        }
-    }
-
-    public function log($mixedContents, $sSection = '')
-    {
-        if(!$this->_bWriteLog)
-            return;
-
-        if(is_array($mixedContents))
-            $mixedContents = var_export($mixedContents, true);	
-        else if(is_object($mixedContents))
-            $mixedContents = json_encode($mixedContents);
-
-        if(empty($sSection))
-            $sSection = "Core";
-
-        bx_log('sys_agents', ":\n[" . $sSection . "] " . $mixedContents, BX_LOG_ERR);
-    }
-
-    protected function _evalCode($aAutomator, $aParams = [])
-    {
-        $sCode = '';
-        switch($aAutomator['type']) {
-            case BX_DOL_AI_AUTOMATOR_EVENT:
-                $sCode = $aAutomator['code']. '; onAlert($aParams["alert"]->iObject , $aParams["alert"]->iSender , $aParams["alert"]->aExtras);';
-                break;
-
-            case BX_DOL_AI_AUTOMATOR_SCHEDULER:
-                $sCode = $aAutomator['code'] . '; onCron();';
-                break;
-
-            case BX_DOL_AI_AUTOMATOR_WEBHOOK:
-                $sCode = $aAutomator['code'] . '; onHook();';
-                break;
-        }
-
-        eval($sCode);
-    }
-}
-
-class BxDolAIMessage
-{
-    /**
-     * @var string - message Type with following values: hb, ai 
-     */
-    protected $_sType;
-    
-    /**
-     * @var mixed - an array of message parts (text, image_url) or a string.
-     */
-    protected $_mixedContent;
-    
-    /**
-     * @var array - an array of of files attached to the message.
-     */
-    protected $_aAttachments;
-
-    public function __construct($sType)
-    {
-        $this->_sType = $sType;
-    }
-
-    public function isAi()
-    {
-        return $this->_sType == 'ai';
-    }
-
-    public function getContent()
-    {
-        return $this->_mixedContent;
-    }
-
-    public function getAttachments()
-    {
-        return $this->_aAttachments;
-    }
-}
-
-class BxDolAIMessageString extends BxDolAIMessage
-{
-    public function __construct($sType, $sContent)
-    {
-        parent::__construct($sType);
-
-        $this->_mixedContent = is_string($sContent) ? $sContent : '';
-    }
-}
-
-class BxDolAIMessageArray extends BxDolAIMessage
-{
-    public function __construct($sType, $aContent = '')
-    {
-        parent::__construct($sType);
-
-        $this->_mixedContent = is_array($aContent) ? $aContent : [];
-    }
-
-    public function addText($sText)
-    {
-        $this->_mixedContent[] = [
-            'type' => 'text',
-            'text' => $sText
-        ];
-    }
-
-    public function addImageUrl($sUrl, $sDetail = 'high')
-    {
-        $this->_mixedContent[] = [
-            'type' => 'image_url',
-            'image_url' => [
-                'url' => $sUrl,
-                'detail' => $sDetail
-            ]
-        ];
-    }
-
-    public function addAttachments($mixedAttachments, $mixedTools = false)
-    {
-        if(!is_array($mixedAttachments))
-            $mixedAttachments = [$mixedAttachments];
-
-        if(!$mixedTools)
-            $mixedTools = [['type' => 'file_search']];
-
-        foreach($mixedAttachments as $sAttachment)
-            $this->_aAttachments[] = [
-                'file_id' => $sAttachment,
-                'tools' => $mixedTools
-            ];
-    }
-}
-
-class BxDolAIMessages
-{
-    /**
-     * @var array - an array of items (messages)
-     */
-    protected $_aItems;
-
-    public function __construct($aItems = [])
-    {
-        $this->_aItems = !empty($aItems) && is_array($aItems) ? $aItems : [];
-    }
-
-    public function add($sType, $mixedMessage)
-    {
-        $sClass = 'BxDolAIMessage' . (is_string($mixedMessage) ? 'String' : 'Array');
-        $this->_aItems[] = new $sClass($sType, $mixedMessage);
-    }
-
-    public function getAll()
-    {
-        return $this->_aItems;
-    }
-
-    public function getLast()
-    {
-        return end($this->_aItems);
-    }
-}

+ 0 - 361
inc/classes/BxDolAIAssistant.php

@@ -1,361 +0,0 @@
-<?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
- * @{
- */
-
-define('BX_DOL_AI_ASST_TYPE_PERMANENT', 1);
-define('BX_DOL_AI_ASST_TYPE_TRANSIENT', 10);
-
-class BxDolAIAssistant extends BxDol
-{
-    protected static $_sFileText = 'sys_asst_text.json';
-    protected static $_sFileFaq = 'sys_asst_faq.json';
-
-    protected $_oDb;   
-
-    protected $_iId;
-    protected $_aData;    
-
-    public function __construct($aAssistant)
-    {
-        parent::__construct();
-
-        if(empty($aAssistant) || !is_array($aAssistant))
-            $this->_log("Unexpected value provided for the credentials");
-
-        $this->_oDb = new BxDolAIQuery();
-
-        $this->_iId = (int)$aAssistant['id'];
-        $this->_aData = $aAssistant;
-    }
-
-    /**
-     * Get assistant object instance by ID
-     * @param $iId assistant ID
-     * @return object instance or false on error
-     */
-    public static function getObjectInstance($iId)
-    {
-        $sPrefix = 'BxDolAIAssistant!';
-
-        if(isset($GLOBALS['bxDolClasses'][$sPrefix . $iId]))
-            return $GLOBALS['bxDolClasses'][$sPrefix . $iId];
-
-        $aAssistant = BxDolAIQuery::getAssistantObject($iId);
-        if(!$aAssistant)
-            return false;
-
-        $o = new BxDolAIAssistant($aAssistant);
-        return ($GLOBALS['bxDolClasses'][$sPrefix . $iId] = $o);
-    }
-
-    public static function getName($sName)
-    {
-        return uriGenerate($sName, 'sys_agents_assistants', 'name', ['lowercase' => false]);
-    }
-
-    public static function getChatName($sName)
-    {
-        return uriGenerate($sName, 'sys_agents_assistants_chats', 'name', ['lowercase' => false]);
-    }
-
-    public static function pruning()
-    {
-        $oAi = BxDolAI::getInstance();
-
-        if(getParam('sys_agents_asst_chats_trans_del') == 'on') {
-            $aChats = $oAi->getAssistantChatsTransient(3600);
-            foreach($aChats as $aChat)
-                if(!empty($aChat['assistant_id']) && ($oAssistant = self::getObjectInstance($aChat['assistant_id'])) !== false)
-                    $oAssistant->deleteChat($aChat);
-        }
-    }
-
-    public function getModelObject()
-    {
-        return BxDolAI::getInstance()->getModelObject($this->_aData['model_id']);
-    }
-
-    public function getChatCmtsObject($iChatId)
-    {
-        return BxDolAI::getInstance()->getAssistantChatCmtsObject($iChatId);
-    }
-
-    public function getAskButton($sText)
-    {
-        $sTitle = _t('_sys_agents_assistants_txt_ask');
-        if(!empty($sText))
-            $sTitle .= ': ' . strmaxtextlen($sText, 16);
-
-        return BxDolTemplate::getInstance()->parseButton($sTitle, [
-            'class' => 'bx-btn sys-agents-ask',
-            'onclick' => "javascrip:bx_agents_action(this, 'asst', 'ask', {id: " . $this->_iId . ", text: '" . $sText . "'})"
-        ]);
-    }
-
-    public function getAskChat($sName = '', $sDescription = '', $sText = '', $oTemplate = false)
-    {
-        if(!$oTemplate)
-            $oTemplate = BxDolTemplate::getInstance();
-
-        $bName = !empty($sName);
-        $bDescription = !empty($sDescription);
-        $bText = !empty($sText);
-
-        $iChatId = 0;
-        if($bName) {
-            $aChat = $this->_oDb->getChatsBy(['sample' => 'name', 'name' => $sName]);
-            if(!empty($aChat) && is_array($aChat))
-                $iChatId = (int)$aChat['id'];
-        }
-
-        if(empty($iChatId)) {
-            if(!$bName) {
-                if($bDescription)
-                    $sName = strmaxtextlen(_t($sDescription), 8);
-                else if($bText)
-                    $sName = strmaxtextlen($sText, 8);
-                else
-                    $sName = genRndPwd();
-
-                $sName = self::getChatName($sName);
-            }
-
-            $iChatId = $this->_oDb->insertChat([
-                'name' => $sName,
-                'type' => BX_DOL_AI_ASST_TYPE_TRANSIENT,
-                'assistant_id' => $this->_iId, 
-                'description' => $sDescription,
-                'added' => time(),
-            ]);
-        }
-
-        $sResult = '';
-        if($iChatId !== false && ($oCmts = BxDolAI::getInstance()->getAssistantChatCmtsObject($iChatId, $oTemplate)) !== false) {
-            $oCmts->setAllowDelete(false);
-
-            if(!empty($sText))
-                $oCmts->add([
-                    'cmt_author_id' => bx_get_logged_profile_id(),
-                    'cmt_parent_id' => 0,
-                    'cmt_text' => $sText
-                ]);
-
-            $sResult = $oCmts->getCommentsBlock([]);
-        }
-
-        return $sResult;
-    }
-    
-    public function getAskBlock($aParams = [])
-    {
-        return $this->getAskChat('sys_block_assistant', '_sys_agents_assistants_chat_dsc_block');
-    }
-
-    public function deleteChat($mixedChat)
-    {
-        $aChat = is_array($mixedChat) ? $mixedChat : $this->_oDb->getChatsBy(['sample' => 'id', 'id' => (int)$mixedChat]);
-        if(empty($aChat) || !is_array($aChat))
-            return false;      
-
-        if(($oCmts = $this->getChatCmtsObject($aChat['id'])) !== false)
-            $oCmts->onObjectDelete();
-
-        if(!empty($aChat['ai_file_id'])) {
-            $oAIModel = $this->getModelObject();
-            $oAIModel->callVectorStoresFilesDelete($this->_aData['ai_vs_id'], $aChat['ai_file_id']);
-            $oAIModel->callFilesDelete($aChat['ai_file_id']);
-        }
-
-        return $this->_oDb->deleteChats(['id' => $aChat['id']]);
-    }
-
-    public function processActionAddKnowledge()
-    {
-        $sAction = 'add_knowledge';
-        $oTemplate = BxDolTemplate::getInstance();
-
-        $aForm = $this->_getForm($sAction, ['id' => $this->_iId]);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $oAIModel = $this->getModelObject();
-
-            $sType = $oForm->getCleanValue('type');
-
-            $sFile = self::${'_sFile' . ucfirst($sType)};
-            $aFile = $this->_oDb->getFilesBy(['sample' => 'assistant_id', 'assistant_id' => $this->_iId, 'name' => $sFile]);
-
-            $aContent = [];
-            if(!empty($aFile) && is_array($aFile) && !empty($aFile['ai_file_id'])) {
-                $sContent = $oAIModel->callFilesRetrieveContent($aFile['ai_file_id']);
-                $aContent = json_decode($sContent, true);
-
-                if(!$oAIModel->callVectorStoresFilesDelete($this->_aData['ai_vs_id'], $aFile['ai_file_id']))
-                    return echoJson([]);
-
-                if(!$oAIModel->callFilesDelete($aFile['ai_file_id']))
-                    return echoJson([]);
-
-                $this->_oDb->deleteFiles(['id' => $aFile['id']]);
-            }
-
-            switch($sType) {
-                case 'text':
-                    $aContent[] = bx_process_input($oForm->getCleanValue('text'));
-                    break;
-
-                case 'faq':
-                    $aContent[] = [
-                        'question' => bx_process_input($oForm->getCleanValue('faq_q')),
-                        'answer' => bx_process_input($oForm->getCleanValue('faq_a'))
-                    ];
-                    break;
-            }
-
-            $aFileResponse = $oAIModel->callFiles(['content' => json_encode($aContent), 'name' => $sFile, 'mime' => 'application/json']);
-            if(!$aFileResponse)
-                return echoJson([]);
-
-            if(!$oAIModel->callVectorStoresFiles($this->_aData['ai_vs_id'], ['file_id' => $aFileResponse['id']]))
-                return echoJson([]);
-
-            $this->_oDb->insertFile([
-                'name' => $sFile, 
-                'assistant_id' => $this->_iId, 
-                'added' => time(), 
-                'ai_file_id' => $aFileResponse['id'], 
-                'ai_file_size' => $aFileResponse['bytes']
-            ]);
-
-            return echoJson(['msg' => _t('_sys_agents_assistants_msg_knowledge_added')]);
-        }
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_assistants_popup_add_knowledge'), $oTemplate->parseHtmlByName('agents_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    public function processActionAsk()
-    {
-        $sAction = 'ask';
-        $oTemplate = BxDolTemplate::getInstance();
-
-        $sText = bx_get('text');
-        if($sText !== false)
-            $sText = bx_process_input($sText);
-
-        $iChatId = $this->_oDb->insertChat([
-            'name' => self::getChatName(strmaxtextlen($sText, 8)),
-            'type' => BX_DOL_AI_ASST_TYPE_TRANSIENT,
-            'assistant_id' => $this->_iId, 
-            'description' => '_sys_agents_assistants_chat_dsc_live_search',
-            'added' => time(),
-        ]);
-
-        $sPopupContent = '';
-        if($iChatId !== false && ($oCmts = BxDolAI::getInstance()->getAssistantChatCmtsObject($iChatId, $oTemplate)) !== false) {
-            $oCmts->setAllowDelete(false);
-            $oCmts->add([
-                'cmt_author_id' => bx_get_logged_profile_id(),
-                'cmt_parent_id' => 0,
-                'cmt_text' => $sText
-            ]);
-
-            $sPopupContent = $oCmts->getCommentsBlock([], ['dynamic_mode' => true]);
-        }
-
-        $sPopupId = 'bx_agents_assistants_ask';
-        $sPopupContent = BxTemplFunctions::getInstance()->popupBox($sPopupId, _t('_sys_agents_assistants_popup_ask'), $oTemplate->parseHtmlByName('agents_popup.html', [
-            'content' => $sPopupContent
-        ]));
-
-        return echoJson(['popup' => ['html' => $sPopupContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    protected function _getForm($sAction, $aAssistant = [])
-    {
-        $aForm = [
-            'form_attrs' => [
-                'id' => 'bx_agents_assistants_' . $sAction,
-                'action' => BX_DOL_URL_ROOT . bx_append_url_params('agents.php', ['t' => 'asst', 'a' => $sAction]),
-                'method' => 'post',
-            ],
-            'params' => [
-                'db' => [
-                    'submit_name' => 'do_submit',
-                ],
-            ],
-            'inputs' => [],
-        ];
-
-        switch($sAction) {
-            case 'add_knowledge':
-                $aForm['inputs'] = [
-                    'id' => [
-                        'type' => 'hidden',
-                        'name' => 'id',
-                        'value' => $aAssistant['id'],
-                    ],
-                    'type' => [
-                        'type' => 'select',
-                        'name' => 'type',
-                        'caption' => _t('_sys_agents_assistants_field_kwg_type'),
-                        'value' => '',
-                        'values' => [
-                            ['key' => 'text', 'value' => _t('_sys_agents_assistants_field_kwg_type_text')],
-                            ['key' => 'faq', 'value' => _t('_sys_agents_assistants_field_kwg_type_faq')]
-                        ],
-                        'attrs' => ['onchange' => "javascript:bx_aa_ak_type_change(this)"],
-                    ],
-                    'text' => [
-                        'type' => 'textarea',
-                        'name' => 'text',
-                        'caption' => _t('_sys_agents_assistants_field_kwg_text'),
-                        'value' => '',
-                        'tr_attrs' => ['df' => '1', 'dt' => 'text']
-                    ],
-                    'faq_q' => [
-                        'type' => 'text',
-                        'name' => 'faq_q',
-                        'caption' => _t('_sys_agents_assistants_field_kwg_faq_q'),
-                        'value' => '',
-                        'tr_attrs' => ['df' => '1', 'dt' => 'faq', 'style' => 'display:none']
-                    ],
-                    'faq_a' => [
-                        'type' => 'textarea',
-                        'name' => 'faq_a',
-                        'caption' => _t('_sys_agents_assistants_field_kwg_faq_a'),
-                        'value' => '',
-                        'tr_attrs' => ['df' => '1', 'dt' => 'faq', 'style' => 'display:none']
-                    ],
-                    'submit' => [
-                        'type' => 'input_set',
-                        0 => [
-                            'type' => 'submit',
-                            'name' => 'do_submit',
-                            'value' => _t('_sys_submit'),
-                        ],
-                        1 => [
-                            'type' => 'reset',
-                            'name' => 'close',
-                            'value' => _t('_sys_close'),
-                            'attrs' => ['class' => 'bx-def-margin-sec-left', 'onclick' => '$(\'.bx-popup-applied:visible\').dolPopupHide();'],
-                        ],
-                    ],
-                ];
-                break;
-        }
-
-        return $aForm;
-    }
-}

+ 0 - 151
inc/classes/BxDolAIModel.php

@@ -1,151 +0,0 @@
-<?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 BxDolAIModel extends BxDol
-{
-    protected $_oDb;
-    protected $_iId;
-    protected $_sName;
-    protected $_sCaption;
-    protected $_sKey;
-    protected $_aParams;
-
-    protected $_mixedError;
-    protected $_bError;
-
-    public function __construct($aModel)
-    {
-        parent::__construct();
-
-        if(empty($aModel) || !is_array($aModel) || strcmp($aModel['name'], $this->_sName) != 0)
-            $this->_log("Unexpected value provided for the credentials");
-
-        $this->_oDb = new BxDolAIQuery();
-
-        $this->_iId = (int)$aModel['id'];
-        $this->_sName = $aModel['name'];
-        $this->_sCaption = _t($aModel['title']);
-        $this->_sKey = !empty($aModel['key']) ? $aModel['key'] : BxDolAI::getDefaultApiKey();
-        $this->_aParams = !empty($aModel['params']) ? json_decode($aModel['params'], true) : [];
-
-        $this->_mixedError = null;
-        $this->_bError = false;
-    }
-
-    /**
-     * Get model object instance by model name
-     * @param $sName model name
-     * @return object instance or false on error
-     */
-    public static function getObjectInstance($iId)
-    {
-        $sPrefix = 'BxDolAIModel!';
-
-        if(isset($GLOBALS['bxDolClasses'][$sPrefix . $iId]))
-            return $GLOBALS['bxDolClasses'][$sPrefix . $iId];
-
-        $aModel = BxDolAIQuery::getModelObject($iId);
-        if(!$aModel || !is_array($aModel))
-            return false;
-
-        $sClass = 'BxDolAIModel';
-        if(!empty($aModel['class_name'])) {
-            $sClass = $aModel['class_name'];
-            if(!empty($aModel['class_file']))
-                require_once(BX_DIRECTORY_PATH_ROOT . $aModel['class_file']);
-        }
-
-        $o = new $sClass($aModel);
-        return ($GLOBALS['bxDolClasses'][$sPrefix . $iId] = $o);
-    }
-    
-    public function getParams()
-    {
-        return $this->_aParams;
-    }
-
-    public function setParams($aParams)
-    {
-        if(empty($aParams) || !is_array($aParams))
-            return;
-
-        $this->_aParams = array_merge($this->_aParams, $aParams);
-    }
-
-    public function isError()
-    {
-        return $this->_bError;
-    }
-
-    public function getError()
-    {
-        return $this->_bError ? $this->_mixedError : false;
-    }
-
-    public function getErrorMessage()
-    {
-        if(!$this->_bError)
-            return false;
-
-        if(is_string($this->_mixedError))
-            return $this->_mixedError;
-
-        if(is_array($this->_mixedError) && !empty($this->_mixedError['message']))
-            return $this->_mixedError['message'];
-
-        return _t('_sys_agents_err_failed');
-    }
-
-    public function setError($mixedError)
-    {
-        if(empty($mixedError))
-            return false;
-
-        $this->_mixedError = $mixedError;
-        $this->_bError = true;
-
-        return true;
-    }
-
-    public function resetError()
-    {
-        $this->_mixedError = null;
-        $this->_bError = false;
-    }
-
-    public function getResponseInit($sType, $aMessage, $aParams = [])
-    {
-        // Should be overwritten to get init call response.
-    }
-
-    public function getResponse($sType, $aMessage, $aParams = [])
-    {
-        // Should be overwritten to get call response.
-    }
-
-    /**
-     * Internal methods.
-     */
-    protected function _log($mixedError, $bUseLog = true)
-    {
-        if(!$bUseLog) {
-            $sMessage = 'Error occurred';
-            if(is_string($mixedError))
-                $sMessage = $mixedError;
-            else if(is_array($mixedError) && isset($mixedError['message']))
-                $sMessage = $mixedError['message'];
-
-            throw new Exception($sMessage);
-        }
-        else
-            BxDolAI::getInstance()->log($mixedError, 'Models');
-
-        return false;
-    }    
-}

+ 43 - 7
inc/classes/BxDolAIModelFactory.php

@@ -9,18 +9,21 @@
 
 class BxDolAIModelFactory extends BxDolFactory
 {
-    public static function getModelInstance(int $iId): NeuronAI\Providers\AIProviderInterface | NeuronAI\RAG\Embeddings\EmbeddingsProviderInterface
+    public static function getModelInstance(int $iId, array $aOverrides = []): NeuronAI\Providers\AIProviderInterface | NeuronAI\RAG\Embeddings\EmbeddingsProviderInterface
     {
-        if (isset($GLOBALS['bxDolClasses'][__CLASS__ . '_Model_' . $iId]))
-            return $GLOBALS['bxDolClasses'][__CLASS__ . '_Model_' . $iId];
+        $iMaxTokensOverride = (int)($aOverrides['max_tokens'] ?? 0);
+        $sCacheKey = __CLASS__ . '_Model_' . $iId . ($iMaxTokensOverride > 0 ? '_' . $iMaxTokensOverride : '');
+        if (isset($GLOBALS['bxDolClasses'][$sCacheKey]))
+            return $GLOBALS['bxDolClasses'][$sCacheKey];
 
-        $aProvidersWithKey = ['anthropic'];
-        $a = BxDolAIQuery::getModelObject($iId);
+        $aProvidersWithKey = ['anthropic', 'openai-embeddings', 'voyageai-embeddings', 'openai-like-embeddings', 'openai-responses', 'openai-like'];
+        $a = BxDolAiQuery::getModelObject($iId);
         if (!$a) {
             bx_log('sys_agents', "Agent AI Model with id {$iId} not found", BX_LOG_ERR);
             throw new Exception("Agent AI Model with id {$iId} not found");
         }
-        if (in_array($a['type'], $aProvidersWithKey) && empty($a['key'])) {
+        $a['key'] = (string)$a['key'];
+        if (in_array($a['type'], $aProvidersWithKey, true) && $a['key'] === '') {
             bx_log('sys_agents', "Model with id {$iId} has empty key, can't be used", BX_LOG_ERR);
             throw new Exception("Model with id {$iId} has empty key, can't be used");
         }
@@ -40,6 +43,39 @@ class BxDolAIModelFactory extends BxDolFactory
             'model' => $a['model']
         ]);
 
+        if ($iMaxTokensOverride > 0) {
+            $aParameters['max_tokens'] = $iMaxTokensOverride;
+            if (!isset($aParameters['parameters']) || !is_array($aParameters['parameters']))
+                $aParameters['parameters'] = [];
+
+            switch ($a['type']) {
+                case 'openai-responses':
+                    $aParameters['parameters']['max_output_tokens'] = $iMaxTokensOverride;
+                    break;
+                case 'azure-openai':
+                    $aParameters['parameters']['max_completion_tokens'] = $iMaxTokensOverride;
+                    break;
+                case 'gemini':
+                    if (!isset($aParameters['parameters']['generationConfig']) || !is_array($aParameters['parameters']['generationConfig']))
+                        $aParameters['parameters']['generationConfig'] = [];
+                    $aParameters['parameters']['generationConfig']['maxOutputTokens'] = $iMaxTokensOverride;
+                    break;
+                case 'ollama':
+                    if (!isset($aParameters['parameters']['options']) || !is_array($aParameters['parameters']['options']))
+                        $aParameters['parameters']['options'] = [];
+                    $aParameters['parameters']['options']['num_predict'] = $iMaxTokensOverride;
+                    break;
+                case 'aws-bedrock':
+                    if (!isset($aParameters['inferenceConfig']) || !is_array($aParameters['inferenceConfig']))
+                        $aParameters['inferenceConfig'] = [];
+                    $aParameters['inferenceConfig']['maxTokens'] = $iMaxTokensOverride;
+                    break;
+                default:
+                    $aParameters['parameters']['max_tokens'] = $iMaxTokensOverride;
+                    break;
+            }
+        }
+
         switch($a['type']) {
             // regular AI providers ------------------------
             case 'anthropic':
@@ -186,7 +222,7 @@ class BxDolAIModelFactory extends BxDolFactory
                 throw new Exception("Model type {$a['type']} is not supported");
         }
 
-        $GLOBALS['bxDolClasses'][__CLASS__ . '_Model_' . $iId] = $o;
+        $GLOBALS['bxDolClasses'][$sCacheKey] = $o;
 
         return $o;
     }

+ 0 - 150
inc/classes/BxDolAIModelGpt35.php

@@ -1,150 +0,0 @@
-<?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 BxDolAIModelGpt35 extends BxDolAIModel
-{
-    public static $NAME = 'gpt-3.5-turbo';
-
-    protected $_sEndpoint;
-    protected $_sPathInst;
-
-    public function __construct($aModel)
-    {
-        $this->_sName = self::$NAME;
-
-        parent::__construct($aModel);
-
-        $this->_sEndpoint = "https://api.openai.com/v1/chat/completions";
-        $this->_sPathInst = BX_DIRECTORY_PATH_ROOT . 'ai/instructions/';
-    }
-
-    public function getResponseText($sPrompt, $sMessage)
-    {
-        $aMessages = [
-            ['role' => 'system', 'content' => $sPrompt],
-            ['role' => 'user', 'content' => $sMessage]
-        ];
-
-        $sResponse = $this->call($aMessages);
-        if($sResponse == 'false')
-            return false;
-        
-        return $sResponse;
-    }
-    
-    public function getResponseInit($sType, $oMessage, $aParams = [])
-    {
-        if(!is_a($oMessage, 'BxDolAIMessage'))
-            return false;
-
-        $aMessages = [
-            ['role' => 'system', 'content' => $this->_getInstructions($sType . '_init')],
-            ['role' => 'user', 'content' => $oMessage->getContent()]
-        ];
-
-        $sResponse = $this->call($aMessages);
-        if($sResponse == 'false')
-            return false;
-
-        $mixedResult = [];
-        switch($sType) {
-            case BX_DOL_AI_AUTOMATOR_EVENT:
-                $aResponse = json_decode($sResponse, true);
-
-                $mixedResult = [
-                    'alert_unit' => $aResponse['alert_unit'],
-                    'alert_action' => $aResponse['alert_action'],
-                    'params' => [
-                        'trigger' => $aResponse['trigger']
-                    ]
-                ];
-                break;
-
-            case BX_DOL_AI_AUTOMATOR_SCHEDULER:
-                $mixedResult = [
-                    'params' => [
-                        'scheduler_time' => $sResponse
-                    ]
-                ];
-                break;
-
-            case BX_DOL_AI_AUTOMATOR_WEBHOOK:
-                break;
-        }
-        
-
-        return $mixedResult;
-    }
-
-    public function getResponse($sType, $mixedMessage, $aParams = [])
-    {
-        $bMessage = is_a($mixedMessage, 'BxDolAIMessageString');
-        $bMessages = is_a($mixedMessage, 'BxDolAIMessages');
-        if(!$bMessage && !$bMessages)
-            return false;
-
-        $aCall = [
-            ['role' => 'system', 'content' => $this->_getInstructions($sType, true)]
-        ];
-
-        if($bMessages && ($aMessages = $mixedMessage->getAll()))
-            foreach($aMessages as $oMessage)
-                $aCall[] = [
-                    'role' => $oMessage->isAi() ? 'assistant' : 'user',
-                    'content' => $oMessage->getContent() . (!$oMessage->isAi() ? ' return only function, without explanation or false if you havn`t enougth data' : '')
-                ];
-        else if($bMessage)
-            $aCall[] = [
-                'role' => $oMessage->isAi() ? 'assistant' : 'user', 
-                'content' => $mixedMessage->getContent()
-            ];
-
-        $sResponse = $this->call($aCall);
-        if($sResponse == 'false')
-            return false;
-
-        return $sResponse;
-    }
-
-    public function call($aMessages, $aParams = [])
-    {
-        $aData = [
-            'model' => $this->_sName,
-            'messages' => $aMessages
-        ];
-
-        if(!empty($this->_aParams['call']) && is_array($this->_aParams['call']))
-            $aData = array_merge($aData, $this->_aParams['call']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $sResponse = bx_file_get_contents($this->_sEndpoint, $aData, "post-json", [
-            "Authorization: Bearer " . $this->_sKey, 
-            'Content-Type: application/json', 
-            'OpenAI-Beta: assistants=v1'
-        ]);
-
-        $aResponse = json_decode($sResponse, true);
-        if(isset($aResponse['error'])) {
-            $this->_log($aResponse['error']);
-            return 'false';
-        }
-
-        return trim(str_replace(['```json', '```php', '```'], '', $aResponse['choices'][0]['message']['content']));
-    }
-
-    protected function _getInstructions($sType, $bIncludeCommon = false)
-    {
-        $sResult = file_get_contents($this->_sPathInst . $sType . '.html');
-        if($bIncludeCommon)
-            $sResult .= file_get_contents($this->_sPathInst. 'common.html');
-
-        return $sResult;
-    }
-}

+ 0 - 521
inc/classes/BxDolAIModelGpt40.php

@@ -1,521 +0,0 @@
-<?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 BxDolAIModelGpt40 extends BxDolAIModel
-{
-    public static $NAME = 'gpt-4o';
-
-    protected $_sEndpoint;
-    protected $_sEndpointRuns;
-    protected $_sEndpointRunsCheck;
-    protected $_sEndpointMessages;
-
-    protected $_sEndpointAssistants;
-    protected $_sEndpointAssistantsModify;
-    protected $_sEndpointAssistantsDelete;
-
-    protected $_sEndpointFiles;
-    protected $_sEndpointFilesRetrieve;
-    protected $_sEndpointFilesRetrieveContent;
-    protected $_sEndpointFilesDelete;
-
-    protected $_sEndpointVectorStores;
-    protected $_sEndpointVectorStoresDelete;
-
-    protected $_sEndpointVectorStoresFiles;
-    protected $_sEndpointVectorStoresFilesRetrieve;
-    protected $_sEndpointVectorStoresFilesDelete;   
-
-    protected $_sEndpointChat;
-
-    public function __construct($aModel)
-    {
-        $this->_sName = self::$NAME;
-
-        parent::__construct($aModel);
-
-        $this->_sEndpoint = 'https://api.openai.com/v1/threads';
-        $this->_sEndpointRuns = $this->_sEndpoint . '/%s/runs';
-        $this->_sEndpointRunsCheck = $this->_sEndpoint . '/%s/runs/%s';
-        $this->_sEndpointMessages = $this->_sEndpoint . '/%s/messages';
-
-        $this->_sEndpointAssistants = 'https://api.openai.com/v1/assistants';
-        $this->_sEndpointAssistantsModify = $this->_sEndpointAssistants . '/%s';
-        $this->_sEndpointAssistantsDelete = $this->_sEndpointAssistants . '/%s';
-
-        $this->_sEndpointFiles = 'https://api.openai.com/v1/files';
-        $this->_sEndpointFilesRetrieve = $this->_sEndpointFiles . '/%s';
-        $this->_sEndpointFilesRetrieveContent = $this->_sEndpointFiles . '/%s/content';
-        $this->_sEndpointFilesDelete = $this->_sEndpointFilesRetrieve;
-
-        $this->_sEndpointVectorStores = 'https://api.openai.com/v1/vector_stores';
-        $this->_sEndpointVectorStoresDelete = $this->_sEndpointVectorStores . '/%s';
-
-        $this->_sEndpointVectorStoresFiles = $this->_sEndpointVectorStores . '/%s/files';
-        $this->_sEndpointVectorStoresFilesRetrieve = $this->_sEndpointVectorStoresFiles . '/%s';
-        $this->_sEndpointVectorStoresFilesDelete = $this->_sEndpointVectorStoresFiles . '/%s';
-
-        $this->_sEndpointChat = 'https://api.openai.com/v1/chat/completions';
-    }
-    
-    public function getResponseText($sPrompt, $sMessage)
-    {
-        $aMessages = [
-            ['role' => 'system', 'content' => $sPrompt],
-            ['role' => 'user', 'content' => $sMessage]
-        ];
-
-        $sResponse = $this->callChat($aMessages);
-        if($sResponse == 'false')
-            return false;
-        
-        return $sResponse;
-    }
-
-    public function getResponseInit($sType, $oMessage, $aParams = [])
-    {
-        if(!is_a($oMessage, 'BxDolAIMessage'))
-            return false;
-
-        $aResponse = $this->call(['messages' => [['role' => 'user', 'content' => $oMessage->getContent(), 'attachments' => $oMessage->getAttachments()]]]);
-        if(!isset($aResponse['id'], $aResponse['object']) || $aResponse['object'] != 'thread')
-            return false;
-
-        $sThreadId = $aResponse['id'];
-        $sAssistantId = isset($aParams['assistant_id']) ? $aParams['assistant_id'] : $this->_getAssistantId($sType . '_init');
-
-        if(!$this->callRuns($sThreadId, ['assistant_id' => $sAssistantId]))
-            return false;
-
-        $sResponse = $this->getMessages($sThreadId);
-
-        $mixedResult = [];
-        switch($sType) {
-            case BX_DOL_AI_AUTOMATOR_EVENT:
-                $aResponse = json_decode($sResponse, true);
-
-                $mixedResult = [
-                    'alert_unit' => $aResponse['alert_unit'],
-                    'alert_action' => $aResponse['alert_action'],
-                    'params' => [
-                        'thread_id' => $sThreadId,
-                        'trigger' => $aResponse['trigger']
-                    ]
-                ];
-                break;
-
-            case BX_DOL_AI_AUTOMATOR_SCHEDULER:
-                $mixedResult = [
-                    'params' => [
-                        'thread_id' => $sThreadId,
-                        'scheduler_time' => $sResponse
-                    ]
-                ];
-                break;
-
-            case BX_DOL_AI_AUTOMATOR_WEBHOOK:
-            case BX_DOL_AI_ASSISTANT:
-                $mixedResult = [
-                    'params' => [
-                        'thread_id' => $sThreadId,
-                    ]
-                ];
-                break;
-        }
-
-        return $mixedResult;
-    }
-
-    public function getResponse($sType, $mixedMessage, $aParams = [])
-    {
-        $bMessages = false;
-        if(!(is_a($mixedMessage, 'BxDolAIMessage')) && !($bMessages = is_a($mixedMessage, 'BxDolAIMessages')))
-            return false;
-
-        if(empty($aParams['thread_id']))
-            return false;
-
-        if($bMessages)
-            $mixedMessage = $mixedMessage->getLast();
-
-        $sThreadId = $aParams['thread_id'];
-        if($mixedMessage->isAi() || !$this->callMessages($sThreadId, ['role' => 'user', 'content' => $mixedMessage->getContent(), 'attachments' => $mixedMessage->getAttachments()]))
-            return false;
-
-        $aResponse = $this->callRuns($sThreadId, [
-            'assistant_id' => isset($aParams['assistant_id']) ? $aParams['assistant_id'] : $this->_getAssistantId($sType)
-        ]);
-
-        if(!$aResponse || (isset($aResponse['status']) && $aResponse['status'] != 'completed')) {
-            if(!empty($aResponse['last_error']) && is_array($aResponse['last_error']))
-                return $aResponse['last_error']['message'];
-
-            return false;
-        }
-
-        return $this->getMessages($sThreadId);
-    }
-
-    public function getAssistant($aParams = [])
-    {
-        $aResponseVs = $this->callVectorStores(['name' => $aParams['name']]);
-        if($aResponseVs === false)
-            return false;
-        
-        $sVectorStoreId = $aResponseVs['id'];
-
-        $aResponseAsst = $this->callAssistants([
-            'model' => $this->_sName, 
-            'name' => $aParams['name'], 
-            'instructions' => $aParams['prompt'], 
-            'tools' => [
-                ['type' => 'file_search']
-            ],
-            'tool_resources' => [
-                'file_search' => [
-                    'vector_store_ids' => [$sVectorStoreId]
-                ]
-            ]
-        ]);
-        if($aResponseAsst === false)
-            return false;
-        
-        $sAssistantId = $aResponseAsst['id'];
-
-        return [
-            'vector_store_id' => $sVectorStoreId,
-            'assistant_id' => $sAssistantId
-        ];
-    }
-
-    public function editAssistant($sId, $aParams = [])
-    {
-        $aResponseAsst = $this->callAssistantsModify($sId, [
-            'name' => $aParams['name'], 
-            'instructions' => $aParams['prompt']
-        ]);
-
-        return $aResponseAsst !== false;
-    }
-
-    public function call($aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call']) && is_array($this->_aParams['call']))
-            $aData = array_merge($aData, $this->_aParams['call']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        return $this->_call($this->_sEndpoint, $aData);
-        
-    }
-
-    public function callRuns($sThreadId, $aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_runs']) && is_array($this->_aParams['call_runs']))
-            $aData = array_merge($aData, $this->_aParams['call_runs']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $aResponse = $this->_call(sprintf($this->_sEndpointRuns, $sThreadId), $aData);
-        if($aResponse !== false && isset($aResponse['id'])) {
-            $sRunId = $aResponse['id'];
-
-            while(in_array($aResponse['status'], ['queued', 'in_progress'])) {
-                sleep(1);
-
-                $aResponse = $this->_call(sprintf($this->_sEndpointRunsCheck, $sThreadId, $sRunId), [], 'get');
-            }
-        }
-
-        return $aResponse;
-    }
-
-    public function callMessages($sThreadId, $aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_messages']) && is_array($this->_aParams['call_messages']))
-            $aData = array_merge($aData, $this->_aParams['call_messages']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $mixedResponse = $this->_call(sprintf($this->_sEndpointMessages, $sThreadId), $aData);
-        if($mixedResponse !== false)
-            $mixedResponse = $mixedResponse['content'][0]['text']['value'];
-
-        return $mixedResponse;
-    }
-
-    /**
-     * Create a vector store.
-     * 
-     * @param type $aParams - should have 'name'
-     * @return boolean
-     */
-    public function callVectorStores($aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_vs']) && is_array($this->_aParams['call_vs']))
-            $aData = array_merge($aData, $this->_aParams['call_vs']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $mixedResponse = $this->_call($this->_sEndpointVectorStores, $aData);
-        if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'vector_store')
-            return false;
-
-        return $mixedResponse;
-    }
-    
-    public function callVectorStoresDelete($sVectorStoreId, $aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_vs_delete']) && is_array($this->_aParams['call_vs_delete']))
-            $aData = array_merge($aData, $this->_aParams['call_vs_delete']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $mixedResponse = $this->_call(sprintf($this->_sEndpointVectorStoresDelete, $sVectorStoreId), [], 'DELETE');
-        if(empty($mixedResponse) || !is_array($mixedResponse) || !$mixedResponse['deleted'])
-            return false;
-
-        return $mixedResponse;
-    }
-
-    /**
-     * Create a vector store file by attaching a File to a vector store.
-     * 
-     * @param type $sVectorStoreId
-     * @param type $aParams - should have 'file_id'
-     * @return boolean
-     */
-    public function callVectorStoresFiles($sVectorStoreId, $aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_vs_files']) && is_array($this->_aParams['call_vs_files']))
-            $aData = array_merge($aData, $this->_aParams['call_vs_files']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $mixedResponse = $this->_call(sprintf($this->_sEndpointVectorStoresFiles, $sVectorStoreId), $aData);
-        if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'vector_store.file')
-            return false;
-
-        return $mixedResponse;
-    }
-    
-    public function callVectorStoresFilesList($sVectorStoreId, $aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_vs_flist']) && is_array($this->_aParams['call_vs_flist']))
-            $aData = array_merge($aData, $this->_aParams['call_vs_flist']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $mixedResponse = $this->_call(sprintf($this->_sEndpointVectorStoresFiles, $sVectorStoreId), $aData, 'get');
-        if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'list')
-            return false;
-
-        return $mixedResponse['data'];
-    }
-
-    public function callVectorStoresFilesRetrieveFile($sVectorStoreId, $sFileId)
-    {
-        return $this->_call(sprintf($this->_sEndpointVectorStoresFilesRetrieve, $sVectorStoreId, $sFileId), [], 'get');
-    }
-    
-    public function callVectorStoresFilesDelete($sVectorStoreId, $sFileId, $aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_vs_files_retrieve']) && is_array($this->_aParams['call_vs_files_retrieve']))
-            $aData = array_merge($aData, $this->_aParams['call_vs_files_retrieve']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        return $this->_call(sprintf($this->_sEndpointVectorStoresFilesDelete, $sVectorStoreId, $sFileId), [], 'DELETE');
-    }
-
-    public function callAssistants($aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_assts']) && is_array($this->_aParams['call_assts']))
-            $aData = array_merge($aData, $this->_aParams['call_assts']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $mixedResponse = $this->_call($this->_sEndpointAssistants, $aData);
-        if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'assistant')
-            return false;
-
-        return $mixedResponse;
-    }
-
-    public function callAssistantsModify($sAsstId, $aParams = [])
-    {
-        $aData = [];
-        if(($sKey = 'call_assts_modify') && !empty($this->_aParams[$sKey]) && is_array($this->_aParams[$sKey]))
-            $aData = array_merge($aData, $this->_aParams[$sKey]);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $mixedResponse = $this->_call(sprintf($this->_sEndpointAssistantsModify, $sAsstId), $aData);
-        if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'assistant')
-            return false;
-
-        return $mixedResponse;
-    }
-
-    public function callAssistantsDelete($sAsstId, $aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_assts_delete']) && is_array($this->_aParams['call_assts_delete']))
-            $aData = array_merge($aData, $this->_aParams['call_assts_delete']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $mixedResponse = $this->_call(sprintf($this->_sEndpointAssistantsDelete, $sAsstId), $aData, 'DELETE');
-        if(empty($mixedResponse) || !is_array($mixedResponse) || !$mixedResponse['deleted'])
-            return false;
-
-        return $mixedResponse;
-    }
-
-    public function callFiles($aFile, $aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_files']) && is_array($this->_aParams['call_files']))
-            $aData = array_merge($aData, $this->_aParams['call_files']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        if(empty($aData['purpose']))
-            $aData['purpose'] = 'assistants';
-
-        $sName = !empty($aFile['name']) ? $aFile['name'] : 'file_' . time() . '.txt';
-        $sMime = !empty($aFile['mime']) ? $aFile['mime'] : 'text/plain';
-        $aData['file'] = new CURLStringFile($aFile['content'], $sName, $sMime);
-
-        $mixedResponse = $this->_callFiles($this->_sEndpointFiles, $aData);
-        if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'file')
-            return false;
-
-        return $mixedResponse;
-    }
-
-    public function callFilesRetrieve($sFileId)
-    {
-        $mixedResponse = $this->_callFiles(sprintf($this->_sEndpointFilesRetrieve, $sFileId), [], 'get');
-        if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'file')
-            return false;
-
-        return $mixedResponse;
-    }
-
-    public function callFilesRetrieveContent($sFileId)
-    {
-        return bx_file_get_contents(sprintf($this->_sEndpointFilesRetrieveContent, $sFileId), [], 'get', [
-            "Authorization: Bearer " . $this->_sKey
-        ]);
-    }
-
-    public function callFilesDelete($sFileId, $aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['call_files_delete']) && is_array($this->_aParams['call_files_delete']))
-            $aData = array_merge($aData, $this->_aParams['call_files_delete']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $mixedResponse = $this->_callFiles(sprintf($this->_sEndpointFilesDelete, $sFileId), $aData, 'DELETE');
-        if(empty($mixedResponse) || !is_array($mixedResponse) || $mixedResponse['object'] != 'file')
-            return false;
-
-        return $mixedResponse;
-    }
-
-    public function callChat($aMessages, $aParams = [])
-    {
-        $aData = [
-            'model' => $this->_sName,
-            'messages' => $aMessages
-        ];
-
-        if(!empty($this->_aParams['call']) && is_array($this->_aParams['call']))
-            $aData = array_merge($aData, $this->_aParams['call']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $sResponse = bx_file_get_contents($this->_sEndpointChat, $aData, 'post-json', [
-            'Authorization: Bearer ' . $this->_sKey, 
-            'OpenAI-Beta: assistants=v1'
-        ]);
-
-        $aResponse = json_decode($sResponse, true);
-        if(isset($aResponse['error'])) {
-            $this->_log($aResponse['error']);
-            return 'false';
-        }
-
-        return trim(str_replace(['```json', '```php', '```'], '', $aResponse['choices'][0]['message']['content']));
-    }
-
-    public function getMessages($sThreadId, $aParams = [])
-    {
-        $aData = [];
-        if(!empty($this->_aParams['get_messages']) && is_array($this->_aParams['get_messages']))
-            $aData = array_merge($aData, $this->_aParams['get_messages']);
-        if(!empty($aParams) && is_array($aParams))
-            $aData = array_merge($aData, $aParams);
-
-        $mixedResponse = $this->_call(sprintf($this->_sEndpointMessages, $sThreadId), $aData, "get");
-        if($mixedResponse !== false)
-            $mixedResponse = trim(str_replace(['```json', '```php', '```'], '', $mixedResponse['data'][0]['content'][0]['text']['value']));
-
-        return $mixedResponse;
-    }
-
-    protected function _call($sEndpoint, $aData, $sMethod = 'post-json')
-    {
-        $sResponse = bx_file_get_contents($sEndpoint, $aData, $sMethod, array_merge([
-            'Authorization: Bearer ' . $this->_sKey, 
-            'OpenAI-Beta: assistants=v2'
-        ], ($sMethod != 'post-json' ? ['Content-Type: application/json'] : [])));
-
-        $aResponse = json_decode($sResponse, true);
-        if(isset($aResponse['error'])) {
-            $this->setError($aResponse['error']);
-
-            $this->_log(['endpoint' => $sEndpoint, 'method' => $sMethod, 'error' => $aResponse['error']]);
-            return false;
-        }
-
-        return $aResponse;
-    }
-
-    protected function _callFiles($sEndpoint, $aData, $sMethod = "post-raw")
-    {
-        $sResponse = bx_file_get_contents($sEndpoint, $aData, $sMethod, [
-            "Authorization: Bearer " . $this->_sKey
-        ]);
-
-        $aResponse = json_decode($sResponse, true);
-        if(isset($aResponse['error'])) {
-            $this->_log($aResponse['error']);
-            return false;
-        }
-
-        return $aResponse;
-    }
-
-    protected function _getAssistantId($sType)
-    {
-        return isset($this->_aParams['assistants'][$sType]) ? $this->_aParams['assistants'][$sType] : '';
-    }
-}

+ 0 - 142
inc/classes/BxDolAIProvider.php

@@ -1,142 +0,0 @@
-<?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 BxDolAIProvider extends BxDol
-{
-    protected $_oDb;
-    protected $_iId;
-    protected $_sProviderName;
-    protected $_sName;
-    protected $_sPrefix;
-    protected $_aOptions;
-
-    public function __construct($aProvider)
-    {
-        parent::__construct();
-
-        if(empty($aProvider) || !is_array($aProvider) || strcmp($aProvider['type']['name'], $this->_sProviderName) != 0)
-            $this->_log("Unexpected value provided for the credentials");
-
-        $this->_oDb = new BxDolAIQuery();
-
-        $this->_iId = (int)$aProvider['id'];
-        $this->_sProviderName = $aProvider['type']['name'];
-        $this->_sName = _t($aProvider['name']);
-        $this->_sPrefix = $aProvider['type']['option_prefix'];
-
-        $this->_aOptions = [];
-        if(!empty($aProvider['options']) && is_array($aProvider['options']))
-            $this->initOptions($aProvider['options']);
-    }
-
-    /**
-     * Get provider object instance by provider unique name
-     * @param $sName provider unique name
-     * @return object instance or false on error
-     */
-    public static function getObjectInstanceByName($sName)
-    {
-        $iId = BxDolAIQuery::getProviderIdByName($sName);
-        if(!$iId)
-            return false;
-
-        return self::getObjectInstance($iId);
-    }
-
-    /**
-     * Get provider object instance by provider ID
-     * @param $iId provider ID
-     * @return object instance or false on error
-     */
-    public static function getObjectInstance($iId)
-    {
-        $sPrefix = 'BxDolAIProvider!';
-
-        if(isset($GLOBALS['bxDolClasses'][$sPrefix . $iId]))
-            return $GLOBALS['bxDolClasses'][$sPrefix . $iId];
-
-        $aProvider = BxDolAIQuery::getProviderObject($iId);
-        if(!$aProvider || !is_array($aProvider))
-            return false;
-
-        $sClass = 'BxDolAIProvider';
-        if(!empty($aProvider['type']['class_name'])) {
-            $sClass = $aProvider['type']['class_name'];
-            if(!empty($aProvider['type']['class_file']))
-                require_once(BX_DIRECTORY_PATH_ROOT . $aProvider['type']['class_file']);
-        }
-
-        $o = new $sClass($aProvider);
-        return ($GLOBALS['bxDolClasses'][$sPrefix . $iId] = $o);
-    }
-
-    public function initOptions($aOptions)
-    {
-    	$this->_aOptions = $aOptions;
-    }
-
-    public function getInfo()
-    {
-        $aInfo = $this->_oDb->getProvidersBy(['sample' => 'id', 'id' => $this->_iId]);
-        $aInfo['options'] = $this->getOptions();
-
-        return $aInfo;
-    }
-
-    public function getOptions()
-    {
-        $aOptions = $this->_oDb->getProvidersBy(['sample' => 'options_by_id', 'id' => $this->_iId]);
-        foreach($aOptions as &$aOption)
-            $aOption['value'] = $this->getOption($aOption['name']);
-
-        return $aOptions;
-    }
-
-    public function getOption($sName)
-    {
-        $sCustomMethod = 'getOption' . bx_gen_method_name(str_replace($this->_sPrefix, '', $sName));
-        if(method_exists($this, $sCustomMethod))
-             return $this->$sCustomMethod();
-        
-    	if(substr($sName, 0, strlen($this->_sPrefix)) != $this->_sPrefix)
-            $sName = $this->_sPrefix . $sName;
-
-        return isset($this->_aOptions[$sName]) ? $this->_aOptions[$sName]['value'] : '';
-    }
-
-    public function processActionWebhook()
-    {
-        // Should be overwritten to process webhooks.
-    }
-
-    public function call($sRequest, $aParams, $sMethod = 'post-json', $aHeaders = [])
-    {
-        // Should be overwritten to process calls.
-    }
-
-    /**
-     * Internal methods.
-     */
-    protected function _log($mixedError, $bUseLog = true)
-    {
-        if(!$bUseLog) {
-            $sMessage = 'Error occurred';
-            if(is_string($mixedError))
-                $sMessage = $mixedError;
-            else if(is_array($mixedError) && isset($mixedError['message']))
-                $sMessage = $mixedError['message'];
-
-            throw new Exception($sMessage);
-        }
-        else
-            BxDolAI::getInstance()->log($mixedError, 'Providers');
-
-        return false;
-    }    
-}

+ 0 - 133
inc/classes/BxDolAIProviderShopifyAdmin.php

@@ -1,133 +0,0 @@
-<?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
- * @{
- */
-
-/*
- * Login for Owner/Collaborator account:
- * https://admin.shopify.com/store/your_shop/
- * 
- * Integration:
- * 0. Custom apps creation guide (for a shop Owner/Collaborator, not a Partner):
- * https://help.shopify.com/en/manual/apps/app-types/custom-apps
- * 
- * 1. Make authenticated requests to the REST Admin API:
- * https://shopify.dev/docs/apps/auth/admin-app-access-tokens
- * 
- * 2. REST Admin API reference:
- * https://shopify.dev/docs/api/admin-rest
- * 
- * 3. REST Admin API -> Product
- * https://shopify.dev/docs/api/admin-rest/2023-10/resources/product
- * 
- * 4. REST Admin API -> Order
- * https://shopify.dev/docs/api/admin-rest/2023-10/resources/order
- * 
- */
-class BxDolAIProviderShopifyAdmin extends BxDolAIProvider
-{
-    public static $PROVIDER_NAME = 'shopify_admin';
-
-    protected $_sShopDomain;
-    protected $_sAccessToken;
-
-    protected $_sEndpoint;
-    protected $_sStorefront;
-
-    public function __construct($aProvider)
-    {
-        $this->_sProviderName = self::$PROVIDER_NAME;
-
-        parent::__construct($aProvider);
-
-        $this->_sShopDomain = $this->getOption('shop_domain');
-        $this->_sAccessToken = $this->getOption('access_token');
-
-        $this->_sEndpoint = "https://{$this->_sShopDomain}/admin/api/2023-10/";
-        $this->_sStorefront = "https://{$this->_sShopDomain}/";
-    }
-
-    public function getEntry($sId)
-    {
-        $sProduct = $this->call('products/' . $sId . '.json', [
-            'fields' => 'id,title,handle,body_html,tags,variants',
-        ], 'get');
-        
-        if(empty($sProduct))
-            return [];
-
-        $aProduct = json_decode($sProduct, true);
-        if(empty($aProduct) || !is_array($aProduct) || empty($aProduct['product']))
-            return [];
-
-        return $aProduct['product'];
-    }
-
-    public function getOptionWebhookUrl()
-    {
-        return bx_append_url_params(BX_DOL_URL_ROOT . 'agents.php', [
-            'p' => $this->_iId,
-            'a' => 'webhook'
-        ]);
-    }
-
-    public function processActionWebhook()
-    {
-        $sData = @file_get_contents('php://input');
-
-        $this->_log(json_decode($sData, true));
-
-        $sHmacHeader = isset($_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256']) ? $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'] : '';
-        if(!$this->_verifyWebhook($sData, $sHmacHeader)) {
-            http_response_code(401);
-            return;
-        }
-
-        $oAi = BxDolAI::getInstance();
-
-        $aAutomators = $oAi->getAutomatorsWebhook($this->_iId);
-        foreach($aAutomators as $aAutomator)
-            $oAi->callAutomator(BX_DOL_AI_AUTOMATOR_WEBHOOK, [
-                'automator' => $aAutomator
-            ]);
-
-        http_response_code(200);
-    }
-
-    public function call($sRequest, $aParams, $sMethod = 'post-json', $aHeaders = [])
-    {
-        $aHeaders[] = 'Content-Type: application/json';
-        if(!empty($this->_sAccessToken))
-            $aHeaders[] = 'X-Shopify-Access-Token: ' . $this->_sAccessToken;
-        else
-            $aHeaders[] = 'Authorization: Basic ' . base64_encode($this->_sApiKey . ':' . $this->_sApiSecretKey);
-
-        $sResponse = bx_file_get_contents($this->_sEndpoint . $sRequest, $aParams, $sMethod, $aHeaders);
-        if(empty($sResponse))
-            return false;
-        
-        $aResponse = json_decode($sResponse, true);
-        if(empty($aResponse) || !is_array($aResponse))
-            return false;
-
-        return $aResponse;
-    }
-
-    /**
-     * Internal methods.
-     */    
-    protected function _dateI2S($iTimestamp)
-    {
-        return date("Y-m-d", $iTimestamp);
-    }
-
-    protected function _verifyWebhook($sData, $sHmacHeader)
-    {
-        $sHmacCalc = base64_encode(hash_hmac('sha256', $sData, $this->getOption('secret_key'), true));
-        return hash_equals($sHmacCalc, $sHmacHeader);
-    }
-}

+ 0 - 1117
inc/classes/BxDolAIQuery.php

@@ -1,1117 +0,0 @@
-<?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 BxDolAIQuery extends BxDolDb
-{
-    public function __construct()
-    {
-        parent::__construct();
-    }
-
-    static protected function _getObject(int $iId, string $sTable): array|bool
-    {
-        $oDb = BxDolDb::getInstance();
-
-        $a = $oDb->getRow("SELECT * FROM `$sTable` WHERE `id` = :id", ['id' => $iId]);
-        if(!$a || !is_array($a))
-            return false;
-
-        return $a;
-    }
-
-    static public function getModelObject(int $iId): array|bool
-    {
-        return self::_getObject($iId, 'sys_agents_models');
-    }
-
-    static public function getVectorStoreObject(int $iId): array|bool
-    {
-        return self::_getObject($iId, 'sys_agents_vector_store');
-    }
-
-    static public function getAgentObject(int $iId): array|bool
-    {
-        return self::_getObject($iId, 'sys_agents_agents');
-    }
-
-    static public function getToolObject(int $iId): array|bool
-    {
-        return self::_getObject($iId, 'sys_agents_tools');
-    }
-
-    static public function getProviderObject(int $iId)
-    {
-        $oDb = BxDolDb::getInstance();
-
-        $aProvider = $oDb->getRow("SELECT * FROM `sys_agents_providers` WHERE `id` = :id", ['id' => $iId]);
-        if(!$aProvider || !is_array($aProvider))
-            return false;
-
-        // get type
-        $aProviderType = $oDb->getRow("SELECT * FROM `sys_agents_provider_types` WHERE `id` = :id", ['id' => $aProvider['type_id']]);
-        if(!$aProviderType || !is_array($aProviderType))
-            return false;
-
-        $aProvider['type'] = $aProviderType;
-
-        // get options
-        $sQuery = "SELECT
-               `tpo`.`name` AS `name`,
-               `tpv`.`value` AS `value`
-            FROM `sys_agents_provider_options` AS `tpo`
-            LEFT JOIN `sys_agents_providers_values` AS `tpv` ON `tpo`.`id` = `tpv`.`option_id` AND `tpv`.`provider_id` = :provider_id
-            WHERE 1 ORDER BY `tpo`.`order`";
-
-        $aProvider['options'] = $oDb->getAllWithKey($sQuery, 'name', [
-            'provider_id' => $iId
-        ]);
-
-        return $aProvider;
-    }
-
-    static public function getProviderIdByName($sName)
-    {
-        return (int)BxDolDb::getInstance()->getOne("SELECT `id` FROM `sys_agents_providers` WHERE `name`=:name LIMIT 1", [
-            'name' => $sName
-        ]);
-    }
-    
-    static public function getAssistantObject($iId)
-    {
-        $oDb = BxDolDb::getInstance();
-
-        $aAssistant = $oDb->getRow("SELECT * FROM `sys_agents_assistants` WHERE `id` = :id", ['id' => $iId]);
-        if(!$aAssistant || !is_array($aAssistant))
-            return false;
-
-        return $aAssistant;
-    }
-
-    public function insertModel($aModel)
-    {
-        if(empty($aModel))
-            return false;
-
-        return (int)$this->query("INSERT INTO `sys_agents_models` SET " . $this->arrayToSQL($aModel));
-    }
-
-    public function getModelsBy($aParams = [])
-    {
-        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
-    	$sWhereClause = "";
-
-        switch($aParams['sample']) {
-            case 'id':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sWhereClause .= " AND `id`=:id";
-                break;
-
-            case 'all_pairs':
-                $aMethod['name'] = 'getPairs';
-                $aMethod['params'][1] = 'id';
-                $aMethod['params'][2] = 'title';
-                $aMethod['params'][3] = [];
-
-                if(isset($aParams['active'])) {
-                    $aMethod['params'][3]['active'] = $aParams['active'];
-
-                    $sWhereClause .= " AND `active`=:active";
-                }
-
-                if(isset($aParams['capabilities'])) {
-                    if (is_array($aParams['capabilities']))
-                    {                        
-                        $sWhereClause .= " AND `capabilities` IN (" . $this->implode_escape($aParams['capabilities']) . ")"; 
-                    }
-                    else {
-                        $aMethod['params'][3]['capabilities'] = $aParams['capabilities'];
-                        $sWhereClause .= " AND `capabilities` = :capabilities";
-                    }
-                }
-                break;
-        }
-
-        $aMethod['params'][0] = "SELECT * 
-            FROM `sys_agents_models`
-            WHERE 1" . $sWhereClause;
-
-        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
-    }
-
-    public function getAutomatorsBy($aParams = [], $bFromCache = true)
-    {
-        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
-        $sSelectClause = "`taa`.*";
-    	$sPostfix = $sJoinClause = $sWhereClause = "";
-
-        switch($aParams['sample']) {
-            case 'id':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sWhereClause .= " AND `taa`.`id`=:id";
-                break;
-
-            case 'id_full':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sSelectClause .= ", `tam`.`name` AS `model_name`, `tam`.`title` AS `model_title`, `tam`.`key` AS `model_key`, `tam`.`params` AS `model_params`";
-                $sJoinClause .= " LEFT JOIN `sys_agents_models` AS `tam` ON `taa`.`model_id`=`tam`.`id`";
-                $sWhereClause .= " AND `taa`.`id`=:id";
-                break;
-
-            case 'type':
-                $sPostfix .= '_' . $aParams['type'];
-
-                $aMethod['params'][1] = [
-                    'type' => $aParams['type']
-                ];
-
-                $sWhereClause .= " AND `taa`.`type`=:type";
-
-                if(isset($aParams['active'])) {
-                    $iActive = (int)$aParams['active'];
-                    $sPostfix .= '_' . (!$iActive ? 'in' : '') . 'active';
-
-                    $aMethod['params'][1]['active'] = $iActive;
-                    $sWhereClause .= " AND `taa`.`active`=:active";
-                }
-                break;
-
-            case 'events':
-                $aBindings = [
-                    'type' => BX_DOL_AI_AUTOMATOR_EVENT,
-                    'alert_unit' => $aParams['alert_unit'],
-                    'alert_action' => $aParams['alert_action']
-                ];
-                $sPostfix .= '_' . implode('_', $aBindings);
-
-                $aMethod['params'][1] = $aBindings;
-                $sWhereClause .= " AND `taa`.`type`=:type AND `taa`.`alert_unit`=:alert_unit AND `taa`.`alert_action`=:alert_action";
-
-                if(isset($aParams['active'])) {
-                    $iActive = (int)$aParams['active'];
-                    $sPostfix .= '_' . (!$iActive ? 'in' : '') . 'active';
-
-                    $aMethod['params'][1]['active'] = $iActive;
-                    $sWhereClause .= " AND `taa`.`active`=:active";
-                }
-                break;
-
-            case 'schedulers':
-                $aMethod['params'][1] = [
-                    'type' => BX_DOL_AI_AUTOMATOR_SCHEDULER,
-                ];
-
-                $sWhereClause .= " AND `taa`.`type`=:type";
-
-                if(isset($aParams['active'])) {
-                    $aMethod['params'][1]['active'] = (int)$aParams['active'];
-
-                    $sWhereClause .= " AND `taa`.`active`=:active";
-                }
-                break;
-
-            case 'webhooks':
-                $aMethod['params'][1] = [
-                    'type' => BX_DOL_AI_AUTOMATOR_WEBHOOK,
-                ];
-
-                $sWhereClause .= " AND `taa`.`type`=:type";
-
-                if(isset($aParams['provider_id'])) {
-                    $aMethod['params'][1]['provider_id'] = (int)$aParams['provider_id'];
-
-                    $sJoinClause = "INNER JOIN `sys_agents_automators_providers` AS `tap` ON `taa`.`id`=`tap`.`automator_id`";
-                    $sWhereClause .= " AND `tap`.`provider_id`=:provider_id";
-                }
-
-                if(isset($aParams['active'])) {
-                    $aMethod['params'][1]['active'] = (int)$aParams['active'];
-
-                    $sWhereClause .= " AND `taa`.`active`=:active";
-                }
-                break;
-
-            case 'providers_by_id_pairs':
-                $aMethod['name'] = 'getPairs';
-                $aMethod['params'][1] = 'id';
-                $aMethod['params'][2] = 'provider_id';
-                $aMethod['params'][3] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sSelectClause = "`taap`.`id`, `taap`.`provider_id`";
-                $sJoinClause = "INNER JOIN `sys_agents_automators_providers` AS `taap` ON `taa`.`id`=`taap`.`automator_id`";
-                $sWhereClause = " AND `taa`.`id`=:id";
-                break;
-            
-            case 'helpers_by_id_pairs':
-                $aMethod['name'] = 'getPairs';
-                $aMethod['params'][1] = 'id';
-                $aMethod['params'][2] = 'helper_id';
-                $aMethod['params'][3] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sSelectClause = "`taah`.`id`, `taah`.`helper_id`";
-                $sJoinClause = "INNER JOIN `sys_agents_automators_helpers` AS `taah` ON `taa`.`id`=`taah`.`automator_id`";
-                $sWhereClause = " AND `taa`.`id`=:id";
-                break;
-            
-            case 'assistants_by_id_pairs':
-                $aMethod['name'] = 'getPairs';
-                $aMethod['params'][1] = 'id';
-                $aMethod['params'][2] = 'assistant_id';
-                $aMethod['params'][3] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sSelectClause = "`taaa`.`id`, `taaa`.`assistant_id`";
-                $sJoinClause = "INNER JOIN `sys_agents_automators_assistants` AS `taaa` ON `taa`.`id`=`taaa`.`automator_id`";
-                $sWhereClause = " AND `taa`.`id`=:id";
-                break;
-        }
-
-        $aMethod['params'][0] = "SELECT " . $sSelectClause . "
-            FROM `sys_agents_automators` AS `taa` " . $sJoinClause . "
-            WHERE 1" . $sWhereClause;
-
-        if(!$bFromCache || empty($sPostfix))
-            return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
-
-        return call_user_func_array([$this, 'fromMemory'], array_merge(['sys_automators' . $sPostfix, $aMethod['name']], $aMethod['params']));
-    }
-
-    public function updateAutomators($aSetClause, $aWhereClause)
-    {
-        if(empty($aSetClause) || empty($aWhereClause))
-            return false;
-
-        return (int)$this->query("UPDATE `sys_agents_automators` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0;
-    }
-
-    public function insertAutomatorProvider($aParamsSet)
-    {
-        if(empty($aParamsSet))
-            return false;
-
-        return (int)$this->query("INSERT INTO `sys_agents_automators_providers` SET " . $this->arrayToSQL($aParamsSet)) > 0 ? (int)$this->lastId() : false;
-    }
-
-    public function updateAutomatorProvider($aParamsSet, $aParamsWhere)
-    {
-        if(empty($aParamsSet) || empty($aParamsWhere))
-            return false;
-
-        return $this->query("UPDATE `sys_agents_automators_providers` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
-    }
-
-    public function deleteAutomatorProviders($aParamsWhere)
-    {
-        if(empty($aParamsWhere))
-            return false;
-
-        return (int)$this->query("DELETE FROM `sys_agents_automators_providers` WHERE " . $this->arrayToSQL($aParamsWhere)) > 0;
-    }
-    
-    public function deleteAutomatorProvidersById($mixedId)
-    {
-        if(!is_array($mixedId))
-            $mixedId = [$mixedId];
-
-        return (int)$this->query("DELETE FROM `sys_agents_automators_providers` WHERE `id` IN (" . $this->implode_escape($mixedId) . ")") > 0;
-    }
-
-    public function insertAutomatorHelper($aParamsSet)
-    {
-        if(empty($aParamsSet))
-            return false;
-
-        return (int)$this->query("INSERT INTO `sys_agents_automators_helpers` SET " . $this->arrayToSQL($aParamsSet)) > 0 ? (int)$this->lastId() : false;
-    }
-
-    public function updateAutomatorHelper($aParamsSet, $aParamsWhere)
-    {
-        if(empty($aParamsSet) || empty($aParamsWhere))
-            return false;
-
-        return $this->query("UPDATE `sys_agents_automators_helpers` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
-    }
-
-    public function deleteAutomatorHelpers($aParamsWhere)
-    {
-        if(empty($aParamsWhere))
-            return false;
-
-        return (int)$this->query("DELETE FROM `sys_agents_automators_helpers` WHERE " . $this->arrayToSQL($aParamsWhere)) > 0;
-    }
-
-    public function deleteAutomatorHelpersById($mixedId)
-    {
-        if(!is_array($mixedId))
-            $mixedId = [$mixedId];
-
-        return (int)$this->query("DELETE FROM `sys_agents_automators_helpers` WHERE `id` IN (" . $this->implode_escape($mixedId) . ")") > 0;
-    }
-
-    public function insertAutomatorAssistant($aParamsSet)
-    {
-        if(empty($aParamsSet))
-            return false;
-
-        return (int)$this->query("INSERT INTO `sys_agents_automators_assistants` SET " . $this->arrayToSQL($aParamsSet)) > 0 ? (int)$this->lastId() : false;
-    }
-
-    public function updateAutomatorAssistant($aParamsSet, $aParamsWhere)
-    {
-        if(empty($aParamsSet) || empty($aParamsWhere))
-            return false;
-
-        return $this->query("UPDATE `sys_agents_automators_assistants` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
-    }
-
-    public function deleteAutomatorAssistants($aParamsWhere)
-    {
-        if(empty($aParamsWhere))
-            return false;
-
-        return (int)$this->query("DELETE FROM `sys_agents_automators_assistants` WHERE " . $this->arrayToSQL($aParamsWhere)) > 0;
-    }
-    
-    public function deleteAutomatorAssistantsById($mixedId)
-    {
-        if(!is_array($mixedId))
-            $mixedId = [$mixedId];
-
-        return (int)$this->query("DELETE FROM `sys_agents_automators_assistants` WHERE `id` IN (" . $this->implode_escape($mixedId) . ")") > 0;
-    }
-
-    public function getProviderTypesBy($aParams = [])
-    {
-        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
-    	$sWhereClause = "";
-
-        switch($aParams['sample']) {
-            case 'id':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sWhereClause .= " AND `id`=:id";
-                break;
-
-            case 'name':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'name' => $aParams['name']
-                ];
-
-                $sWhereClause .= " AND `name`=:name";
-                break;
-
-            case 'all_pairs':
-                $aMethod['name'] = 'getPairs';
-                $aMethod['params'][1] = 'id';
-                $aMethod['params'][2] = 'title';
-
-                if(isset($aParams['active'])) {
-                    $aMethod['params'][3] = [
-                        'active' => $aParams['active']
-                    ];
-
-                    $sWhereClause = " AND `active`=:active";
-                }
-                break;
-        }
-
-        $aMethod['params'][0] = "SELECT * 
-            FROM `sys_agents_provider_types`
-            WHERE 1" . $sWhereClause;
-
-        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
-    }
-
-    public function getProviderOptionsBy($aParams = [])
-    {
-        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
-    	$sWhereClause = "";
-
-        switch($aParams['sample']) {
-            case 'id':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sWhereClause .= " AND `id`=:id";
-                break;
-
-            case 'provider_type_id':
-            	$aMethod['params'][1] = [
-                    'provider_type_id' => $aParams['provider_type_id']
-                ];
-
-                $sWhereClause .= " AND `provider_type_id`=:provider_type_id";
-                break;
-        }
-
-        $aMethod['params'][0] = "SELECT * 
-            FROM `sys_agents_provider_options`
-            WHERE 1" . $sWhereClause;
-
-        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
-    }
-
-    public function getProvidersBy($aParams = [])
-    {
-    	$aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
-
-    	$sSelectClause = "`tp`.*";
-    	$sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
-        switch($aParams['sample']) {
-            case 'id':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sWhereClause = " AND `tp`.`id`=:id";
-                break;
-
-            case 'ids':
-                $sSelectClause .= ", `tpt`.`name` AS `type_name`";
-                $sJoinClause = "INNER JOIN `sys_agents_provider_types` AS `tpt` ON `tp`.`type_id`=`tpt`.`id`";
-                $sWhereClause = " AND `tp`.`id` IN (" . $this->implode_escape($aParams['ids']) . ")";
-                break;
-                 
-            case 'options_by_id':
-                $aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sSelectClause = "`tpo`.`id`, `tpo`.`name`, `tpo`.`type`, `tpo`.`title`, `tpo`.`description`, `tpv`.`value`";
-                $sJoinClause = "LEFT JOIN `sys_agents_provider_options` AS `tpo` ON `tp`.`type_id`=`tpo`.`provider_type_id` LEFT JOIN `sys_agents_providers_values` AS `tpv` ON `tp`.`id`=`tpv`.`provider_id` AND `tpo`.`id`=`tpv`.`option_id`";
-                $sWhereClause = " AND `tp`.`id`=:id";
-                break;
-            
-            case 'all_pairs':
-                $aMethod['name'] = 'getPairs';
-                $aMethod['params'][1] = 'id';
-                $aMethod['params'][2] = 'name';
-
-                if(isset($aParams['active'])) {
-                    $aMethod['params'][3] = [
-                        'active' => $aParams['active']
-                    ];
-
-                    $sWhereClause = " AND `tp`.`active`=:active";
-                }
-                break;
-        }
-
-        $sOrderClause = !empty($sOrderClause) ? "ORDER BY " . $sOrderClause : $sOrderClause;
-        $sLimitClause = !empty($sLimitClause) ? "LIMIT " . $sLimitClause : $sLimitClause;
-
-        $aMethod['params'][0] = "SELECT
-                " . $sSelectClause . "
-            FROM `sys_agents_providers` AS `tp` " . $sJoinClause . "
-            WHERE 1" . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
-
-        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
-    }
-
-    public function insertProviderValue($aParamsSet)
-    {
-        if(empty($aParamsSet) || !is_array($aParamsSet) || !isset($aParamsSet['value']))
-            return false;
-
-        return (int)$this->query("INSERT INTO `sys_agents_providers_values` SET " . $this->arrayToSQL($aParamsSet) . " ON DUPLICATE KEY UPDATE `value`=:value", [
-            'value' => $aParamsSet['value']
-        ]) > 0 ? (int)$this->lastId() : false;
-    }
-
-    public function updateProviderValue($aParamsSet, $aParamsWhere)
-    {
-        if(empty($aParamsSet))
-            return false;
-
-        $sWhereClause = "1";
-        if(!empty($aParamsWhere) && is_array($aParamsWhere))
-            $sWhereClause = $this->arrayToSQL($aParamsWhere, ' AND ');
-
-        return $this->query("UPDATE `sys_agents_providers_values` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $sWhereClause) !== false;
-    }
-
-    public function deleteProviderValues($aParamsWhere)
-    {
-        if(empty($aParamsWhere))
-            return false;
-
-        return (int)$this->query("DELETE FROM `sys_agents_providers_values` WHERE " . $this->arrayToSQL($aParamsWhere, ' AND ')) > 0;
-    }
-    
-    public function getHelpersBy($aParams = [])
-    {
-        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
-        $sSelectClause = "`th`.*";
-    	$sJoinClause = $sWhereClause = "";
-
-        switch($aParams['sample']) {
-            case 'id':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sWhereClause .= " AND `th`.`id`=:id";
-                break;
-            case 'name':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'name' => $aParams['name']
-                ];
-
-                $sWhereClause .= " AND `th`.`name`=:name";
-                break;
-            case 'ids':
-                $sWhereClause = " AND `th`.`id` IN (" . $this->implode_escape($aParams['ids']) . ")";
-                break;
-
-            case 'all_pairs':
-                $aMethod['name'] = 'getPairs';
-                $aMethod['params'][1] = 'id';
-                $aMethod['params'][2] = 'name';
-
-                if(isset($aParams['active'])) {
-                    $aMethod['params'][3] = [
-                        'active' => $aParams['active']
-                    ];
-
-                    $sWhereClause = " AND `th`.`active`=:active";
-                }
-                break;
-        }
-
-        $aMethod['params'][0] = "SELECT " . $sSelectClause . "
-            FROM `sys_agents_helpers` AS `th` " . $sJoinClause . "
-            WHERE 1" . $sWhereClause;
-
-        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
-    }
-
-    public function getAssistantsBy($aParams = [])
-    {
-        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
-        $sSelectClause = "`ta`.*";
-    	$sJoinClause = $sWhereClause = "";
-
-        switch($aParams['sample']) {
-            case 'id':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sWhereClause .= " AND `ta`.`id`=:id";
-                break;
-
-            case 'name':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'name' => $aParams['name']
-                ];
-
-                $sWhereClause .= " AND `ta`.`name`=:name";
-                break;
-
-            case 'ids':
-                $sWhereClause = " AND `ta`.`id` IN (" . $this->implode_escape($aParams['ids']) . ")";
-                break;
-
-            case 'all_pairs':
-                $aMethod['name'] = 'getPairs';
-                $aMethod['params'][1] = 'id';
-                $aMethod['params'][2] = 'name';
-                $aMethod['params'][3] = [];
-
-                if(isset($aParams['active'])) {
-                    $aMethod['params'][3]['active'] = $aParams['active'];
-
-                    $sWhereClause .= " AND `ta`.`active`=:active";
-                }
-
-                if(isset($aParams['hidden'])) {
-                    $aMethod['params'][3]['hidden'] = $aParams['hidden'];
-
-                    $sWhereClause .= " AND `ta`.`hidden`=:hidden";
-                }
-                break;
-        }
-
-        $aMethod['params'][0] = "SELECT " . $sSelectClause . "
-            FROM `sys_agents_assistants` AS `ta` " . $sJoinClause . "
-            WHERE 1" . $sWhereClause;
-
-        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
-    }
-    
-    public function updateAssistants($aSetClause, $aWhereClause)
-    {
-        if(empty($aSetClause) || empty($aWhereClause))
-            return false;
-
-        return (int)$this->query("UPDATE `sys_agents_assistants` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0;
-    }
-
-    public function getChatsBy($aParams = [])
-    {
-        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
-        $sSelectClause = "`tac`.*";
-    	$sJoinClause = $sWhereClause = "";
-
-        switch($aParams['sample']) {
-            case 'id':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sWhereClause .= " AND `tac`.`id`=:id";
-                break;
-
-            case 'name':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'name' => $aParams['name']
-                ];
-
-                $sWhereClause .= " AND `tac`.`name`=:name";
-                break;
-
-            case 'assistant_id':
-                $aMethod['params'][1] = [
-                    'assistant_id' => $aParams['assistant_id']
-                ];
-
-                $sWhereClause .= " AND `tac`.`assistant_id`=:assistant_id";
-                break;
-
-            case 'type':
-                $aMethod['params'][1] = [
-                    'type' => $aParams['type']
-                ];
-
-                $sWhereClause .= " AND `tac`.`type`=:type";
-                
-                if(isset($aParams['lifetime']) && (int)$aParams['lifetime'] > 0) {
-                    $aMethod['params'][1]['lifetime'] = (int)$aParams['lifetime'];
-
-                    $sWhereClause .= " AND (UNIX_TIMESTAMP() - `tac`.`added`) >= :lifetime";
-                }
-                break;
-        }
-
-        $aMethod['params'][0] = "SELECT " . $sSelectClause . "
-            FROM `sys_agents_assistants_chats` AS `tac` " . $sJoinClause . "
-            WHERE 1" . $sWhereClause;
-
-        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
-    }
-    
-    public function insertChat($aParamsSet)
-    {
-        if(empty($aParamsSet) || !is_array($aParamsSet))
-            return false;
-
-        return (int)$this->query("INSERT INTO `sys_agents_assistants_chats` SET " . $this->arrayToSQL($aParamsSet)) > 0 ? (int)$this->lastId() : false;
-    }
-
-    public function updateChats($aSetClause, $aWhereClause)
-    {
-        if(empty($aSetClause) || empty($aWhereClause))
-            return false;
-
-        return (int)$this->query("UPDATE `sys_agents_assistants_chats` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0;
-    }
-    
-    public function deleteChats($aParamsWhere)
-    {
-        if(empty($aParamsWhere))
-            return false;
-
-        return (int)$this->query("DELETE FROM `sys_agents_assistants_chats` WHERE " . $this->arrayToSQL($aParamsWhere, ' AND ')) > 0;
-    }
-    
-    public function getFilesBy($aParams = [])
-    {
-        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
-        $sSelectClause = "`taf`.*";
-    	$sJoinClause = $sWhereClause = "";
-
-        switch($aParams['sample']) {
-            case 'id':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sWhereClause .= " AND `taf`.`id`=:id";
-                break;
-
-            case 'assistant_id':
-                $aMethod['params'][1] = [
-                    'assistant_id' => $aParams['assistant_id']
-                ];
-
-                $sWhereClause .= " AND `taf`.`assistant_id`=:assistant_id";
-
-                if(!empty($aParams['name'])) {
-                    $aMethod['name'] = 'getRow';
-                    $aMethod['params'][1]['name'] = $aParams['name'];
-
-                    $sWhereClause .= " AND `taf`.`name`=:name";
-                }
-                break;                
-        }
-
-        $aMethod['params'][0] = "SELECT " . $sSelectClause . "
-            FROM `sys_agents_assistants_files` AS `taf` " . $sJoinClause . "
-            WHERE 1" . $sWhereClause;
-
-        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
-    }
-
-    public function insertFile($aParamsSet)
-    {
-        if(empty($aParamsSet) || !is_array($aParamsSet))
-            return false;
-
-        return (int)$this->query("INSERT INTO `sys_agents_assistants_files` SET " . $this->arrayToSQL($aParamsSet)) > 0 ? (int)$this->lastId() : false;
-    }
-
-    public function updateFiles($aSetClause, $aWhereClause)
-    {
-        if(empty($aSetClause) || empty($aWhereClause))
-            return false;
-
-        return (int)$this->query("UPDATE `sys_agents_assistants_files` SET " . $this->arrayToSQL($aSetClause) . " WHERE " . $this->arrayToSQL($aWhereClause)) > 0;
-    }
-
-    public function deleteFiles($aParamsWhere)
-    {
-        if(empty($aParamsWhere))
-            return false;
-
-        return (int)$this->query("DELETE FROM `sys_agents_assistants_files` WHERE " . $this->arrayToSQL($aParamsWhere, ' AND ')) > 0;
-    }
-
-    public function getVectorStores (): mixed
-    {
-        $sQuery = "SELECT `id`, `title` FROM `sys_agents_vector_store` WHERE `active` = 1 ORDER BY `title` ASC";
-        return $this->getPairs($sQuery, 'id', 'title'); 
-    }
-
-    public function getVectorStoreById (int $iId): mixed
-    {
-        $sQuery = "SELECT * FROM `sys_agents_vector_store` WHERE `id` = :id";
-        return $this->getRow($sQuery, ['id' => $iId]); 
-    }
-
-    public function insertVectorStore($aVectorStore)
-    {
-        if(empty($aVectorStore) || !is_array($aVectorStore))
-            return false;
-
-        return (int)$this->query("INSERT INTO `sys_agents_vector_store` SET " . $this->arrayToSQL($aVectorStore));
-    }
-
-    public function getVectorStoreDataNum (int $iVectorStoreId): int
-    {
-        $sQuery = "SELECT COUNT(*) FROM `sys_agents_vector_store_data` WHERE `vector_store_id` = :vector_store_id";
-        return (int)$this->getOne($sQuery, ['vector_store_id' => $iVectorStoreId]);
-    }
-
-    public function addVectorStoreData (int $iVectorStoreId, string $sType, string $sName, int $iFileSize, string $sMetadata, string $sSettings, string $sContent): bool
-    {
-        $sQuery = "INSERT INTO `sys_agents_vector_store_data` SET  
-            `vector_store_id` = :vector_store_id,
-            `type` = :type, 
-            `name` = :name, 
-            `size` = :size,
-            `metadata` = :metadata, 
-            `settings` = :settings, 
-            `content` = :content,
-            `added` = :ts
-        ";
-        return $this->query($sQuery, [
-            'vector_store_id' => $iVectorStoreId,
-            'type' => $sType, 
-            'name' => $sName, 
-            'size' => $iFileSize,
-            'metadata' => $sMetadata, 
-            'settings' => $sSettings, 
-            'content' => $sContent,
-            'ts' => time()]) > 0;
-    }
-
-    public function getVectorStoreDataById (int $iId): mixed
-    {
-        $sQuery = "SELECT * FROM `sys_agents_vector_store_data` WHERE `id` = :id";
-        return $this->getRow($sQuery, ['id' => $iId]);
-    }
-
-    static public function getVectorStorePendingData (int $iLimit = 1): mixed
-    {
-        $oDb = BxDolDb::getInstance();
-        $sQuery = "SELECT * FROM `sys_agents_vector_store_data` WHERE `status` = 'pending' ORDER BY `added` ASC LIMIT :limit";
-        return $oDb->getAll($sQuery, ['limit' => $iLimit]);
-    }
-
-    static public function updateVectorStoreDataStatus (int $iId, string $sStatus): mixed
-    {
-        $oDb = BxDolDb::getInstance();
-        $sQuery = "UPDATE `sys_agents_vector_store_data` SET `status` = :status WHERE `id` = :id";
-        return $oDb->query($sQuery, ['status' => $sStatus, 'id' => $iId]);
-    }
-
-    public function getAgentsWithAlert($bActiveOnly = true)
-    {
-        return $this->fromCache('sys_agents_with_alert', 'getAll', "SELECT * FROM `sys_agents_agents` WHERE `trigger` = 'alert' AND `active` = :active", ['active' => $bActiveOnly ? 1 : 0]);
-    }
-
-    public function getAgentsByProfileId($iProfileId, $bActiveOnly = true)
-    {
-        return $this->getAll("SELECT * FROM `sys_agents_agents` WHERE `trigger` = 'message' AND `profile_id` = :profile AND `active` = :active", ['profile' => $iProfileId, 'active' => $bActiveOnly ? 1 : 0]);
-    }
-
-    public function getAgentsByFormObject($sFormObject, $bActiveOnly = true)
-    {
-        return $this->fromMemory('sys_agents_with_form_' . $sFormObject, 'getAll', "SELECT * FROM `sys_agents_agents` WHERE `trigger` = 'form-input' AND `form_object` = :form_object AND `active` = :active", ['form_object' => $sFormObject, 'active' => $bActiveOnly ? 1 : 0]);
-    }
-
-    public function getAgentsBy($aParams = [])
-    {
-        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
-    	$sWhereClause = "";
-
-        switch($aParams['sample']) {
-            case 'id':
-            	$aMethod['name'] = 'getRow';
-            	$aMethod['params'][1] = [
-                    'id' => $aParams['id']
-                ];
-
-                $sWhereClause .= " AND `id`=:id";
-                break;
-
-            case 'all':
-                if(isset($aParams['active'])) {
-                    $aMethod['params'][3]['active'] = $aParams['active'];
-
-                    $sWhereClause .= " AND `active`=:active";
-                }
-                break;
-        }
-
-        $aMethod['params'][0] = "SELECT * 
-            FROM `sys_agents_agents`
-            WHERE 1" . $sWhereClause;
-
-        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
-    }
-
-    public function getAgentById(int $iId): mixed
-    {
-        return $this->getAgentsBy(['sample' => 'id', 'id' => $iId]);
-    }
-
-    public function getAgentByTriggerWebhookKey($sKey, $bActiveOnly = true)
-    {
-        return $this->getRow("SELECT * FROM `sys_agents_agents` WHERE `trigger` = 'scheduler' AND `webhook_key` = :key AND `active` = :active", ['key' => $sKey, 'active' => $bActiveOnly ? 1 : 0]);
-    }
-    
-    public function getAgentsByTriggerType($sTrigger, $bActiveOnly = true)
-    {
-        return $this->getAll("SELECT * FROM `sys_agents_agents` WHERE `trigger` = :trigger AND `active` = :active", ['trigger' => $sTrigger, 'active' => $bActiveOnly ? 1 : 0]);
-    }
-
-    public function updateAgentField($iId, $sField, $sValue)
-    {
-        return $this->query("UPDATE `sys_agents_agents` SET `$sField` = :value WHERE `id` = :id", ['value' => $sValue, 'id' => $iId]);
-    }
-
-    public function getToolById (int $iId): mixed
-    {
-        $sQuery = "SELECT * FROM `sys_agents_tools` WHERE `id` = :id";
-        return $this->getRow($sQuery, ['id' => $iId]); 
-    }
-
-    public function insertTool(array $aFields): int
-    {
-        if(empty($aFields) || !is_array($aFields))
-            return false;
-
-        return (int)$this->query("INSERT INTO `sys_agents_tools` SET " . $this->arrayToSQL($aFields));
-    }
-
-    public function getTools()
-    {
-        $sQuery = "SELECT `id`, `title` FROM `sys_agents_tools` WHERE `active` = 1 ORDER BY `title` ASC";
-        return $this->getPairs($sQuery, 'id', 'title'); 
-    }
-
-    public function wipeAgentChatHistory($aAgent) 
-    {
-        $sQuery = "DELETE FROM `sys_agents_chat_history` WHERE `thread_id` LIKE :val";
-        $iAffected = 0;
-        $iAffected += $this->query($sQuery, ['val' => $aAgent['trigger'] . ':' . $aAgent['id'] . ':%']);
-        $iAffected += $this->query($sQuery, ['val' => $aAgent['trigger'] . ':' . $aAgent['id']]);
-        return $iAffected;
-    }
-
-    /**
-     * Rename this agent's guest threads (`…:{sessionId}`) onto a profile id.
-     * If the member thread already has messages, drop the guest copy.
-     */
-    public function adoptGuestChatHistory($aAgent, $sSessionId, $iProfileId)
-    {
-        $sSessionId = (string)$sSessionId;
-        $iProfileId = (int)$iProfileId;
-        if ($sSessionId === '' || !$iProfileId || empty($aAgent['id']))
-            return 0;
-
-        $sPrefix = $aAgent['trigger'] . ':' . $aAgent['id'] . ':';
-        $sOldSuffix = ':' . $sSessionId;
-        $sNewSuffix = ':' . $iProfileId;
-        $iLen = strlen($sOldSuffix);
-        $aBindings = [
-            'p' => $sPrefix . '%',
-            'o' => $sOldSuffix,
-            'n' => $sNewSuffix,
-        ];
-
-        $this->query("
-            DELETE `g` FROM `sys_agents_chat_history` AS `g`
-            INNER JOIN `sys_agents_chat_history` AS `m`
-                ON `m`.`thread_id` = CONCAT(LEFT(`g`.`thread_id`, CHAR_LENGTH(`g`.`thread_id`) - $iLen), :n)
-            WHERE `g`.`thread_id` LIKE :p AND RIGHT(`g`.`thread_id`, $iLen) = :o
-              AND `m`.`messages` IS NOT NULL AND `m`.`messages` != '' AND `m`.`messages` != '[]'
-        ", $aBindings);
-
-        $this->query("
-            DELETE `m` FROM `sys_agents_chat_history` AS `m`
-            INNER JOIN `sys_agents_chat_history` AS `g`
-                ON `g`.`thread_id` LIKE :p AND RIGHT(`g`.`thread_id`, $iLen) = :o
-               AND `m`.`thread_id` = CONCAT(LEFT(`g`.`thread_id`, CHAR_LENGTH(`g`.`thread_id`) - $iLen), :n)
-            WHERE `m`.`messages` = '' OR `m`.`messages` = '[]' OR `m`.`messages` IS NULL
-        ", $aBindings);
-
-        $iAffected = (int)$this->query("
-            UPDATE `sys_agents_chat_history`
-            SET `thread_id` = CONCAT(LEFT(`thread_id`, CHAR_LENGTH(`thread_id`) - $iLen), :n)
-            WHERE `thread_id` LIKE :p AND RIGHT(`thread_id`, $iLen) = :o
-        ", $aBindings);
-
-        return $iAffected;
-    }
-
-    static public function getAlert($s) {
-        if (!$s)
-            return false;        
-        $a = explode(':', $s);
-        $oDb = BxDolDb::getInstance();
-        return $oDb->getRow("SELECT * FROM `sys_alerts_log` WHERE `unit` = :unit AND `action` = :action", [
-            'unit' => $a[0] ?? '',
-            'action' => $a[1] ?? '',
-        ]);
-    }
-
-    public function getAlerts()
-    {
-        $aValues = [];
-        $aAlerts = $this->getAll("SELECT `unit`, `action`, `counter_24h`, `counter_per_request` FROM `sys_alerts_log` ORDER BY `unit`, `action`");
-        foreach ($aAlerts as $a) {
-            $sKey = $a['unit'] . ':' . $a['action'];
-            $aValues[$sKey] = [
-                'key' => $sKey,
-                'unit' => $a['unit'],
-                'action' => $a['action'],
-                'name' => $a['unit'] . ' - ' . $a['action'],
-                'desc' => $this->getAlertDesc($a['unit'] . ':' . $a['action']),
-                'counter_24h' => $a['counter_24h'],
-                'counter_per_request' => $a['counter_per_request'],
-            ];
-        }
-        return $aValues;
-    }
-
-    public function getFormDisplay($sObject)
-    {
-        $aDisplays = $this->getColumn("SELECT `display_name` FROM `sys_form_displays` WHERE `object` = :object", ['object' => $sObject]);
-        return $aDisplays ? current($aDisplays) : null;
-    }
-
-    public function getFormObjects()
-    {
-        $aValues = [];
-        $aForms = $this->getAll("SELECT `f`.`object`, `f`.`title`, `f`.`module`, `m`.`title` AS `module_title` FROM `sys_objects_form` AS f INNER JOIN `sys_modules` as `m` ON `m`.`name` = `f`.`module` ORDER BY `f`.`module`, `f`.`object` ASC");
-        foreach ($aForms as $r) {
-            if ('system' != $r['module']) {
-                $oModule = BxDolModule::getInstance($r['module']);
-                if (!$oModule || !$oModule->isEnabled())
-                    continue;
-            }
-            
-            $sDisplay = $this->getFormDisplay($r['object']);
-            if (!$sDisplay) 
-                continue;
-            $oForm = BxDolForm::getObjectInstance($r['object'], $sDisplay);
-            $sFormId = $oForm->getId();
-            $aValues[$r['object']] = [
-                'form_id' => $sFormId,
-                'form_object' => $r['object'],
-                'module' => $r['module'],
-                'module_title' => $r['module_title'],
-                'form_title' => _t($r['title']),
-            ];
-        }
-        return $aValues;
-    }
-
-    static public function getAlertDesc($sAlert) 
-    {
-        if (!$sAlert)
-            return '';
-        $oDb = BxDolDb::getInstance();
-        [$sUnit, $sAction] = explode(':', $sAlert);
-        $sDesc = $oDb->getOne("SELECT `description` FROM `sys_alerts_desc` WHERE `unit` = :unit AND `action` = :action LIMIT 1", [
-            'unit' => $sUnit,
-            'action' => $sAction,
-        ]);
-        if (!$sDesc) {
-            $oDb->getOne("SELECT `description` FROM `sys_alerts_desc` WHERE `unit` LIKE '{%}' AND `action` = :action LIMIT 1", [
-                'action' => $sAction,
-            ]);
-        }
-        return $sDesc;
-    }
-}
-
-/** @} */

+ 70 - 0
inc/classes/BxDolAIToolChatArtifactSave.php

@@ -0,0 +1,70 @@
+<?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
+ * @{
+ */
+
+use NeuronAI\Tools\PropertyType;
+use NeuronAI\Tools\ToolProperty;
+
+class BxDolAIToolChatArtifactSave extends BxDolAITool
+{
+    public function __construct()
+    {
+        parent::__construct(
+            'chat_artifact_save',
+            'Save one collected field from this conversation (for example name, email, group_name). Call as soon as the visitor gives a value. Do not print saved values to the visitor.',
+        );
+    }
+
+    protected function properties(): array
+    {
+        return [
+            new ToolProperty(
+                name: 'field_name',
+                type: PropertyType::STRING,
+                description: 'Field key, e.g. name, email, group_name.',
+                required: true
+            ),
+            new ToolProperty(
+                name: 'field_value',
+                type: PropertyType::STRING,
+                description: 'Field value as given by the visitor.',
+                required: true
+            ),
+        ];
+    }
+
+    public function __invoke(string $sFieldName, string $sFieldValue): string
+    {
+        $sName = strtolower(trim((string)$sFieldName));
+        $sName = preg_replace('/[^a-z0-9_]/', '', $sName);
+        $sValue = trim((string)$sFieldValue);
+        if ($sName === '' || $sValue === '')
+            return 'ignored';
+
+        $iHistoryId = (int)BxDolAi::getInstance()->getCurrentChatHistoryId();
+        if ($iHistoryId <= 0)
+            return 'no_history';
+
+        BxDolDb::getInstance()->query("
+            INSERT INTO `sys_agents_chat_artifacts`
+                (`history_id`, `field_name`, `field_value`, `updated_at`)
+            VALUES
+                (:h, :n, :v, :t)
+            ON DUPLICATE KEY UPDATE
+                `field_value` = VALUES(`field_value`),
+                `updated_at` = VALUES(`updated_at`)
+        ", [
+            'h' => $iHistoryId,
+            'n' => $sName,
+            'v' => $sValue,
+            't' => time(),
+        ]);
+
+        return 'ok';
+    }
+}

+ 40 - 0
inc/classes/BxDolAIToolChatGoalReached.php

@@ -0,0 +1,40 @@
+<?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
+ * @{
+ */
+
+use NeuronAI\Tools\PropertyType;
+use NeuronAI\Tools\ToolProperty;
+
+class BxDolAIToolChatGoalReached extends BxDolAITool
+{
+    public function __construct()
+    {
+        parent::__construct(
+            'chat_goal_reached',
+            'Call once when the session goal is reached (next step given, link sent). Do not print tool output to the visitor. Do not call again in the same session.',
+        );
+    }
+
+    protected function properties(): array
+    {
+        return [
+            new ToolProperty(
+                name: 'summary',
+                type: PropertyType::STRING,
+                description: 'Short internal note: group type, size, readiness, which link was given.',
+                required: false
+            ),
+        ];
+    }
+
+    public function __invoke(string $sSummary = ''): string
+    {
+        $b = BxDolAi::getInstance()->emitConversationClosed('goal', trim((string)$sSummary));
+        return $b ? 'ok' : 'already_closed';
+    }
+}

+ 1 - 1
inc/classes/BxDolAIToolFactory.php

@@ -14,7 +14,7 @@ class BxDolAIToolFactory extends BxDolFactory
         if (isset($GLOBALS['bxDolClasses'][__CLASS__ . '_AiAgentTool_' . $iId]))
             return $GLOBALS['bxDolClasses'][__CLASS__ . '_AiAgentTool_' . $iId];
 
-        $a = BxDolAIQuery::getToolObject($iId);
+        $a = BxDolAiQuery::getToolObject($iId);
         if (!$a) {
             bx_log('sys_agents', "Tool with id {$iId} not found", BX_LOG_ERR);
             throw new Exception("Tool with id {$iId} not found");

+ 1 - 1
inc/classes/BxDolAIVectorStoreFactory.php

@@ -15,7 +15,7 @@ class BxDolAIVectorStoreFactory extends BxDolFactory
         if (isset($GLOBALS['bxDolClasses'][__CLASS__ . '_VectorStore_' . $iId]))
             return $GLOBALS['bxDolClasses'][__CLASS__ . '_VectorStore_' . $iId];
 
-        $a = BxDolAIQuery::getVectorStoreObject($iId);
+        $a = BxDolAiQuery::getVectorStoreObject($iId);
         if (!$a) {
             bx_log('sys_agents', "Vector store with id {$iId} not found", BX_LOG_ERR);
             throw new Exception("Vector store with id {$iId} not found");

+ 1533 - 0
inc/classes/BxDolAi.php

@@ -0,0 +1,1533 @@
+<?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
+ * @{
+ */
+
+define('BX_DOL_AI_ASSISTANT', 'assistant');
+define('BX_DOL_AI_AUTOMATOR_EVENT', 'event');
+define('BX_DOL_AI_AUTOMATOR_SCHEDULER', 'scheduler');
+define('BX_DOL_AI_AUTOMATOR_WEBHOOK', 'webhook');
+
+define('BX_DOL_AI_AUTOMATOR_STATUS_AUTO', 'auto');
+define('BX_DOL_AI_AUTOMATOR_STATUS_MANUAL', 'manual');
+define('BX_DOL_AI_AUTOMATOR_STATUS_READY', 'ready');
+
+class BxDolAi extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_oDb;
+    protected $_iProfileId;
+    
+    protected $_aExcludeAlertUnits;
+
+    protected $_bWriteLog;
+
+    protected $_aChatContext;
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_oDb = new BxDolAiQuery();
+
+        $this->_iProfileId = (int)getParam('sys_profile_bot'); 
+
+        $this->_aExcludeAlertUnits = [
+            'system', 'module_template_method_call'
+        ];
+
+        $this->_bWriteLog = true;
+
+        $this->_aChatContext = null;
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if (!isset($GLOBALS['bxDolClasses'][__CLASS__])) {
+            $GLOBALS['bxDolClasses'][__CLASS__] = BxDolDb::getInstance()->isTableExists('sys_agents_agents') ? new BxDolAi() : null;
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    public static function getAgentInstance(int $iId, array $aParams = []): NeuronAI\Agent\Agent
+    {
+        if (isset($GLOBALS['bxDolClasses'][__CLASS__ . '_Agent_' . $iId]))
+            return $GLOBALS['bxDolClasses'][__CLASS__ . '_Agent_' . $iId];
+
+        $a = BxDolAIQuery::getAgentObject($iId);
+        if (!$a) {
+            $s = "Agent with id {$iId} not found";
+            bx_log('sys_agents', $s, BX_LOG_ERR);
+            throw new Exception($s);
+        }
+        if (!$a['active']) {
+            $s = "Agent with id {$iId} isn't active";
+            bx_log('sys_agents', $s, BX_LOG_ERR);
+            throw new Exception($s);
+        }
+        if (!$a['prompt_system'] || !$a['model_id']) {
+            $s = "Agent with id {$iId} can't be used because it haven't prompt or AI model";
+            bx_log('sys_agents', $s, BX_LOG_ERR);
+            throw new Exception($s);
+        }
+
+        $o = BxDolAiAgent::make($a, $aParams);
+
+        if ((int)$a['tools_max_run'] > 0)
+            $o->toolMaxRuns($a['tools_max_run']);
+
+        if ($a['vector_store_id']) {
+            $aVectorStore = BxDolAIQuery::getVectorStoreObject($a['vector_store_id']);
+            if ($aVectorStore && $aVectorStore['embedding_provider_id']) {
+                $oEmbedder = self::getAiEmbeddingsProviderInstance($aVectorStore['embedding_provider_id']);
+                $o->setEmbeddingsProvider($oEmbedder);
+            }
+        }
+
+        $GLOBALS['bxDolClasses'][__CLASS__ . '_Agent_' . $iId] = $o;
+
+        return $o;
+    }
+
+    public static function getAiProviderInstance(int $iId):NeuronAI\Providers\AIProviderInterface
+    {
+        return BxDolAIModelFactory::getModelInstance($iId);
+    }   
+
+    public static function getAiEmbeddingsProviderInstance(int $iId):NeuronAI\RAG\Embeddings\EmbeddingsProviderInterface
+    {
+        return BxDolAIModelFactory::getModelInstance($iId);
+    }
+
+    public static function pruning() {}
+
+    public static function getDefaultApiKey()
+    {
+        return getParam('sys_agents_api_key');
+    }
+
+    public static function getDefaultModel()
+    {
+        return (int)getParam('sys_agents_model');
+    }
+
+    public function getProfileId()
+    {
+        return $this->_iProfileId;
+    }
+
+    public function getModels($aParams = [])
+    {
+        $aParamsDb = ['sample' => 'all_pairs'];
+        if(isset($aParams['active']))
+            $aParamsDb['active'] = $aParams['active'] === true ? 1 : 0;
+        if(isset($aParams['capabilities']))
+            $aParamsDb['capabilities'] = $aParams['capabilities'];
+
+        return $this->_oDb->getModelsBy($aParamsDb);
+    }
+
+    public function getModel($iId)
+    {
+        $aModel = $this->_oDb->getModelsBy(['sample' => 'id', 'id' => $iId]);
+        if(!empty($aModel['params']))
+            $aModel['params'] = json_decode($aModel['params'], true);
+
+        return $aModel;
+    }
+
+    public function callAgent($sType, $aAgent, $mixedParams = [])
+    {
+        if ($mixedParams)
+            $sParams = is_string($mixedParams) ? $mixedParams : json_encode($mixedParams);
+        else
+            $sParams = 'START';
+
+        // update sample data
+        $a = ['webhook' => 'webhook_sample'];
+        if (isset($a[$sType]) && empty($aAgent[$a[$sType]])) {
+            $this->_oDb->updateAgentField($aAgent['id'], $a[$sType], $sParams);
+        }
+
+        // set additional params
+        $aParams = [];
+        if ('message' == $sType || 'form-input' == $sType || 'alert' == $sType) {
+            $aParams = ['chat_history_subindex' => (int)($mixedParams['sender_profile_id'] ?? 0)];
+        }
+
+        if ('message' == $sType && is_array($mixedParams) && isset($mixedParams['message_text'])) {
+            $mixedParams['message_text'] = $this->applyChatInputLimit((string)$mixedParams['message_text'], $aAgent);
+            $sParams = json_encode($mixedParams);
+        }
+        else if (is_string($mixedParams) && in_array($sType, ['manual', 'message'], true)) {
+            $sParams = $this->applyChatInputLimit($sParams, $aAgent);
+        }
+
+        if (in_array($sType, ['manual', 'message'], true) && $this->isChatTurnLimitReached($aAgent, $this->getChatUserTurnCount($aAgent['id'], $aParams))) {
+            $this->emitConversationClosed('limit', '', $aAgent, $aParams);
+            return $this->getChatLimitMessage($aAgent);
+        }
+
+        if (in_array($sType, ['manual', 'message'], true) && $this->isChatSessionRateLimited($aAgent, $aParams))
+            throw new Exception($this->getChatSessionRateLimitError());
+
+        $this->_aChatContext = ['agent' => $aAgent, 'params' => $aParams];
+
+        // call agent
+        $mixed = '';
+        try {                        
+            $o = self::getAgentInstance($aAgent['id'], $aParams);
+            if (!$o)
+                return false;
+
+            $oMessage = $o->chat(new NeuronAI\Chat\Messages\UserMessage($sParams))->getMessage();
+
+            $mixed = $oMessage->getContent();
+
+        } catch (Exception $exception) {            
+            bx_log('sys_agents', "Exception in '{$aAgent['name']}' agent: " . $exception->getMessage() . " INPUT:" . $sParams, BX_LOG_ERR);
+            $mixed = _t('_sys_agents_exception');
+        }
+
+        return $mixed;
+    }
+
+    /**
+     * Whether the current (or given) member may interact with the agent
+     * via chat, messenger, or form-input. Empty acl_levels means Nobody.
+     * Site admins / Studio operators always may, so they can debug.
+     */
+    public function canInteract($aAgent, $iProfileId = false, $bAllowOperators = true)
+    {
+        if (!is_array($aAgent) || empty($aAgent['id']))
+            return false;
+
+        if ($bAllowOperators && isAdmin())
+            return true;
+
+        $iLevels = (int)($aAgent['acl_levels'] ?? 0);
+        if (!$iLevels)
+            return false;
+
+        return (bool)BxDolAcl::getInstance()->isMemberLevelInSet($iLevels, $iProfileId);
+    }
+
+    /**
+     * Direct HTTP chat (`sys-ai-chat`). Operators may talk to any trigger;
+     * everyone else only `manual` and `message` agents they can interact with.
+     */
+    public function canChatDirectly($aAgent, $iProfileId = false)
+    {
+        if (!$this->canInteract($aAgent, $iProfileId))
+            return false;
+
+        if (isAdmin())
+            return true;
+
+        return in_array($aAgent['trigger'] ?? '', ['manual', 'message'], true);
+    }
+
+    /**
+     * Runner limits from the agent row. 0 = no cap. Never put these in the prompt.
+     */
+    public function applyChatInputLimit($sPrompt, $aAgent)
+    {
+        $sPrompt = (string)$sPrompt;
+        $iMax = (int)($aAgent['max_input_chars'] ?? 0);
+        if ($iMax <= 0)
+            return $sPrompt;
+
+        if (function_exists('mb_strlen') && function_exists('mb_substr')) {
+            if (mb_strlen($sPrompt, 'UTF-8') > $iMax)
+                return mb_substr($sPrompt, 0, $iMax, 'UTF-8');
+            return $sPrompt;
+        }
+
+        return strlen($sPrompt) > $iMax ? substr($sPrompt, 0, $iMax) : $sPrompt;
+    }
+
+    public function getChatLimitMessage($aAgent)
+    {
+        $s = trim((string)($aAgent['limit_message'] ?? ''));
+        return $s !== '' ? $s : 'This chat has reached its limit.';
+    }
+
+    public function getChatSessionRateLimitError()
+    {
+        $s = _t('_sys_agents_err_session_rate_limit');
+        return ($s !== '' && $s !== '_sys_agents_err_session_rate_limit')
+            ? $s
+            : 'Too many chat sessions from this IP. Try again later.';
+    }
+
+    public function countChatUserTurns($aUiMessages)
+    {
+        $i = 0;
+        if (!is_array($aUiMessages))
+            return 0;
+        foreach ($aUiMessages as $aMessage) {
+            if (is_array($aMessage) && ($aMessage['role'] ?? '') === 'user')
+                $i++;
+        }
+        return $i;
+    }
+
+    public function isChatTurnLimitReached($aAgent, $iTurns, $iRequestUserTurns = 0)
+    {
+        $iMax = (int)($aAgent['max_turns'] ?? 0);
+        if ($iMax <= 0)
+            return false;
+        if ((int)$iTurns >= $iMax)
+            return true;
+        // Client transcript includes the message being submitted.
+        return (int)$iRequestUserTurns > $iMax;
+    }
+
+    public function getChatUserTurnCount($iAgentId, $aParams = [])
+    {
+        if (!isset($aParams['chat_history_subindex']))
+            $aParams = array_merge($this->resolveChatHistoryParams($iAgentId), $aParams);
+
+        try {
+            $o = self::getAgentInstance((int)$iAgentId, $aParams);
+        } catch (Throwable $oException) {
+            return 0;
+        }
+
+        $i = 0;
+        foreach ($o->getChatHistory()->getMessages() as $oMessage) {
+            if ($oMessage instanceof NeuronAI\Chat\Messages\ToolCallMessage
+                || $oMessage instanceof NeuronAI\Chat\Messages\ToolResultMessage
+            ) {
+                continue;
+            }
+            if ($oMessage instanceof NeuronAI\Chat\Messages\UserMessage)
+                $i++;
+        }
+        return $i;
+    }
+
+    /**
+     * New chat threads per IP for this agent. 0 on the agent = no cap.
+     * Continuation of an existing thread is not a new session.
+     */
+    public function isChatSessionRateLimited($aAgent, $aParams = [])
+    {
+        $iHour = (int)($aAgent['max_sessions_per_hour'] ?? 0);
+        $iDay = (int)($aAgent['max_sessions_per_day'] ?? 0);
+        if ($iHour <= 0 && $iDay <= 0)
+            return false;
+
+        if (!isset($aParams['chat_history_subindex']))
+            $aParams = array_merge($this->resolveChatHistoryParams((int)($aAgent['id'] ?? 0)), $aParams);
+
+        $sThreadId = self::chatHistoryThreadId($aAgent, $aParams);
+        if (!$this->isNewChatSession($sThreadId))
+            return false;
+
+        $sIp = $this->getVisitorIpHash();
+        if ($sIp === '0' || $sIp === '')
+            return false;
+
+        $sPrefix = ($aAgent['trigger'] ?? '') . ':' . ($aAgent['id'] ?? '');
+        if ($sPrefix === ':')
+            return false;
+
+        if ($iHour > 0 && $this->countChatSessionsByIp($sIp, $sPrefix, 3600) >= $iHour)
+            return true;
+        if ($iDay > 0 && $this->countChatSessionsByIp($sIp, $sPrefix, 86400) >= $iDay)
+            return true;
+
+        return false;
+    }
+
+    protected function getVisitorIpHash()
+    {
+        if (!function_exists('getVisitorIP') || !function_exists('bx_get_ip_hash'))
+            return '0';
+        return (string)bx_get_ip_hash(getVisitorIP());
+    }
+
+    protected function isNewChatSession($sThreadId)
+    {
+        $sThreadId = (string)$sThreadId;
+        if ($sThreadId === '')
+            return true;
+
+        $aRow = $this->_oDb->getRow("SELECT `ip`, `messages` FROM `sys_agents_chat_history` WHERE `thread_id` = :t", [
+            't' => $sThreadId,
+        ]);
+        if (!$aRow)
+            return true;
+
+        $iIp = (int)($aRow['ip'] ?? 0);
+        $sMessages = trim((string)($aRow['messages'] ?? ''));
+        return $iIp <= 0 || $sMessages === '' || $sMessages === '[]';
+    }
+
+    protected function countChatSessionsByIp($sIp, $sPrefix, $iWindowSec)
+    {
+        $sSince = date('Y-m-d H:i:s', time() - (int)$iWindowSec);
+        return (int)$this->_oDb->getOne("
+            SELECT COUNT(*) FROM `sys_agents_chat_history`
+            WHERE `ip` = :ip
+              AND (`thread_id` = :prefix OR `thread_id` LIKE :prefix_like)
+              AND `created_at` >= :since
+        ", [
+            'ip' => $sIp,
+            'prefix' => $sPrefix,
+            'prefix_like' => $sPrefix . ':%',
+            'since' => $sSince,
+        ]);
+    }
+
+    public function extractChatPromptFromRequest($aData)
+    {
+        if (!is_array($aData))
+            return '';
+
+        if (!empty($aData['prompt']) && is_string($aData['prompt']))
+            return trim($aData['prompt']);
+
+        if (empty($aData['messages']) || !is_array($aData['messages']))
+            return '';
+
+        for ($i = count($aData['messages']) - 1; $i >= 0; $i--) {
+            $aMessage = $aData['messages'][$i];
+            if (!is_array($aMessage))
+                continue;
+
+            if (($aMessage['role'] ?? '') !== 'user')
+                continue;
+
+            if (isset($aMessage['content']) && is_string($aMessage['content']))
+                return trim($aMessage['content']);
+
+            if (!empty($aMessage['parts']) && is_array($aMessage['parts'])) {
+                $aText = [];
+                foreach ($aMessage['parts'] as $aPart) {
+                    if (is_array($aPart) && ($aPart['type'] ?? '') === 'text' && isset($aPart['content']))
+                        $aText[] = $aPart['content'];
+                }
+                $s = trim(implode("\n", $aText));
+                if ($s !== '')
+                    return $s;
+            }
+        }
+
+        return '';
+    }
+
+    /**
+     * Guests: UNA session id. Members: profile id.
+     * Guest chats remember agent ids in session so login can adopt those threads.
+     */
+    public function resolveChatHistoryParams($iAgentId = 0)
+    {
+        $iAgentId = (int)$iAgentId;
+        $iProfileId = (int)bx_get_logged_profile_id();
+        $oSession = BxDolSession::getInstance();
+        $sSessionKey = 'ai_chat_agent_ids';
+
+        if ($iProfileId) {
+            $aIds = $oSession->getValue($sSessionKey);
+            if (is_array($aIds) && $aIds) {
+                $sSessionId = (string)$oSession->getId();
+                if ($iAgentId)
+                    $aIds[] = $iAgentId;
+                foreach (array_unique(array_map('intval', $aIds)) as $iStoredAgentId) {
+                    if (!$iStoredAgentId || $sSessionId === '')
+                        continue;
+                    $aAgent = BxDolAIQuery::getAgentObject($iStoredAgentId);
+                    if ($aAgent)
+                        $this->_oDb->adoptGuestChatHistory($aAgent, $sSessionId, $iProfileId);
+                }
+                $oSession->unsetValue($sSessionKey);
+            }
+
+            return [
+                'sender_profile_id' => $iProfileId,
+                'chat_history_subindex' => (string)$iProfileId,
+            ];
+        }
+
+        if ($iAgentId) {
+            $aIds = $oSession->getValue($sSessionKey);
+            $aIds = array_map('intval', is_array($aIds) ? $aIds : []);
+            if (!in_array($iAgentId, $aIds, true)) {
+                $aIds[] = $iAgentId;
+                $oSession->setValue($sSessionKey, $aIds);
+            }
+        }
+
+        return [
+            'sender_profile_id' => 0,
+            'chat_history_subindex' => (string)$oSession->getId(),
+        ];
+    }
+
+    /**
+     * `{trigger}:{agentId}:{contextPid}:{userSubindex}`.
+     * Context is omitted when 0 so existing site-wide threads still load.
+     * User suffix (session id or profile id) stays last so adopt can replace it.
+     */
+    public static function chatHistoryThreadId($aAgent, $aParams = [])
+    {
+        $s = ($aAgent['trigger'] ?? '') . ':' . ($aAgent['id'] ?? '');
+        $iContextPid = (int)($aParams['chat_history_context_pid'] ?? 0);
+        if ($iContextPid > 0)
+            $s .= ':' . $iContextPid;
+        if (isset($aParams['chat_history_subindex']) && $aParams['chat_history_subindex'] !== '')
+            $s .= ':' . (string)$aParams['chat_history_subindex'];
+        return $s;
+    }
+
+    public function getChatHistoryRowId($aAgent, $aParams = [])
+    {
+        $sThreadId = self::chatHistoryThreadId($aAgent, $aParams);
+        if ($sThreadId === '' || $sThreadId === ':')
+            return 0;
+        return (int)$this->_oDb->getOne("SELECT `id` FROM `sys_agents_chat_history` WHERE `thread_id` = :t", [
+            't' => $sThreadId,
+        ]);
+    }
+
+    public function getCurrentChatHistoryId()
+    {
+        if (!is_array($this->_aChatContext))
+            return 0;
+
+        $aAgent = $this->_aChatContext['agent'] ?? null;
+        $aParams = $this->_aChatContext['params'] ?? [];
+        if (!is_array($aAgent))
+            return 0;
+
+        return $this->getChatHistoryRowId($aAgent, is_array($aParams) ? $aParams : []);
+    }
+
+    public function emitConversationClosed($sReason, $sSummary = '', $aAgent = null, $aParams = null)
+    {
+        if (!is_array($aAgent) && is_array($this->_aChatContext))
+            $aAgent = $this->_aChatContext['agent'] ?? null;
+        if (!is_array($aParams) && is_array($this->_aChatContext))
+            $aParams = $this->_aChatContext['params'] ?? [];
+        if (!is_array($aAgent) || empty($aAgent['id']))
+            return false;
+
+        $iHistoryId = $this->getChatHistoryRowId($aAgent, is_array($aParams) ? $aParams : []);
+        if ($iHistoryId <= 0)
+            return false;
+
+        if ($this->_oDb->isFieldExists('sys_agents_chat_history', 'closed_reason')) {
+            $sClosed = (string)$this->_oDb->getOne(
+                "SELECT `closed_reason` FROM `sys_agents_chat_history` WHERE `id` = :id",
+                ['id' => $iHistoryId]
+            );
+            if ($sClosed !== '')
+                return false;
+        }
+
+        bx_alert('system', 'agent_conversation_closed', $iHistoryId, 0, [
+            'agent_id' => (int)$aAgent['id'],
+            'summary' => (string)$sSummary,
+            'reason' => (string)$sReason,
+        ]);
+        return true;
+    }
+
+    /**
+     * @return array{thread_id:string,chat_history_context_pid:int,chat_history_subindex:string}|false
+     */
+    public static function parseChatHistoryThreadId($aAgent, $sThreadId)
+    {
+        $sThreadId = (string)$sThreadId;
+        $sBase = ($aAgent['trigger'] ?? '') . ':' . ($aAgent['id'] ?? '');
+        if ($sBase === ':' || $sThreadId === '')
+            return false;
+        if ($sThreadId !== $sBase && strpos($sThreadId, $sBase . ':') !== 0)
+            return false;
+
+        $sRest = $sThreadId === $sBase ? '' : substr($sThreadId, strlen($sBase) + 1);
+        $aParts = $sRest === '' ? [] : explode(':', $sRest);
+        $iContext = 0;
+        $sSub = '';
+        if (count($aParts) >= 2) {
+            $iContext = (int)$aParts[0];
+            $sSub = implode(':', array_slice($aParts, 1));
+        } elseif (count($aParts) === 1) {
+            $sSub = $aParts[0];
+        }
+
+        return [
+            'thread_id' => $sThreadId,
+            'chat_history_context_pid' => $iContext,
+            'chat_history_subindex' => $sSub,
+        ];
+    }
+
+    public function isOwnAgentChatThread($aAgent, $sThreadId)
+    {
+        $aParsed = self::parseChatHistoryThreadId($aAgent, $sThreadId);
+        if (!$aParsed)
+            return false;
+
+        $aMine = $this->resolveChatHistoryParams((int)$aAgent['id']);
+        $sMine = (string)($aMine['chat_history_subindex'] ?? '');
+        return $sMine !== '' && $aParsed['chat_history_subindex'] === $sMine;
+    }
+
+    public function storedChatJsonToUiMessages($sJson)
+    {
+        $aStored = json_decode((string)$sJson, true);
+        if (!is_array($aStored) || $aStored === [])
+            return [];
+
+        $aResult = [];
+        $i = 0;
+        foreach ($aStored as $aMessage) {
+            if (!is_array($aMessage))
+                continue;
+
+            $sType = (string)($aMessage['type'] ?? '');
+            if ($sType === 'tool_call' || $sType === 'tool_call_result')
+                continue;
+
+            $sRole = (string)($aMessage['role'] ?? '');
+            if ($sRole === 'model')
+                $sRole = 'assistant';
+            if ($sRole !== 'user' && $sRole !== 'assistant')
+                continue;
+
+            $aParts = [];
+            $mixedContent = $aMessage['content'] ?? '';
+            if (is_string($mixedContent) && $mixedContent !== '') {
+                $aParts[] = ['type' => 'text', 'content' => $mixedContent];
+            } elseif (is_array($mixedContent)) {
+                foreach ($mixedContent as $aBlock) {
+                    if (!is_array($aBlock))
+                        continue;
+                    $sBlockType = (string)($aBlock['type'] ?? '');
+                    $sBlockText = (string)($aBlock['content'] ?? '');
+                    if ($sBlockText === '')
+                        continue;
+                    if ($sBlockType === 'text' || $sBlockType === '')
+                        $aParts[] = ['type' => 'text', 'content' => $sBlockText];
+                }
+            }
+            $sId = '';
+            if (!empty($aMessage['__id']) && is_string($aMessage['__id']))
+                $sId = $aMessage['__id'];
+            elseif (!empty($aMessage['metadata']['__id']) && is_string($aMessage['metadata']['__id']))
+                $sId = $aMessage['metadata']['__id'];
+
+            $mixedActions = $aMessage['actions'] ?? ($aMessage['metadata']['actions'] ?? null);
+            $aUi = $this->finalizeUiChatMessage($sId !== '' ? $sId : ('msg_' . $i), $sRole, $aParts, $mixedActions);
+            if (!$aUi)
+                continue;
+
+            $aResult[] = $aUi;
+            $i++;
+        }
+
+        return $aResult;
+    }
+
+    public function getChatHistoryUiMessagesByThread($aAgent, $sThreadId)
+    {
+        if (!self::parseChatHistoryThreadId($aAgent, $sThreadId))
+            return false;
+
+        $sJson = $this->_oDb->getOne("SELECT `messages` FROM `sys_agents_chat_history` WHERE `thread_id` = :t", [
+            't' => (string)$sThreadId,
+        ]);
+
+        return $this->storedChatJsonToUiMessages($sJson);
+    }
+
+    public function getChatHistoryArtifactsByThread($aAgent, $sThreadId)
+    {
+        if (!self::parseChatHistoryThreadId($aAgent, $sThreadId))
+            return [];
+
+        return $this->_oDb->getChatArtifactsByThreadId((string)$sThreadId);
+    }
+
+    public function listAgentChatThreads($aAgent)
+    {
+        $aMineParams = $this->resolveChatHistoryParams((int)$aAgent['id']);
+        $sMySub = (string)($aMineParams['chat_history_subindex'] ?? '');
+        $sMyDefault = self::chatHistoryThreadId($aAgent, $aMineParams);
+
+        $aOut = [];
+        $bHaveMineDefault = false;
+        $aRows = $this->_oDb->getAgentChatHistoryRows($aAgent);
+        if (!is_array($aRows))
+            $aRows = [];
+
+        $aHistoryIds = [];
+        foreach ($aRows as $aRow)
+            $aHistoryIds[] = (int)($aRow['id'] ?? 0);
+        $aArtifactsMap = $this->_oDb->getChatArtifactsForAgent($aAgent);
+        if (!$aArtifactsMap && $aHistoryIds)
+            $aArtifactsMap = $this->_oDb->getChatArtifactsByHistoryIds($aHistoryIds);
+
+        foreach ($aRows as $aRow) {
+            $sThreadId = (string)($aRow['thread_id'] ?? '');
+            $aParsed = self::parseChatHistoryThreadId($aAgent, $sThreadId);
+            if (!$aParsed)
+                continue;
+
+            $aUi = $this->storedChatJsonToUiMessages($aRow['messages'] ?? '');
+            if (!$aUi && $sThreadId !== $sMyDefault)
+                continue;
+
+            $bMine = ($sMySub !== '' && $aParsed['chat_history_subindex'] === $sMySub);
+            if ($sThreadId === $sMyDefault)
+                $bHaveMineDefault = true;
+
+            $iHistoryId = (int)($aRow['id'] ?? 0);
+            $aArtifacts = ($iHistoryId && !empty($aArtifactsMap[$iHistoryId])) ? $aArtifactsMap[$iHistoryId] : [];
+            $aOut[] = $this->formatAgentChatThread($aParsed, $aRow, $aUi, $bMine, $aArtifacts);
+        }
+
+        if (!$bHaveMineDefault && $sMyDefault !== '') {
+            $aParsedMine = self::parseChatHistoryThreadId($aAgent, $sMyDefault);
+            if ($aParsedMine)
+                $aOut[] = $this->formatAgentChatThread($aParsedMine, [], [], true);
+        }
+
+        usort($aOut, function ($a, $b) {
+            return strcmp((string)$b['updated_sort'], (string)$a['updated_sort']);
+        });
+
+        return $aOut;
+    }
+
+    protected function formatAgentChatThread($aParsed, $aRow, $aUi, $bMine, $aArtifacts = [])
+    {
+        $iContext = (int)$aParsed['chat_history_context_pid'];
+        $sSub = (string)$aParsed['chat_history_subindex'];
+        $sContextName = '';
+        if ($iContext > 0) {
+            $oContext = BxDolProfile::getInstance($iContext);
+            $sContextName = $oContext ? $oContext->getDisplayName() : ('#' . $iContext);
+        }
+
+        $sTitle = _t('_sys_agents_agents_txt_guest_chat');
+        $oProfile = (ctype_digit($sSub) && (int)$sSub > 0) ? BxDolProfile::getInstance((int)$sSub) : false;
+        if ($oProfile)
+            $sTitle = $oProfile->getDisplayName();
+        if ($sContextName !== '')
+            $sTitle .= ' · ' . $sContextName;
+
+        $sReason = trim((string)($aRow['closed_reason'] ?? ''));
+        $sStatus = $sReason !== '' ? 'closed' : 'opened';
+        $sStatusLabel = $sStatus;
+        if ($sStatus === 'closed' && $sReason !== '')
+            $sStatusLabel .= ' (' . $sReason . ')';
+
+        $aArtifacts = is_array($aArtifacts) ? $aArtifacts : [];
+        $sCreated = $this->formatAgentChatThreadTime($aRow['created_at'] ?? '');
+        $sUpdated = $this->formatAgentChatThreadTime($aRow['updated_at'] ?? '');
+        $sUpdatedRaw = (string)($aRow['updated_at'] ?? '');
+
+        $sMeta = 'status: ' . $sStatusLabel;
+        $sDates = '';
+        if ($sCreated !== '' && $sUpdated !== '')
+            $sDates = $sCreated . ' - ' . $sUpdated;
+        else if ($sCreated !== '')
+            $sDates = $sCreated;
+        else if ($sUpdated !== '')
+            $sDates = $sUpdated;
+        if ($sDates !== '')
+            $sMeta .= "\n" . $sDates;
+
+        return [
+            'thread_id' => $aParsed['thread_id'],
+            'writable' => $bMine ? 1 : 0,
+            'context_pid' => $iContext,
+            'title' => $sTitle,
+            'meta' => $sMeta,
+            'status' => $sStatus,
+            'closed_reason' => $sReason,
+            'created_at' => $sCreated,
+            'updated_at' => $sUpdated,
+            'artifacts' => is_array($aArtifacts) ? $aArtifacts : [],
+            'updated_sort' => $sUpdatedRaw,
+            'class_mine' => $bMine ? ' bx-agents-popup-chat-thread-mine' : '',
+        ];
+    }
+
+    protected function formatAgentChatThreadTime($mixed): string
+    {
+        $s = trim((string)$mixed);
+        if ($s === '' || $s === '0')
+            return '';
+        $i = ctype_digit($s) ? (int)$s : (int)strtotime($s);
+        if ($i <= 86400)
+            return '';
+        return date('d.m.Y H:i', $i);
+    }
+
+    /**
+     * Context from chat HTTP (`?context=`). Missing/0 = site-wide. Invalid/denied = false (403).
+     * Posting in a group is not the same as viewing it; chat requires post by default.
+     * @return int|false
+     */
+    public function resolveChatHistoryContextPid($bRequirePost = true)
+    {
+        $mixed = bx_get('context');
+        if ($mixed === false)
+            return 0;
+
+        $iPid = (int)$mixed;
+        if ($iPid <= 0)
+            return 0;
+
+        $oProfile = BxDolProfile::getInstance($iPid);
+        if (!$oProfile || !bx_srv('system', 'is_module_context', [$oProfile->getModule()]))
+            return false;
+
+        if (!$this->isAllowedContextChat($oProfile, $bRequirePost))
+            return false;
+
+        return $iPid;
+    }
+
+    /**
+     * Context of the current page (AI agent block).
+     * `bx_get_page_info()` reads the URL; App page JSON merges `i`/`id` into $_GET instead.
+     * @param bool $bRequirePost also require posting into the context (membership / post ACL)
+     */
+    public function resolveChatHistoryContextPidFromPage($bRequirePost = false)
+    {
+        $aInfo = bx_get_page_info();
+        if ($aInfo && !empty($aInfo['context_profile_id']))
+            return $this->filterContextPid((int)$aInfo['context_profile_id'], $bRequirePost);
+
+        $sUri = bx_get('i');
+        $iContentId = (int)bx_process_input(bx_get('id'), BX_DATA_INT);
+        if ($sUri && $iContentId > 0) {
+            $oPage = BxDolPage::getObjectInstanceByURI($sUri);
+            $sModule = $oPage ? (string)$oPage->getModule() : '';
+            if ($sModule !== '' && bx_srv('system', 'is_module_context', [$sModule])) {
+                $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $sModule);
+                if ($oProfile && ($iPid = $this->filterContextPid((int)$oProfile->id(), $bRequirePost)))
+                    return $iPid;
+            }
+        }
+
+        $iPid = (int)bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if ($iPid > 0 && ($iPid = $this->filterContextPid($iPid, $bRequirePost)))
+            return $iPid;
+
+        return 0;
+    }
+
+    public function filterContextPid($iPid, $bRequirePost = false)
+    {
+        $iPid = (int)$iPid;
+        if ($iPid <= 0)
+            return 0;
+
+        $oProfile = BxDolProfile::getInstance($iPid);
+        if (!$oProfile || !bx_srv('system', 'is_module_context', [$oProfile->getModule()]))
+            return 0;
+
+        if (!$this->isAllowedContextChat($oProfile, $bRequirePost))
+            return 0;
+
+        return $iPid;
+    }
+
+    protected function isAllowedContextChat($oProfile, $bRequirePost = false)
+    {
+        if ($oProfile->checkAllowedProfileView() !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        if ($bRequirePost && $oProfile->checkAllowedPostInProfile() !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        return true;
+    }
+
+    /**
+     * Transcript for TanStack `useChat` hydrate / `initialMessages`.
+     * Loads the same NeuronAI chat history the agent uses when streaming.
+     *
+     * @param bool $bAppendLimitMessage when the session is at max_turns, append limit_message for the UI
+     */
+    public function getChatHistoryUiMessages($iAgentId, $aParams = [], $bAppendLimitMessage = true)
+    {
+        if (!isset($aParams['chat_history_subindex']))
+            $aParams = array_merge($this->resolveChatHistoryParams($iAgentId), $aParams);
+
+        $aAgent = BxDolAIQuery::getAgentObject((int)$iAgentId);
+        if (!$aAgent || empty($aAgent['chat_history_context']))
+            return [];
+
+        $sThreadId = self::chatHistoryThreadId($aAgent, $aParams);
+        $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 [];
+        }
+
+        $aResult = $this->neuronMessagesToUiMessages($o->getChatHistory()->getMessages());
+        if ($bAppendLimitMessage && $this->isChatTurnLimitReached($aAgent, $this->countChatUserTurns($aResult))) {
+            $aResult[] = [
+                'id' => 'agent_limit_message',
+                'role' => 'assistant',
+                'parts' => [['type' => 'text', 'content' => $this->getChatLimitMessage($aAgent)]],
+            ];
+        }
+
+        return $aResult;
+    }
+
+    /**
+     * @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];
+            }
+
+            $sId = $oMessage->getMetadata('__id');
+            $aUi = $this->finalizeUiChatMessage(
+                is_string($sId) && $sId !== '' ? $sId : ('msg_' . $i),
+                $sRole,
+                $aParts,
+                $sRole === 'assistant' ? $oMessage->getMetadata('actions') : null
+            );
+            if (!$aUi)
+                continue;
+
+            $aResult[] = $aUi;
+            $i++;
+        }
+
+        return $aResult;
+    }
+
+    /**
+     * Split assistant JSON {content, actions} into UI text + actions.
+     * User messages pass through unchanged.
+     *
+     * @param array<int, array<string, mixed>> $aParts
+     * @return array<string, mixed>|null
+     */
+    protected function finalizeUiChatMessage($sId, $sRole, $aParts, $mixedActions = null)
+    {
+        $aActions = $sRole === 'assistant' ? $this->sanitizeChatActions($mixedActions) : [];
+        if ($sRole === 'assistant') {
+            $sText = '';
+            foreach ($aParts as $aPart) {
+                if (($aPart['type'] ?? '') === 'text')
+                    $sText .= (string)($aPart['content'] ?? '');
+            }
+            $aParsed = $this->parseAssistantChatPayload($sText);
+            if ($aParsed) {
+                $aKept = [];
+                foreach ($aParts as $aPart) {
+                    if (($aPart['type'] ?? '') === 'text')
+                continue;
+                    $aKept[] = $aPart;
+                }
+                if ($aParsed['content'] !== '')
+                    $aKept[] = ['type' => 'text', 'content' => $aParsed['content']];
+                $aParts = $aKept;
+                if (!$aActions)
+                    $aActions = $aParsed['actions'];
+            }
+        }
+
+        if (!$aParts && !$aActions)
+            return null;
+
+        $aOut = [
+            'id' => $sId,
+                'role' => $sRole,
+                'parts' => $aParts,
+            ];
+        if ($sRole === 'assistant')
+            $aOut['actions'] = $aActions;
+
+        return $aOut;
+    }
+
+    /**
+     * @return array{content: string, actions: array}|null
+     */
+    protected function parseAssistantChatPayload($sText)
+    {
+        $s = trim((string)$sText);
+        if ($s === '')
+            return null;
+
+        if (preg_match('/^```(?:json)?\s*(\{.*\})\s*```$/s', $s, $aM))
+            $s = trim($aM[1]);
+
+        if ($s === '' || $s[0] !== '{') {
+            $iStart = strpos($s, '{');
+            $iEnd = strrpos($s, '}');
+            if ($iStart === false || $iEnd === false || $iEnd <= $iStart)
+                return null;
+            $s = substr($s, $iStart, $iEnd - $iStart + 1);
+        }
+
+        $a = json_decode($s, true);
+        if (!is_array($a) || !array_key_exists('content', $a) || !is_string($a['content']))
+            return null;
+        if (!array_key_exists('actions', $a))
+            return null;
+
+        return [
+            'content' => $a['content'],
+            'actions' => $this->sanitizeChatActions($a['actions']),
+        ];
+    }
+
+    /**
+     * @return array<int, array<string, mixed>>
+     */
+    protected function sanitizeChatActions($mixed)
+    {
+        if (!is_array($mixed))
+            return [];
+
+        $aOut = [];
+        foreach ($mixed as $aItem) {
+            if (!is_array($aItem))
+                continue;
+
+            $sType = strtolower(trim((string)($aItem['type'] ?? '')));
+            $sLabel = trim((string)($aItem['label'] ?? ''));
+            if ($sLabel === '')
+                continue;
+            if (function_exists('mb_substr'))
+                $sLabel = mb_substr($sLabel, 0, 80);
+            else
+                $sLabel = substr($sLabel, 0, 80);
+
+            if ($sType === 'reply') {
+                $aOut[] = ['type' => 'reply', 'label' => $sLabel];
+            } elseif ($sType === 'link') {
+                $sUrl = trim((string)($aItem['url'] ?? ''));
+                if (!$this->isAllowedChatActionUrl($sUrl))
+                    continue;
+                $aOut[] = ['type' => 'link', 'label' => $sLabel, 'url' => $sUrl];
+            }
+
+            if (count($aOut) >= 8)
+                break;
+        }
+
+        return $aOut;
+    }
+
+    protected function isAllowedChatActionUrl($sUrl)
+    {
+        $a = parse_url((string)$sUrl);
+        if (($a['scheme'] ?? '') !== 'https' || empty($a['host']))
+            return false;
+
+        $sHost = strtolower((string)$a['host']);
+        $aAllowed = ['hiweave.com', 'www.hiweave.com'];
+        if (defined('BX_DOL_URL_ROOT')) {
+            $aSite = parse_url(BX_DOL_URL_ROOT);
+            if (!empty($aSite['host']))
+                $aAllowed[] = strtolower((string)$aSite['host']);
+        }
+
+        return in_array($sHost, $aAllowed, true);
+    }
+
+    protected function sseChatEvent($aPayload)
+    {
+        return 'data: ' . json_encode($aPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n\n";
+    }
+
+    protected function parseSseChatEvent($sEvent)
+    {
+        $s = trim((string)$sEvent);
+        if (!preg_match('/^data:\s*(.+)$/s', $s, $aM))
+            return null;
+
+        $a = json_decode($aM[1], true);
+        return is_array($a) ? $a : null;
+    }
+
+    protected function resetChatActionsSseState()
+    {
+        return [
+            'mode' => 'undecided',
+            'buf' => '',
+            'messageId' => '',
+            'held' => [],
+        ];
+    }
+
+    protected function emitHeldChatActionsSse(&$aState, $fEmit)
+    {
+        foreach ($aState['held'] as $sHeld)
+            $fEmit($sHeld);
+        $aState['held'] = [];
+    }
+
+    protected function emitParsedChatActionsSse(&$aState, $fEmit, $aParsed)
+    {
+        $sId = $aState['messageId'] !== '' ? $aState['messageId'] : ('msg_' . uniqid());
+        $fEmit($this->sseChatEvent([
+            'type' => 'TEXT_MESSAGE_START',
+            'messageId' => $sId,
+            'role' => 'assistant',
+        ]));
+        if ($aParsed['content'] !== '') {
+            $fEmit($this->sseChatEvent([
+                'type' => 'TEXT_MESSAGE_CONTENT',
+                'messageId' => $sId,
+                'delta' => $aParsed['content'],
+            ]));
+        }
+        $fEmit($this->sseChatEvent([
+            'type' => 'TEXT_MESSAGE_END',
+            'messageId' => $sId,
+        ]));
+        if (!empty($aParsed['actions'])) {
+            $fEmit($this->sseChatEvent([
+                'type' => 'CUSTOM',
+                'name' => 'chat_actions',
+                'messageId' => $sId,
+                'value' => $aParsed['actions'],
+            ]));
+        }
+        $aState = $this->resetChatActionsSseState();
+    }
+
+    protected function processChatActionsSseEvent($sEvent, &$aState, $fEmit)
+    {
+        $aPayload = $this->parseSseChatEvent($sEvent);
+        $sType = is_array($aPayload) ? (string)($aPayload['type'] ?? '') : '';
+
+        if ($sType === 'TEXT_MESSAGE_START') {
+            if ($aState['held'] || $aState['buf'] !== '')
+                $this->flushChatActionsSseState($aState, $fEmit);
+            $aState['messageId'] = (string)($aPayload['messageId'] ?? '');
+            $aState['held'][] = $sEvent;
+            return;
+        }
+
+        if ($sType === 'TEXT_MESSAGE_CONTENT') {
+            $sDelta = (string)($aPayload['delta'] ?? '');
+            $aState['buf'] .= $sDelta;
+            if ($aState['mode'] === 'text') {
+                $fEmit($sEvent);
+                return;
+            }
+            if ($aState['mode'] === 'undecided') {
+                $mixed = $this->chatActionsStreamLooksLikeJson($aState['buf']);
+                if ($mixed === false) {
+                    $aState['mode'] = 'text';
+                    $this->emitHeldChatActionsSse($aState, $fEmit);
+                    $fEmit($sEvent);
+                    return;
+                }
+                if ($mixed === true)
+                    $aState['mode'] = 'json';
+            }
+            $aState['held'][] = $sEvent;
+            return;
+        }
+
+        if ($sType === 'TEXT_MESSAGE_END') {
+            $aParsed = $this->parseAssistantChatPayload($aState['buf']);
+            if ($aParsed && ($aState['mode'] === 'json' || $aState['mode'] === 'undecided')) {
+                $this->emitParsedChatActionsSse($aState, $fEmit, $aParsed);
+                return;
+            }
+            $this->emitHeldChatActionsSse($aState, $fEmit);
+            $fEmit($sEvent);
+            $aState = $this->resetChatActionsSseState();
+            return;
+        }
+
+        if ($sType === 'RUN_ERROR' || $sType === 'RUN_FINISHED') {
+            $this->flushChatActionsSseState($aState, $fEmit);
+            $fEmit($sEvent);
+            return;
+        }
+
+        if ($aState['mode'] === 'json' || $aState['held'])
+            $this->flushChatActionsSseState($aState, $fEmit);
+
+        $fEmit($sEvent);
+    }
+
+    protected function chatActionsStreamLooksLikeJson($sBuf)
+    {
+        $s = ltrim((string)$sBuf);
+        if ($s === '')
+            return null;
+        $sFirst = $s[0];
+        if ($sFirst === '{' || $sFirst === '`')
+            return true;
+        return false;
+    }
+
+    protected function flushChatActionsSseState(&$aState, $fEmit)
+    {
+        if ($aState['buf'] === '' && !$aState['held'])
+            return;
+
+        $aParsed = $this->parseAssistantChatPayload($aState['buf']);
+        if ($aParsed && $aState['mode'] !== 'text') {
+            $this->emitParsedChatActionsSse($aState, $fEmit, $aParsed);
+            return;
+        }
+
+        $this->emitHeldChatActionsSse($aState, $fEmit);
+        $aState = $this->resetChatActionsSseState();
+    }
+
+    protected function persistAssistantChatActions($oAgent)
+    {
+        if (!is_object($oAgent) || !method_exists($oAgent, 'getChatHistory'))
+            return;
+
+        $oHistory = $oAgent->getChatHistory();
+        if (!($oHistory instanceof BxDolAiChatHistory) || !method_exists($oHistory, 'persistMessages'))
+            return;
+
+        $aMessages = $oHistory->getMessages();
+        for ($i = count($aMessages) - 1; $i >= 0; $i--) {
+            $oMessage = $aMessages[$i];
+            if (
+                $oMessage instanceof NeuronAI\Chat\Messages\ToolCallMessage
+                || $oMessage instanceof NeuronAI\Chat\Messages\ToolResultMessage
+            ) {
+                continue;
+        }
+
+            $sRole = $oMessage->getRole();
+            if ($sRole === 'model')
+                $sRole = 'assistant';
+            if ($sRole !== 'assistant')
+                return;
+
+            $aParsed = $this->parseAssistantChatPayload((string)$oMessage->getContent());
+            if (!$aParsed)
+                return;
+
+            $oMessage->setContents($aParsed['content']);
+            $oMessage->addMetadata('actions', $aParsed['actions']);
+            $oHistory->persistMessages();
+            return;
+        }
+    }
+
+    public function streamAgentChat($iAgentId, $sPrompt, $aParams = [], $sThreadId = null)
+    {
+        if (!isset($aParams['chat_history_subindex']))
+            $aParams = array_merge($this->resolveChatHistoryParams($iAgentId), $aParams);
+
+        // Staging header.inc.php dumps HTML on fatals and warns on null error_get_last().
+        // Any HTML after SSE is parsed by TanStack as "unterminated trailing data".
+        @ini_set('display_errors', '0');
+        @ini_set('zlib.output_compression', '0');
+        @ini_set('implicit_flush', '1');
+        if (function_exists('apache_setenv'))
+            @apache_setenv('no-gzip', '1');
+        while (ob_get_level())
+            ob_end_clean();
+
+        // UNA BxDolDb::pdoExceptionHandler dumps HTML 503 on uncaught PDOException.
+        restore_exception_handler();
+
+        $oAdapter = new NeuronAI\Chat\Messages\Stream\Adapters\AGUIAdapter($sThreadId);
+
+        http_response_code(200);
+        foreach ($oAdapter->getHeaders() as $sName => $sValue)
+            header($sName . ': ' . $sValue);
+
+        $fEmit = function ($sEvent) {
+            echo $sEvent;
+            if (ob_get_level())
+                ob_flush();
+            flush();
+        };
+
+        $aAgent = BxDolAIQuery::getAgentObject((int)$iAgentId);
+        if ($aAgent)
+            $this->_aChatContext = ['agent' => $aAgent, 'params' => $aParams];
+
+        if ($aAgent && $this->isChatSessionRateLimited($aAgent, $aParams)) {
+            $this->emitChatErrorSse($oAdapter, $fEmit, $this->getChatSessionRateLimitError());
+            error_clear_last();
+            @ini_set('display_errors', '0');
+            exit;
+        }
+
+        $iRequestTurns = (int)($aParams['request_user_turns'] ?? 0);
+        if ($aAgent && $this->isChatTurnLimitReached($aAgent, $this->getChatUserTurnCount((int)$iAgentId, $aParams), $iRequestTurns)) {
+            $this->emitConversationClosed('limit', '', $aAgent, $aParams);
+            $this->emitChatLimitSse($oAdapter, $fEmit, $this->getChatLimitMessage($aAgent));
+            error_clear_last();
+            @ini_set('display_errors', '0');
+            exit;
+        }
+
+        $sPrompt = $this->applyChatInputLimit((string)$sPrompt, $aAgent ?: []);
+
+        $bStarted = false;
+        $aSseState = $this->resetChatActionsSseState();
+        $o = null;
+        try {
+            $o = self::getAgentInstance((int)$iAgentId, $aParams);
+            $oHandler = $o->stream(new NeuronAI\Chat\Messages\UserMessage($sPrompt));
+
+            foreach ($oHandler->events($oAdapter) as $sEvent) {
+                $bStarted = true;
+                $this->processChatActionsSseEvent($sEvent, $aSseState, $fEmit);
+            }
+
+            $this->flushChatActionsSseState($aSseState, $fEmit);
+            $this->persistAssistantChatActions($o);
+
+            if ($aAgent && $this->isChatTurnLimitReached($aAgent, $this->getChatUserTurnCount((int)$iAgentId, $aParams)))
+                $this->emitConversationClosed('limit', '', $aAgent, $aParams);
+        } catch (Throwable $oException) {
+            bx_log('sys_agents', "Stream exception for agent {$iAgentId}: " . $oException->getMessage() . " INPUT:" . $sPrompt);
+            $this->flushChatActionsSseState($aSseState, $fEmit);
+            if (!$bStarted) {
+                foreach ($oAdapter->start() as $sEvent)
+                    $fEmit($sEvent);
+            }
+            $sMsg = $oException->getMessage();
+            $fEmit('data: ' . json_encode(['type' => 'RUN_ERROR', 'message' => $sMsg !== '' ? $sMsg : _t('_sys_agents_exception')]) . "\n\n");
+            foreach ($oAdapter->end() as $sEvent)
+                $fEmit($sEvent);
+        }
+
+        error_clear_last();
+        @ini_set('display_errors', '0');
+        exit;
+    }
+
+    /**
+     * SSE assistant reply without calling the model. Used when max_turns is hit.
+     */
+    protected function emitChatLimitSse($oAdapter, $fEmit, $sMessage)
+    {
+        foreach ($oAdapter->start() as $sEvent)
+            $fEmit($sEvent);
+
+        $oChunk = new NeuronAI\Chat\Messages\Stream\Chunks\TextChunk('msg_limit', (string)$sMessage);
+        foreach ($oAdapter->transform($oChunk) as $sEvent)
+            $fEmit($sEvent);
+
+        foreach ($oAdapter->end() as $sEvent)
+            $fEmit($sEvent);
+    }
+
+    /**
+     * SSE error without an assistant bubble. Used when a new session is rate-limited.
+     */
+    protected function emitChatErrorSse($oAdapter, $fEmit, $sMessage)
+    {
+        foreach ($oAdapter->start() as $sEvent)
+            $fEmit($sEvent);
+
+        $sMsg = trim((string)$sMessage);
+        $fEmit('data: ' . json_encode([
+            'type' => 'RUN_ERROR',
+            'message' => $sMsg !== '' ? $sMsg : _t('_sys_agents_exception'),
+        ]) . "\n\n");
+
+        foreach ($oAdapter->end() as $sEvent)
+            $fEmit($sEvent);
+    }
+
+    public function sendMessengerMessage ($iSender, $iRecipient, $sMsg) 
+    {        
+        $oMessengerModule = BxDolModule::getInstance('bx_messenger');
+
+        $aAutoReplyData = [
+            'message' => $sMsg,
+            'participants' => [$iSender, $iRecipient],
+        ];
+
+        $iSaveProfileId = $oMessengerModule->setProfileId($iSender);
+        $a = $oMessengerModule->sendMessage($aAutoReplyData, $iRecipient, $iSender);
+        $oMessengerModule->setProfileId($iSaveProfileId);
+
+        return $a;
+    }
+
+    public function getAgentsByAlertUnitAndAction($sUnit, $sAction)
+    {
+        $aAgents = [];
+        $a = $this->_oDb->getAgentsWithAlert();
+        foreach ($a as $r) {
+            $aAlert = explode(':', $r['alert']); // TODO: remake to concantenate $sUnit and $sAction and then compare
+            if (count($aAlert) == 2 && $aAlert[0] == $sUnit && $aAlert[1] == $sAction)
+                $aAgents[] = $r;
+        }
+
+        return $aAgents;
+    }
+
+    public function getAgentsBy($aParams)
+    {
+        return $this->_oDb->getAgentsBy($aParams);
+    }
+
+    public function getAgentsByProfileId($iProfileId)
+    {
+        return $this->_oDb->getAgentsByProfileId($iProfileId);
+    }
+    
+    public function getAgentsByFormObject($sFormObject)
+    {
+        return $this->_oDb->getAgentsByFormObject($sFormObject);
+    }
+
+    public function getAgentsByTriggerType($sTrigger)
+    {
+        return $this->_oDb->getAgentsByTriggerType($sTrigger);
+    }
+
+    public function getAgentById($iId)
+    {
+        return $this->_oDb->getAgentById($iId);
+    }
+
+    public function getAgentByTriggerWebhookKey($sKey)
+    {
+        return $this->_oDb->getAgentByTriggerWebhookKey($sKey);
+    }
+
+    public function evalCode($aAutomator, $aParams = [])
+    {
+        try {
+            $this->_evalCode($aAutomator, $aParams);
+        }
+        catch (Exception $oException) {
+            $this->log($oException->getFile() . ':' . $oException->getLine() . ' ' . $oException->getMessage());
+        }
+        catch (Error $oError) {
+            $this->log($oError->getFile() . ':' . $oError->getLine() . ' ' . $oError->getMessage());
+        }
+    }
+
+    public function emulCode($aAutomator, $aParams = [])
+    {
+        ob_start();
+
+        try {
+            $this->_evalCode($aAutomator, $aParams);
+        }
+        catch (Exception $oException) {
+            return $oException->getMessage();
+        }
+        catch (Error $oError) {
+            return $oError->getMessage();
+        }
+        finally {
+            $sOutput = ob_get_clean();
+
+            if(!empty($sOutput))
+                return $sOutput;
+        }
+    }
+
+    public function log($mixedContents, $sSection = '')
+    {
+        if(!$this->_bWriteLog)
+            return;
+
+        if(is_array($mixedContents))
+            $mixedContents = var_export($mixedContents, true);	
+        else if(is_object($mixedContents))
+            $mixedContents = json_encode($mixedContents);
+
+        if(empty($sSection))
+            $sSection = "Core";
+
+        bx_log('sys_agents', ":\n[" . $sSection . "] " . $mixedContents, BX_LOG_ERR);
+    }
+
+    protected function _evalCode($aAutomator, $aParams = [])
+    {
+        $sCode = '';
+        switch($aAutomator['type']) {
+            case BX_DOL_AI_AUTOMATOR_EVENT:
+                $sCode = $aAutomator['code']. '; onAlert($aParams["alert"]->iObject , $aParams["alert"]->iSender , $aParams["alert"]->aExtras);';
+                break;
+
+            case BX_DOL_AI_AUTOMATOR_SCHEDULER:
+                $sCode = $aAutomator['code'] . '; onCron();';
+                break;
+
+            case BX_DOL_AI_AUTOMATOR_WEBHOOK:
+                $sCode = $aAutomator['code'] . '; onHook();';
+                break;
+        }
+
+        eval($sCode);
+    }
+}

+ 11 - 5
inc/classes/BxDolAiAgent.php

@@ -33,7 +33,11 @@ class BxDolAiAgent extends RAG
 
     protected function provider(): NeuronAI\Providers\AIProviderInterface
     {
-        return BxDolAIModelFactory::getModelInstance($this->aAgent['model_id']);
+        $aOverrides = [];
+        if ((int)($this->aAgent['max_tokens'] ?? 0) > 0)
+            $aOverrides['max_tokens'] = (int)$this->aAgent['max_tokens'];
+
+        return BxDolAIModelFactory::getModelInstance((int)$this->aAgent['model_id'], $aOverrides);
     }
 
     protected function instructions(): string
@@ -73,9 +77,11 @@ class BxDolAiAgent extends RAG
         if ($this->aAgent['tools']) {
             $aTools = explode(',', $this->aAgent['tools']);
             $aToolInstances = [];
-            foreach ($aTools as $iToolId) {
-                $oTool = BxDolAIToolFactory::getToolInstance($iToolId);
-                $aToolInstances[] = $oTool;                
+            foreach ($aTools as $sToolId) {
+                $iToolId = (int)trim($sToolId);
+                if ($iToolId <= 0)
+                    continue;
+                $aToolInstances[] = BxDolAIToolFactory::getToolInstance($iToolId);
             }
             return $aToolInstances;
         }
@@ -123,7 +129,7 @@ class BxDolAiAgent extends RAG
 
     protected function getСhatHistoryThreadId(): string
     {
-        return BxDolAI::chatHistoryThreadId($this->aAgent, $this->aParams);
+        return BxDolAi::chatHistoryThreadId($this->aAgent, $this->aParams);
     }
 
     protected function getAlertTriggerModifyableKeys(): string

+ 1 - 1
inc/classes/BxDolAiAlertResponse.php

@@ -25,7 +25,7 @@ class BxDolAiAlertResponse extends BxDolAlertsResponse
             return;
 
         // call agents
-        $oAi = BxDolAI::getInstance();
+        $oAi = BxDolAi::getInstance();
         if($aAgents = $oAi->getAgentsByProfileId($iRecipient)) {
             $GLOBALS['glAgentsCallQueue'] = [];
             foreach($aAgents as $a) {

+ 39 - 1
inc/classes/BxDolAiChatHistory.php

@@ -19,8 +19,41 @@ class BxDolAiChatHistory extends SQLChatHistory
 {
     public function addMessage(Message $message): ChatHistoryInterface
     {
+        $bStampIp = $this->isIncomingFirstUserMessage($message);
         $this->ensureValidTail($message);
-        return parent::addMessage($message);
+        $o = parent::addMessage($message);
+        if ($bStampIp)
+            $this->stampVisitorIp();
+        return $o;
+    }
+
+    protected function isIncomingFirstUserMessage(Message $incoming): bool
+    {
+        if (!($incoming instanceof UserMessage) || $incoming instanceof ToolResultMessage)
+            return false;
+
+        foreach ($this->history as $oMessage) {
+            if ($oMessage instanceof UserMessage && !($oMessage instanceof ToolResultMessage))
+                return false;
+        }
+
+        return true;
+    }
+
+    protected function stampVisitorIp(): void
+    {
+        if (!function_exists('getVisitorIP') || !function_exists('bx_get_ip_hash'))
+            return;
+
+        $iIp = bx_get_ip_hash(getVisitorIP());
+        if ($iIp === '' || $iIp === '0' || $iIp === 0)
+            return;
+
+        $stmt = $this->pdo->prepare("UPDATE {$this->table} SET `ip` = :ip WHERE `thread_id` = :thread_id AND (`ip` = 0 OR `ip` IS NULL)");
+        $stmt->execute([
+            'ip' => $iIp,
+            'thread_id' => $this->thread_id,
+        ]);
     }
 
     protected function ensureValidTail(Message $incoming): void
@@ -54,4 +87,9 @@ class BxDolAiChatHistory extends SQLChatHistory
             $this->setMessages($this->history);
         }
     }
+
+    public function persistMessages(): void
+    {
+        $this->setMessages($this->history);
+    }
 }

+ 524 - 0
inc/classes/BxDolAiQuery.php

@@ -0,0 +1,524 @@
+<?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 BxDolAIQuery extends BxDolDb
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    static protected function _getObject(int $iId, string $sTable): array|bool
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $a = $oDb->getRow("SELECT * FROM `$sTable` WHERE `id` = :id", ['id' => $iId]);
+        if(!$a || !is_array($a))
+            return false;
+
+        return $a;
+    }
+
+    static public function getModelObject(int $iId): array|bool
+    {
+        return self::_getObject($iId, 'sys_agents_models');
+    }
+
+    static public function getVectorStoreObject(int $iId): array|bool
+    {
+        return self::_getObject($iId, 'sys_agents_vector_store');
+    }
+
+    static public function getAgentObject(int $iId): array|bool
+    {
+        return self::_getObject($iId, 'sys_agents_agents');
+    }
+
+    static public function getToolObject(int $iId): array|bool
+    {
+        return self::_getObject($iId, 'sys_agents_tools');
+    }
+
+    static public function getAssistantObject($iId)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $aAssistant = $oDb->getRow("SELECT * FROM `sys_agents_assistants` WHERE `id` = :id", ['id' => $iId]);
+        if(!$aAssistant || !is_array($aAssistant))
+            return false;
+
+        return $aAssistant;
+    }
+
+    public function insertModel($aModel)
+    {
+        if(empty($aModel))
+            return false;
+
+        return (int)$this->query("INSERT INTO `sys_agents_models` SET " . $this->arrayToSQL($aModel));
+    }
+
+    public function getModelsBy($aParams = [])
+    {
+        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
+    	$sWhereClause = "";
+
+        switch($aParams['sample']) {
+            case 'id':
+            	$aMethod['name'] = 'getRow';
+            	$aMethod['params'][1] = [
+                    'id' => $aParams['id']
+                ];
+
+                $sWhereClause .= " AND `id`=:id";
+                break;
+
+            case 'all_pairs':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'id';
+                $aMethod['params'][2] = 'title';
+                $aMethod['params'][3] = [];
+
+                if(isset($aParams['active'])) {
+                    $aMethod['params'][3]['active'] = $aParams['active'];
+
+                    $sWhereClause .= " AND `active`=:active";
+                }
+
+                if(isset($aParams['capabilities'])) {
+                    if (is_array($aParams['capabilities']))
+                    {                        
+                        $sWhereClause .= " AND `capabilities` IN (" . $this->implode_escape($aParams['capabilities']) . ")"; 
+                    }
+                    else {
+                        $aMethod['params'][3]['capabilities'] = $aParams['capabilities'];
+                        $sWhereClause .= " AND `capabilities` = :capabilities";
+                    }
+                }
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT * 
+            FROM `sys_agents_models`
+            WHERE 1" . $sWhereClause;
+
+        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
+    }
+
+    public function getVectorStores (): mixed
+    {
+        $sQuery = "SELECT `id`, `title` FROM `sys_agents_vector_store` WHERE `active` = 1 ORDER BY `title` ASC";
+        return $this->getPairs($sQuery, 'id', 'title'); 
+    }
+
+    public function getVectorStoreById (int $iId): mixed
+    {
+        $sQuery = "SELECT * FROM `sys_agents_vector_store` WHERE `id` = :id";
+        return $this->getRow($sQuery, ['id' => $iId]); 
+    }
+
+    public function insertVectorStore($aVectorStore)
+    {
+        if(empty($aVectorStore) || !is_array($aVectorStore))
+            return false;
+
+        return (int)$this->query("INSERT INTO `sys_agents_vector_store` SET " . $this->arrayToSQL($aVectorStore));
+    }
+
+    public function getVectorStoreDataNum (int $iVectorStoreId): int
+    {
+        $sQuery = "SELECT COUNT(*) FROM `sys_agents_vector_store_data` WHERE `vector_store_id` = :vector_store_id";
+        return (int)$this->getOne($sQuery, ['vector_store_id' => $iVectorStoreId]);
+    }
+
+    public function addVectorStoreData (int $iVectorStoreId, string $sType, string $sName, int $iFileSize, string $sMetadata, string $sSettings, string $sContent): bool
+    {
+        $sQuery = "INSERT INTO `sys_agents_vector_store_data` SET  
+            `vector_store_id` = :vector_store_id,
+            `type` = :type, 
+            `name` = :name, 
+            `size` = :size,
+            `metadata` = :metadata, 
+            `settings` = :settings, 
+            `content` = :content,
+            `added` = :ts
+        ";
+        return $this->query($sQuery, [
+            'vector_store_id' => $iVectorStoreId,
+            'type' => $sType, 
+            'name' => $sName, 
+            'size' => $iFileSize,
+            'metadata' => $sMetadata, 
+            'settings' => $sSettings, 
+            'content' => $sContent,
+            'ts' => time()]) > 0;
+    }
+
+    public function getVectorStoreDataById (int $iId): mixed
+    {
+        $sQuery = "SELECT * FROM `sys_agents_vector_store_data` WHERE `id` = :id";
+        return $this->getRow($sQuery, ['id' => $iId]);
+    }
+
+    static public function getVectorStorePendingData (int $iLimit = 1): mixed
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = "SELECT * FROM `sys_agents_vector_store_data` WHERE `status` = 'pending' ORDER BY `added` ASC LIMIT :limit";
+        return $oDb->getAll($sQuery, ['limit' => $iLimit]);
+    }
+
+    static public function updateVectorStoreDataStatus (int $iId, string $sStatus): mixed
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = "UPDATE `sys_agents_vector_store_data` SET `status` = :status WHERE `id` = :id";
+        return $oDb->query($sQuery, ['status' => $sStatus, 'id' => $iId]);
+    }
+
+    public function getAgentsWithAlert($bActiveOnly = true)
+    {
+        return $this->fromCache('sys_agents_with_alert', 'getAll', "SELECT * FROM `sys_agents_agents` WHERE `trigger` = 'alert' AND `active` = :active", ['active' => $bActiveOnly ? 1 : 0]);
+    }
+
+    public function getAgentsByProfileId($iProfileId, $bActiveOnly = true)
+    {
+        return $this->getAll("SELECT * FROM `sys_agents_agents` WHERE `trigger` = 'message' AND `profile_id` = :profile AND `active` = :active", ['profile' => $iProfileId, 'active' => $bActiveOnly ? 1 : 0]);
+    }
+
+    public function getAgentsByFormObject($sFormObject, $bActiveOnly = true)
+    {
+        return $this->fromMemory('sys_agents_with_form_' . $sFormObject, 'getAll', "SELECT * FROM `sys_agents_agents` WHERE `trigger` = 'form-input' AND `form_object` = :form_object AND `active` = :active", ['form_object' => $sFormObject, 'active' => $bActiveOnly ? 1 : 0]);
+    }
+
+    public function getAgentsBy($aParams = [])
+    {
+        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
+    	$sWhereClause = "";
+
+        switch($aParams['sample']) {
+            case 'id':
+            	$aMethod['name'] = 'getRow';
+            	$aMethod['params'][1] = [
+                    'id' => $aParams['id']
+                ];
+
+                $sWhereClause .= " AND `id`=:id";
+                break;
+
+            case 'all':
+                if(isset($aParams['active'])) {
+                    $aMethod['params'][3]['active'] = $aParams['active'];
+
+                    $sWhereClause .= " AND `active`=:active";
+                }
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT * 
+            FROM `sys_agents_agents`
+            WHERE 1" . $sWhereClause;
+
+        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
+    }
+
+    public function getAgentById(int $iId): mixed
+    {
+        return $this->getAgentsBy(['sample' => 'id', 'id' => $iId]);
+    }
+
+    public function getAgentByTriggerWebhookKey($sKey, $bActiveOnly = true)
+    {
+        return $this->getRow("SELECT * FROM `sys_agents_agents` WHERE `trigger` = 'scheduler' AND `webhook_key` = :key AND `active` = :active", ['key' => $sKey, 'active' => $bActiveOnly ? 1 : 0]);
+    }
+    
+    public function getAgentsByTriggerType($sTrigger, $bActiveOnly = true)
+    {
+        return $this->getAll("SELECT * FROM `sys_agents_agents` WHERE `trigger` = :trigger AND `active` = :active", ['trigger' => $sTrigger, 'active' => $bActiveOnly ? 1 : 0]);
+    }
+
+    public function updateAgentField($iId, $sField, $sValue)
+    {
+        return $this->query("UPDATE `sys_agents_agents` SET `$sField` = :value WHERE `id` = :id", ['value' => $sValue, 'id' => $iId]);
+    }
+
+    public function getToolById (int $iId): mixed
+    {
+        $sQuery = "SELECT * FROM `sys_agents_tools` WHERE `id` = :id";
+        return $this->getRow($sQuery, ['id' => $iId]); 
+    }
+
+    public function insertTool(array $aFields): int
+    {
+        if(empty($aFields) || !is_array($aFields))
+            return false;
+
+        return (int)$this->query("INSERT INTO `sys_agents_tools` SET " . $this->arrayToSQL($aFields));
+    }
+
+    public function getTools()
+    {
+        $sQuery = "SELECT `id`, `title` FROM `sys_agents_tools` WHERE `active` = 1 ORDER BY `title` ASC";
+        return $this->getPairs($sQuery, 'id', 'title'); 
+    }
+
+    public function wipeAgentChatHistory($aAgent) 
+    {
+        $sQuery = "DELETE FROM `sys_agents_chat_history` WHERE `thread_id` LIKE :val";
+        $iAffected = 0;
+        $iAffected += $this->query($sQuery, ['val' => $aAgent['trigger'] . ':' . $aAgent['id'] . ':%']);
+        $iAffected += $this->query($sQuery, ['val' => $aAgent['trigger'] . ':' . $aAgent['id']]);
+        return $iAffected;
+    }
+
+    public function getAgentChatHistoryRows($aAgent)
+    {
+        $sExact = ($aAgent['trigger'] ?? '') . ':' . ($aAgent['id'] ?? '');
+        $sFields = "`id`, `thread_id`, `messages`, `created_at`, `updated_at`";
+        if ($this->isFieldExists('sys_agents_chat_history', 'closed_reason'))
+            $sFields .= ", `closed_reason`";
+        return $this->getAll("
+            SELECT $sFields
+            FROM `sys_agents_chat_history`
+            WHERE `thread_id` = :exact OR `thread_id` LIKE :prefix
+            ORDER BY `updated_at` DESC
+            LIMIT 200
+        ", [
+            'exact' => $sExact,
+            'prefix' => $sExact . ':%',
+        ]);
+    }
+
+    public function getChatArtifactsByHistoryIds($aIds)
+    {
+        $aIds = array_values(array_unique(array_filter(array_map('intval', (array)$aIds))));
+        if (!$aIds)
+            return [];
+
+        $sIn = implode(',', $aIds);
+        $aRows = $this->getAll("
+            SELECT `history_id`, `field_name`, `field_value`
+            FROM `sys_agents_chat_artifacts`
+            WHERE `history_id` IN ($sIn)
+            ORDER BY `id` ASC
+        ");
+        return $this->mapChatArtifactRows($aRows);
+    }
+
+    public function getChatArtifactsByThreadId($sThreadId)
+    {
+        $sThreadId = (string)$sThreadId;
+        if ($sThreadId === '')
+            return [];
+
+        $aRows = $this->getAll("
+            SELECT `a`.`history_id`, `a`.`field_name`, `a`.`field_value`
+            FROM `sys_agents_chat_artifacts` AS `a`
+            INNER JOIN `sys_agents_chat_history` AS `h` ON `h`.`id` = `a`.`history_id`
+            WHERE `h`.`thread_id` = :t
+            ORDER BY `a`.`id` ASC
+        ", [
+            't' => $sThreadId,
+        ]);
+        $aMap = $this->mapChatArtifactRows($aRows);
+        if (!$aMap)
+            return [];
+        return (array)reset($aMap);
+    }
+
+    public function getChatArtifactsForAgent($aAgent)
+    {
+        $sExact = ($aAgent['trigger'] ?? '') . ':' . ($aAgent['id'] ?? '');
+        if ($sExact === ':')
+            return [];
+
+        $aRows = $this->getAll("
+            SELECT `a`.`history_id`, `a`.`field_name`, `a`.`field_value`
+            FROM `sys_agents_chat_artifacts` AS `a`
+            INNER JOIN `sys_agents_chat_history` AS `h` ON `h`.`id` = `a`.`history_id`
+            WHERE `h`.`thread_id` = :exact OR `h`.`thread_id` LIKE :prefix
+            ORDER BY `a`.`id` ASC
+        ", [
+            'exact' => $sExact,
+            'prefix' => $sExact . ':%',
+        ]);
+        return $this->mapChatArtifactRows($aRows);
+    }
+
+    protected function mapChatArtifactRows($aRows)
+    {
+        if (!is_array($aRows) || !$aRows)
+            return [];
+
+        $aOut = [];
+        foreach ($aRows as $aRow) {
+            if (!is_array($aRow))
+                continue;
+            $iHistoryId = (int)($aRow['history_id'] ?? 0);
+            $sName = trim((string)($aRow['field_name'] ?? ''));
+            if ($iHistoryId <= 0 || $sName === '')
+                continue;
+            $aOut[$iHistoryId][] = [
+                'field_name' => $sName,
+                'field_value' => (string)($aRow['field_value'] ?? ''),
+            ];
+        }
+        return $aOut;
+    }
+
+    public function getExpiredAgentChatHistoryIds($aAgent, $iTtlMin)
+    {
+        if (!$this->isFieldExists('sys_agents_chat_history', 'closed_reason'))
+            return [];
+
+        $sExact = ($aAgent['trigger'] ?? '') . ':' . ($aAgent['id'] ?? '');
+        $iTtlMin = (int)$iTtlMin;
+        if ($sExact === ':' || $iTtlMin <= 0)
+            return [];
+
+        return $this->getColumn("
+            SELECT `id` FROM `sys_agents_chat_history`
+            WHERE (`thread_id` = :exact OR `thread_id` LIKE :prefix)
+              AND `messages` IS NOT NULL AND `messages` != '' AND `messages` != '[]'
+              AND (`closed_reason` = '' OR `closed_reason` IS NULL)
+              AND `updated_at` <= DATE_SUB(NOW(), INTERVAL :ttl MINUTE)
+        ", [
+            'exact' => $sExact,
+            'prefix' => $sExact . ':%',
+            'ttl' => $iTtlMin,
+        ]);
+    }
+
+    /**
+     * Rename this agent's guest threads (`…:{sessionId}`) onto a profile id.
+     * If the member thread already has messages, drop the guest copy.
+     */
+    public function adoptGuestChatHistory($aAgent, $sSessionId, $iProfileId)
+    {
+        $sSessionId = (string)$sSessionId;
+        $iProfileId = (int)$iProfileId;
+        if ($sSessionId === '' || !$iProfileId || empty($aAgent['id']))
+            return 0;
+
+        $sPrefix = $aAgent['trigger'] . ':' . $aAgent['id'] . ':';
+        $sOldSuffix = ':' . $sSessionId;
+        $sNewSuffix = ':' . $iProfileId;
+        $iLen = strlen($sOldSuffix);
+        $aBindings = [
+            'p' => $sPrefix . '%',
+            'o' => $sOldSuffix,
+            'n' => $sNewSuffix,
+        ];
+
+        $this->query("
+            DELETE `g` FROM `sys_agents_chat_history` AS `g`
+            INNER JOIN `sys_agents_chat_history` AS `m`
+                ON `m`.`thread_id` = CONCAT(LEFT(`g`.`thread_id`, CHAR_LENGTH(`g`.`thread_id`) - $iLen), :n)
+            WHERE `g`.`thread_id` LIKE :p AND RIGHT(`g`.`thread_id`, $iLen) = :o
+              AND `m`.`messages` IS NOT NULL AND `m`.`messages` != '' AND `m`.`messages` != '[]'
+        ", $aBindings);
+
+        $this->query("
+            DELETE `m` FROM `sys_agents_chat_history` AS `m`
+            INNER JOIN `sys_agents_chat_history` AS `g`
+                ON `g`.`thread_id` LIKE :p AND RIGHT(`g`.`thread_id`, $iLen) = :o
+               AND `m`.`thread_id` = CONCAT(LEFT(`g`.`thread_id`, CHAR_LENGTH(`g`.`thread_id`) - $iLen), :n)
+            WHERE `m`.`messages` = '' OR `m`.`messages` = '[]' OR `m`.`messages` IS NULL
+        ", $aBindings);
+
+        $iAffected = (int)$this->query("
+            UPDATE `sys_agents_chat_history`
+            SET `thread_id` = CONCAT(LEFT(`thread_id`, CHAR_LENGTH(`thread_id`) - $iLen), :n)
+            WHERE `thread_id` LIKE :p AND RIGHT(`thread_id`, $iLen) = :o
+        ", $aBindings);
+
+        return $iAffected;
+    }
+
+    static public function getAlert($s) {
+        if (!$s)
+            return false;        
+        $a = explode(':', $s);
+        $oDb = BxDolDb::getInstance();
+        return $oDb->getRow("SELECT * FROM `sys_alerts_log` WHERE `unit` = :unit AND `action` = :action", [
+            'unit' => $a[0] ?? '',
+            'action' => $a[1] ?? '',
+        ]);
+    }
+
+    public function getAlerts()
+    {
+        $aValues = [];
+        $aAlerts = $this->getAll("SELECT `unit`, `action`, `counter_24h`, `counter_per_request` FROM `sys_alerts_log` ORDER BY `unit`, `action`");
+        foreach ($aAlerts as $a) {
+            $sKey = $a['unit'] . ':' . $a['action'];
+            $aValues[$sKey] = [
+                'key' => $sKey,
+                'unit' => $a['unit'],
+                'action' => $a['action'],
+                'name' => $a['unit'] . ' - ' . $a['action'],
+                'desc' => $this->getAlertDesc($a['unit'] . ':' . $a['action']),
+                'counter_24h' => $a['counter_24h'],
+                'counter_per_request' => $a['counter_per_request'],
+            ];
+        }
+        return $aValues;
+    }
+
+    public function getFormDisplay($sObject)
+    {
+        $aDisplays = $this->getColumn("SELECT `display_name` FROM `sys_form_displays` WHERE `object` = :object", ['object' => $sObject]);
+        return $aDisplays ? current($aDisplays) : null;
+    }
+
+    public function getFormObjects()
+    {
+        $aValues = [];
+        $aForms = $this->getAll("SELECT `f`.`object`, `f`.`title`, `f`.`module`, `m`.`title` AS `module_title` FROM `sys_objects_form` AS f INNER JOIN `sys_modules` as `m` ON `m`.`name` = `f`.`module` ORDER BY `f`.`module`, `f`.`object` ASC");
+        foreach ($aForms as $r) {
+            if ('system' != $r['module']) {
+                $oModule = BxDolModule::getInstance($r['module']);
+                if (!$oModule || !$oModule->isEnabled())
+                    continue;
+            }
+            
+            $sDisplay = $this->getFormDisplay($r['object']);
+            if (!$sDisplay) 
+                continue;
+            $oForm = BxDolForm::getObjectInstance($r['object'], $sDisplay);
+            $sFormId = $oForm->getId();
+            $aValues[$r['object']] = [
+                'form_id' => $sFormId,
+                'form_object' => $r['object'],
+                'module' => $r['module'],
+                'module_title' => $r['module_title'],
+                'form_title' => _t($r['title']),
+            ];
+        }
+        return $aValues;
+    }
+
+    static public function getAlertDesc($sAlert) 
+    {
+        if (!$sAlert)
+            return '';
+        $oDb = BxDolDb::getInstance();
+        [$sUnit, $sAction] = explode(':', $sAlert);
+        $sDesc = $oDb->getOne("SELECT `description` FROM `sys_alerts_desc` WHERE `unit` = :unit AND `action` = :action LIMIT 1", [
+            'unit' => $sUnit,
+            'action' => $sAction,
+        ]);
+        if (!$sDesc) {
+            $oDb->getOne("SELECT `description` FROM `sys_alerts_desc` WHERE `unit` LIKE '{%}' AND `action` = :action LIMIT 1", [
+                'action' => $sAction,
+            ]);
+        }
+        return $sDesc;
+    }
+}
+
+/** @} */

+ 1 - 13
inc/classes/BxDolAlerts.php

@@ -153,7 +153,7 @@ class BxDolAlerts extends BxDol
         BxDolAlertsStats::log($this);
 
         // call agents
-        $oAi = BxDolAI::getInstance();
+        $oAi = BxDolAi::getInstance();
         if($aAgents = $oAi->getAgentsByAlertUnitAndAction($this->sUnit, $this->sAction)) {
             foreach($aAgents as $a) {
                 $aParams = [
@@ -187,18 +187,6 @@ class BxDolAlerts extends BxDol
                 }
             }
         }
-
-        // call automators
-        $oAi = BxDolAI::getInstance();
-        if($oAi->hasAutomators(BX_DOL_AI_AUTOMATOR_EVENT, true)) {
-            $aAutomators = $oAi->getAutomatorsEvent($this->sUnit, $this->sAction);
-            foreach($aAutomators as $aAutomator) {
-                $oAi->callAutomator(BX_DOL_AI_AUTOMATOR_EVENT, [
-                    'automator' => $aAutomator,
-                    'alert' => $this
-                ]);
-            }
-        }        
     }
 
     /**

+ 1 - 1
inc/classes/BxDolCronPruning.php

@@ -73,7 +73,7 @@ class BxDolCronPruning extends BxDolCron
         $iDeletedKeys = $oKey ? $oKey->prune() : 0;
 
         // clean ai related data
-        $iDeletedItems = BxDolAI::pruning();
+        $iDeletedItems = BxDolAi::pruning();
 
         // clean background jobs
         $iDeletedJobs = BxDolBackgroundJobs::pruning();

+ 1 - 16
inc/classes/BxDolSearch.php

@@ -38,7 +38,6 @@ define('BX_DOL_SEARCH_KEYWORD_PAGE', 'site-search-page');
 class BxDolSearch extends BxDol
 {
     protected $_bIsApi;
-    protected $_bAssitantForLiveSearch = true;
         
     protected $aClasses = array(); ///< array of all search classes
     protected $aChoice  = array(); ///< array of current search classes which were choosen in search area
@@ -96,12 +95,7 @@ class BxDolSearch extends BxDol
     public function setApiOutput($bIsApi)
     {
         $this->_bIsApi = $bIsApi;
-    }   
-
-    public function setAssistantForLiveSearch($bAssistantForLiveSearch)
-    {
-        $this->_bAssitantForLiveSearch = $bAssistantForLiveSearch;
-    }  
+    }
 
     /**
      * create units for all classes and calling their processing methods
@@ -110,15 +104,6 @@ class BxDolSearch extends BxDol
     {
         $sCode = $this->_bDataProcessing ? [] : '';
 
-        if($this->_bAssitantForLiveSearch && $this->_bLiveSearch && ($iAssistant = BxDolAI::getAssistantForLiveSearch()) != 0) {
-            $sKeyword = '';
-            if(($sKeyword = bx_get('keyword')) !== false)
-                $sKeyword = bx_process_input($sKeyword);
-
-            if($this->_bIsApi)
-                $sCode .= BxDolAIAssistant::getObjectInstance($iAssistant)->getAskButton($sKeyword);
-        }
-
         $bSingle = count($this->aChoice) == 1;
         foreach($this->aChoice as $sKey => $aValue) {
             if(!$this->_sMetaType && !$aValue['GlobalSearch'])

+ 2 - 2
inc/classes/BxDolSearchExtended.php

@@ -186,9 +186,9 @@ class BxDolSearchExtended extends BxDolFactory implements iBxDolFactoryObject
         return urlencode(base64_encode(json_encode($aConditions)));
     }
 
-    static public function decodeConditions($sConditions) 
+    static public function decodeConditions($mixedConditions) 
     {
-        return json_decode(base64_decode(urldecode($sConditions)), true);
+        return is_string($mixedConditions) ? json_decode(base64_decode(urldecode($mixedConditions)), true) : $mixedConditions;
     }
 
     static public function processParams($aParams, $aQueryParts)

+ 1 - 1
inc/utils.inc.php

@@ -2942,7 +2942,7 @@ function bx_ai_process_agents_call_queue($bFinishRequest = true, $bExit = true)
 
     if (!empty($GLOBALS['glAgentsCallQueue']))
     {
-        $oAi = BxDolAI::getInstance();
+        $oAi = BxDolAi::getInstance();
         foreach ($GLOBALS['glAgentsCallQueue'] as $r) 
         {
             $sMessage = $oAi->callAgent($r['type'], $r['agent'], $r['params']);

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
install/sql/system.sql


+ 30 - 161
modules/boonex/english/data/langs/system/en.xml

@@ -1979,9 +1979,6 @@
     <string name="_adm_stg_cpt_option_sys_agents_inspector_key"><![CDATA[Inspector key]]></string>
     <string name="_adm_stg_cpt_option_sys_agents_inspector_key_info"><![CDATA[Get key at https://app.inspector.dev/]]></string>
     <string name="_adm_stg_cpt_option_sys_agents_asst_chats_trans_del"><![CDATA[Auto delete transient chats everyday]]></string>
-    <string name="_adm_stg_cpt_option_sys_agents_sa"><![CDATA[Assistant for Studio]]></string>
-    <string name="_adm_stg_cpt_option_sys_agents_lsa"><![CDATA[Assistant for Live search]]></string>
-    <string name="_adm_stg_cpt_option_sys_agents_aba"><![CDATA[Assistant for 'Ask AI' block]]></string>
     <string name="_adm_stg_cpt_option_sys_viewport_meta_tag"><![CDATA[Viewport meta tag]]></string>
 
     <string name="_adm_stg_cpt_category_pwa_manifest"><![CDATA[Manifest]]></string>
@@ -2398,23 +2395,14 @@
     <string name="_sys_agents_json_field_err"><![CDATA[Invalid JSON format.]]></string>
     <string name="_sys_agents_unauthorized"><![CDATA[You are not allowed to interact with this agent.]]></string>
     <string name="_sys_agents_agent_not_found"><![CDATA[Agent was not found or is inactive.]]></string>
-    <string name="_sys_agents_automators_txt_active"><![CDATA[Active]]></string>
-    <string name="_sys_agents_automators_txt_name"><![CDATA[Name]]></string>
-    <string name="_sys_agents_automators_txt_type"><![CDATA[Type]]></string>
-    <string name="_sys_agents_automators_txt_alert_unit"><![CDATA[Unit]]></string>
-    <string name="_sys_agents_automators_txt_alert_action"><![CDATA[Action]]></string>
-    <string name="_sys_agents_automators_txt_scheduler_time"><![CDATA[Time]]></string>
-    <string name="_sys_agents_automators_txt_model_id"><![CDATA[Model]]></string>
-    <string name="_sys_agents_automators_txt_profile_id"><![CDATA[Profile]]></string>
-    <string name="_sys_agents_automators_txt_message_id"><![CDATA[Prompt]]></string>
-    <string name="_sys_agents_automators_txt_messages"><![CDATA[Messages]]></string>
-    <string name="_sys_agents_automators_txt_code"><![CDATA[Code]]></string>
-    <string name="_sys_agents_automators_txt_added"><![CDATA[Added]]></string>
-    <string name="_sys_agents_automators_txt_status"><![CDATA[Status]]></string>
     <string name="_sys_agents_agents_txt_name"><![CDATA[Name]]></string>
     <string name="_sys_agents_agents_txt_model"><![CDATA[Model]]></string>
     <string name="_sys_agents_agents_txt_trigger"><![CDATA[Trigger]]></string>
     <string name="_sys_agents_agents_txt_profile_id"><![CDATA[Profile]]></string>
+    <string name="_sys_agents_agents_txt_chats"><![CDATA[Chats]]></string>
+    <string name="_sys_agents_agents_txt_my_chat"><![CDATA[My chat]]></string>
+    <string name="_sys_agents_agents_txt_guest_chat"><![CDATA[Guest]]></string>
+    <string name="_sys_agents_agents_txt_readonly"><![CDATA[View only]]></string>
     <string name="_sys_agents_agents_popup_add"><![CDATA[Add agent]]></string>
     <string name="_sys_agents_agents_empty"><![CDATA[No agents yet]]></string>
     <string name="_sys_agents_agents_empty_info"><![CDATA[Agents run automated tasks on your site. Add one to get started.]]></string>
@@ -2423,10 +2411,6 @@
 
     <string name="_Run"><![CDATA[Run]]></string>
 
-    <string name="_sys_agents_automators_btn_add"><![CDATA[Add]]></string>
-    <string name="_sys_agents_automators_btn_tune"><![CDATA[Tune]]></string>
-    <string name="_sys_agents_automators_btn_approve"><![CDATA[Approve]]></string>
-    <string name="_sys_agents_automators_popup_add"><![CDATA[Add automator]]></string>
     <string name="_sys_agents_count_today"><![CDATA[{0} calls today]]></string>
     <string name="_sys_agents_count_per_req"><![CDATA[{0} calls per request]]></string>
     <string name="_sys_agents_field_async"><![CDATA[Delayed execution]]></string>
@@ -2436,16 +2420,10 @@
     <string name="_sys_agents_field_name_info"><![CDATA[Uniq agent name]]></string>
     <string name="_sys_agents_field_model_id"><![CDATA[Model]]></string>
     <string name="_sys_agents_field_model_id_info"><![CDATA[AI model to use for this agent.]]></string>
-    <string name="_sys_agents_automators_field_profile_id"><![CDATA[Profile]]></string>
-    <string name="_sys_agents_automators_field_profile_id_inf"><![CDATA[Leave empty to use default.]]></string>
     <string name="_sys_agents_field_profile_id"><![CDATA[Profile]]></string>
     <string name="_sys_agents_field_profile_id_inf"><![CDATA[AI agent will perform actions under this profile]]></string>
     <string name="_sys_agents_field_acl_levels"><![CDATA[Who can interact]]></string>
     <string name="_sys_agents_field_acl_levels_info"><![CDATA[Membership levels that may chat with this agent, use it in messenger, or fill form fields with it. Leave empty for Nobody. Studio operators can always use the agent to debug. Unauthenticated allows guests.]]></string>
-    <string name="_sys_agents_automators_field_type"><![CDATA[Type]]></string>
-    <string name="_sys_agents_automators_field_type_event"><![CDATA[Event]]></string>
-    <string name="_sys_agents_automators_field_type_scheduler"><![CDATA[Scheduler]]></string>
-    <string name="_sys_agents_automators_field_type_webhook"><![CDATA[Webhook]]></string>
     <string name="_sys_agents_field_trigger"><![CDATA[Trigger]]></string>
     <string name="_sys_agents_field_trigger_info"><![CDATA[Trigger which will initiate agent to run. Alert — system event notification. Scheduler — time-based scheduled execution. Webhook — external HTTP trigger. Manual — user-initiated run. Agent — triggered by another agent. Message — triggered by chat message. Form field — compose form field value.]]></string>
     <string name="_sys_agents_field_trigger_alert"><![CDATA[Alert]]></string>
@@ -2458,15 +2436,37 @@
     <string name="_sys_agents_field_alert_sample"><![CDATA[Alert: payload]]></string>
     <string name="_sys_agents_waiting_for_sample_data_alert"><![CDATA[Please select an alert to see its payload.]]></string>
     <string name="_sys_agents_waiting_for_sample_data_webhook"><![CDATA[Trigger webhook to see its payload.]]></string>
+    <string name="_sys_agents_block_header_alert"><![CDATA[Trigger - alert]]></string>
     <string name="_sys_agents_field_alert"><![CDATA[Alert: event name]]></string>
     <string name="_sys_agents_field_alert_info"><![CDATA[Event that triggers the agent. Usually starts with system or module_name. Only events that have occurred at least once are shown. Second column shows alert stats, if 'n/a' then alert haven't happend yet. If 'n/a' everywhere then stats option is disabled in Developer module.]]></string>
+    <string name="_sys_agents_block_header_scheduler"><![CDATA[Trigger - scheduler]]></string>
     <string name="_sys_agents_field_scheduler_cron"><![CDATA[Scheduler (CRON)]]></string>
     <string name="_sys_agents_field_scheduler_cron_info"><![CDATA[CRON expression for scheduling the agent.]]></string>
-    <string name="_sys_agents_field_webhook_key"><![CDATA[Webhook: key]]></string>
+    <string name="_sys_agents_block_header_webhook"><![CDATA[Trigger - webhook]]></string>
+    <string name="_sys_agents_field_webhook_key"><![CDATA[Key]]></string>
     <string name="_sys_agents_field_webhook_key_info"><![CDATA[Unique key for webhook endpoint, used as Bearer Authentication header.]]></string>
-    <string name="_sys_agents_field_webhook_sample"><![CDATA[Webhook: sample payload]]></string>
-    <string name="_sys_agents_field_message_profile"><![CDATA[Message: profile for communication with agent]]></string>
+    <string name="_sys_agents_field_webhook_sample"><![CDATA[Sample payload]]></string>
+    <string name="_sys_agents_block_header_message"><![CDATA[Trigger - message]]></string>
+    <string name="_sys_agents_field_message_profile"><![CDATA[Profile for communication with agent]]></string>
     <string name="_sys_agents_field_message_profile_info"><![CDATA[Profile to use for sending messages to the agent, messages from other users are ignored.]]></string>
+    <string name="_sys_agents_block_header_form_input"><![CDATA[Trigger - form input]]></string>
+    <string name="_sys_agents_block_header_manual"><![CDATA[Trigger - manual]]></string>
+    <string name="_sys_agents_field_manual_max_turns"><![CDATA[Max turns]]></string>
+    <string name="_sys_agents_field_manual_max_turns_info"><![CDATA[Hard limit on visitor messages in one session. When reached, the chat shows the limit message and stops accepting input. Set to '0' for no limit.]]></string>
+    <string name="_sys_agents_field_manual_chat_ttl_min"><![CDATA[Chat TTL (minutes)]]></string>
+    <string name="_sys_agents_field_manual_chat_ttl_min_info"><![CDATA[0 = no time limit. After this idle time the conversation is closed.]]></string>
+    <string name="_sys_agents_field_manual_limit_message"><![CDATA[Limit message]]></string>
+    <string name="_sys_agents_field_manual_limit_message_info"><![CDATA[Shown when the session hits max turns. The visitor cannot send another message after this.]]></string>
+    <string name="_sys_agents_field_manual_max_input_chars"><![CDATA[Max input characters]]></string>
+    <string name="_sys_agents_field_manual_max_input_chars_info"><![CDATA[Maximum number of characters allowed in a single input message. Set to '0' for no limit.]]></string>
+    <string name="_sys_agents_field_manual_max_tokens"><![CDATA[Max tokens]]></string>
+    <string name="_sys_agents_field_manual_max_tokens_info"><![CDATA[Ceiling on one model reply, passed to the API as max_tokens. Set to '0' for no limit.]]></string>
+    <string name="_sys_agents_field_manual_max_sessions_per_hour"><![CDATA[Max sessions per hour]]></string>
+    <string name="_sys_agents_field_manual_max_sessions_per_hour_info"><![CDATA[New chat threads from one IP per hour. Set to '0' for no limit.]]></string>
+    <string name="_sys_agents_field_manual_max_sessions_per_day"><![CDATA[Max sessions per day]]></string>
+    <string name="_sys_agents_field_manual_max_sessions_per_day_info"><![CDATA[New chat threads from one IP per day. Set to '0' for no limit.]]></string>
+    <string name="_sys_agents_field_manual_hidden_first_message"><![CDATA[Hidden first message]]></string>
+    <string name="_sys_agents_field_manual_hidden_first_message_info"><![CDATA[Empty = do not send. A name is a query key (org = ?org=). Prefixes: query:, value:, data:.]]></string>
     <string name="_sys_agents_field_vector_store_id"><![CDATA[Knowledge]]></string>
     <string name="_sys_agents_field_vector_store_id_info"><![CDATA[Vector store for knowledge retrieval.]]></string>
     <string name="_sys_agents_field_tools"><![CDATA[Tools]]></string>
@@ -2492,68 +2492,10 @@
     <string name="_sys_agents_tools_field_params_info"><![CDATA[Parameters for configuring the tool, specific to each tool]]></string>
     <string name="_sys_agents_field_chat_history_context"><![CDATA[Chat history context window]]></string>
     <string name="_sys_agents_field_chat_history_context_info"><![CDATA[Automatically cut the conversation history based on the size of the context window, set to '0' to disable chat history]]></string>
-    <string name="_sys_agents_automators_field_providers"><![CDATA[Providers]]></string>
-    <string name="_sys_agents_automators_field_providers_add"><![CDATA[Add]]></string>
-    <string name="_sys_agents_automators_field_helpers"><![CDATA[Helpers]]></string>
-    <string name="_sys_agents_automators_field_helpers_add"><![CDATA[Add]]></string>
-    <string name="_sys_agents_automators_field_assistants"><![CDATA[Assistants]]></string>
-    <string name="_sys_agents_automators_field_assistants_add"><![CDATA[Add]]></string>
-    <string name="_sys_agents_automators_field_message"><![CDATA[Prompt]]></string>
-    <string name="_sys_agents_automators_field_message_err"><![CDATA[Prompt is essential. Please type something here.]]></string>
-    <string name="_sys_agents_automators_field_status_auto"><![CDATA[Auto]]></string>
-    <string name="_sys_agents_automators_field_status_manual"><![CDATA[Manual]]></string>
-    <string name="_sys_agents_automators_field_status_ready"><![CDATA[Ready]]></string>
-    <string name="_sys_agents_automators_err_event_not_found"><![CDATA[Event isn't found, please rephrase your prompt.]]></string>
-    <string name="_sys_agents_automators_err_cannot_get_code"><![CDATA[Can't generate code, please rephrase your prompt.]]></string>
-    <string name="_sys_agents_automators_txt_eval_response"><![CDATA[Response:<br />]]></string>
-    <string name="_sys_agents_providers_txt_active"><![CDATA[Active]]></string>
-    <string name="_sys_agents_providers_txt_provider_name"><![CDATA[Name]]></string>
-    <string name="_sys_agents_providers_txt_provider_type"><![CDATA[Type]]></string>
-    <string name="_sys_agents_providers_txt_added"><![CDATA[Added]]></string>
-    <string name="_sys_agents_providers_btn_add"><![CDATA[Add Provider]]></string>
-    <string name="_sys_agents_providers_btn_info"><![CDATA[Info]]></string>
-    <string name="_sys_agents_providers_btn_edit"><![CDATA[Edit]]></string>
-    <string name="_sys_agents_providers_btn_delete"><![CDATA[Delete]]></string>
-    <string name="_sys_agents_providers_popup_add"><![CDATA[Add provider]]></string>
-    <string name="_sys_agents_providers_popup_edit"><![CDATA[Edit provider]]></string>
-    <string name="_sys_agents_providers_popup_view"><![CDATA[View provider]]></string>
-    <string name="_sys_agents_providers_field_type_id"><![CDATA[Type]]></string>
-    <string name="_sys_agents_providers_field_type_id_err"><![CDATA[Type is essential. Please select a value.]]></string>
-    <string name="_sys_agents_providers_field_name"><![CDATA[Name]]></string>        
-    <string name="_sys_agents_pvd_cpt_shopify_admin"><![CDATA[Shopify (Admin API)]]></string>
-    <string name="_sys_agents_pvd_opt_cpt_active"><![CDATA[Active]]></string>
-    <string name="_sys_agents_pvd_opt_dsc_active"><![CDATA[Activate(Deactivate) source]]></string>
-    <string name="_sys_agents_pvd_opt_cpt_shop_domain"><![CDATA[Shop domain]]></string>
-    <string name="_sys_agents_pvd_opt_dsc_shop_domain"><![CDATA[]]></string>
-    <string name="_sys_agents_pvd_opt_cpt_access_token"><![CDATA[Access token]]></string>
-    <string name="_sys_agents_pvd_opt_dsc_access_token"><![CDATA[]]></string>
-    <string name="_sys_agents_pvd_opt_cpt_secret_key"><![CDATA[Secret key]]></string>
-    <string name="_sys_agents_pvd_opt_dsc_secret_key"><![CDATA[]]></string>
-    <string name="_sys_agents_pvd_opt_cpt_webhook_url"><![CDATA[Webhook URL]]></string>
-    <string name="_sys_agents_pvd_opt_dsc_webhook_url"><![CDATA[]]></string>
-    <string name="_sys_agents_helpers_txt_active"><![CDATA[Active]]></string>
-    <string name="_sys_agents_helpers_txt_name"><![CDATA[Name]]></string>
-    <string name="_sys_agents_helpers_txt_model_id"><![CDATA[Model]]></string>
-    <string name="_sys_agents_helpers_txt_profile_id"><![CDATA[Profile]]></string>
-    <string name="_sys_agents_helpers_txt_prompt"><![CDATA[Prompt]]></string>
-    <string name="_sys_agents_helpers_txt_added"><![CDATA[Added]]></string>
-    <string name="_sys_agents_helpers_btn_add"><![CDATA[Add Helper]]></string>
-    <string name="_sys_agents_helpers_btn_tune"><![CDATA[Tune]]></string>
-    <string name="_sys_agents_helpers_field_name"><![CDATA[Name]]></string>
-    <string name="_sys_agents_helpers_field_model_id"><![CDATA[Model]]></string>
-    <string name="_sys_agents_helpers_field_profile_id"><![CDATA[Profile]]></string>
-    <string name="_sys_agents_helpers_field_profile_id_inf"><![CDATA[Leave empty to use default.]]></string>
-    <string name="_sys_agents_helpers_field_description"><![CDATA[Description]]></string>
-    <string name="_sys_agents_helpers_field_prompt"><![CDATA[Prompt]]></string>
-    <string name="_sys_agents_helpers_field_prompt_err"><![CDATA[Prompt is essential. Please type something here.]]></string>
-    <string name="_sys_agents_helpers_field_message"><![CDATA[Message]]></string>
-    <string name="_sys_agents_helpers_field_message_err"><![CDATA[Message is essential. Please type something here.]]></string>
-    <string name="_sys_agents_helpers_field_result"><![CDATA[Result]]></string>
-    <string name="_sys_agents_helpers_popup_add"><![CDATA[Add helper]]></string>
-    <string name="_sys_agents_helpers_popup_edit"><![CDATA[Edit helper]]></string>
     <string name="_sys_agents_models_txt_active"><![CDATA[Active]]></string>
     <string name="_sys_agents_models_txt_name"><![CDATA[Name]]></string>
     <string name="_sys_agents_models_txt_title"><![CDATA[Title]]></string>
+    <string name="_sys_agents_models_txt_type"><![CDATA[Type]]></string>
     <string name="_sys_agents_models_txt_key"><![CDATA[API Key]]></string>
     <string name="_sys_agents_models_txt_params"><![CDATA[Parameters]]></string>
     <string name="_sys_agents_models_txt_added"><![CDATA[Added]]></string>
@@ -2594,68 +2536,9 @@
     <string name="_sys_agents_vector_store_field_files_info"><![CDATA[Data files to upload to the vector store.]]></string>
     <string name="_sys_agents_vector_store_data_queued"><![CDATA[Data queued for vector store processing]]></string>
     
-    <string name="_sys_agents_assistants_txt_active"><![CDATA[Active]]></string>
-    <string name="_sys_agents_assistants_txt_name"><![CDATA[Name]]></string>
-    <string name="_sys_agents_assistants_txt_model_id"><![CDATA[Model]]></string>
-    <string name="_sys_agents_assistants_txt_profile_id"><![CDATA[Profile]]></string>
-    <string name="_sys_agents_assistants_txt_prompt"><![CDATA[Prompt]]></string>
-    <string name="_sys_agents_assistants_txt_added"><![CDATA[Added]]></string>
-    <string name="_sys_agents_assistants_txt_assistant"><![CDATA[Assistant]]></string>
-    <string name="_sys_agents_assistants_txt_codes_onclick"><![CDATA[onClick code for a link]]></string>
-    <string name="_sys_agents_assistants_txt_codes_button"><![CDATA[Button HTML code]]></string>
-    <string name="_sys_agents_assistants_txt_add_knowledge"><![CDATA[Add knowledge]]></string>
-    <string name="_sys_agents_assistants_txt_ask"><![CDATA[Ask]]></string>
-    <string name="_sys_agents_assistants_btn_add"><![CDATA[Add Assistant]]></string>
-    <string name="_sys_agents_assistants_btn_chats"><![CDATA[Chats]]></string>
-    <string name="_sys_agents_assistants_btn_files"><![CDATA[Files]]></string>
-    <string name="_sys_agents_assistants_btn_codes"><![CDATA[Get codes]]></string>
-    <string name="_sys_agents_assistants_field_name"><![CDATA[Name]]></string>
-    <string name="_sys_agents_assistants_field_model_id"><![CDATA[Model]]></string>
-    <string name="_sys_agents_assistants_field_profile_id"><![CDATA[Profile]]></string>
-    <string name="_sys_agents_assistants_field_profile_id_inf"><![CDATA[Leave empty to use default.]]></string>
-    <string name="_sys_agents_assistants_field_description"><![CDATA[Description]]></string>
-    <string name="_sys_agents_assistants_field_prompt"><![CDATA[Prompt]]></string>
-    <string name="_sys_agents_assistants_field_prompt_err"><![CDATA[Prompt is essential. Please type something here.]]></string>
-    <string name="_sys_agents_assistants_field_result"><![CDATA[Result]]></string>
-    <string name="_sys_agents_assistants_popup_add"><![CDATA[Add assistant]]></string>
-    <string name="_sys_agents_assistants_popup_edit"><![CDATA[Edit assistant]]></string>
-    <string name="_sys_agents_assistants_chat_dsc_studio"><![CDATA[Studio assistant]]></string>
-    <string name="_sys_agents_assistants_chat_dsc_block"><![CDATA['Ask AI' assistant]]></string>
-    <string name="_sys_agents_assistants_chat_dsc_live_search"><![CDATA[Live search assistant]]></string>
-    <string name="_sys_agents_assistants_chats_txt_name"><![CDATA[Name]]></string>
-    <string name="_sys_agents_assistants_chats_txt_type"><![CDATA[Type]]></string>
-    <string name="_sys_agents_assistants_chats_txt_type_1"><![CDATA[Permanent]]></string>
-    <string name="_sys_agents_assistants_chats_txt_type_10"><![CDATA[Transient]]></string>
-    <string name="_sys_agents_assistants_chats_txt_description"><![CDATA[Description]]></string>
-    <string name="_sys_agents_assistants_chats_txt_messages"><![CDATA[Messages]]></string>
-    <string name="_sys_agents_assistants_chats_txt_added"><![CDATA[Added]]></string>
-    <string name="_sys_agents_assistants_chats_txt_stored"><![CDATA[Stored]]></string>
-    <string name="_sys_agents_assistants_chats_txt_chat"><![CDATA[Chat]]></string>
-    <string name="_sys_agents_assistants_chats_btn_add"><![CDATA[Add Chat]]></string>
-    <string name="_sys_agents_assistants_chats_btn_chat"><![CDATA[Chat]]></string>
-    <string name="_sys_agents_assistants_chats_btn_store"><![CDATA[Store for future usage]]></string>
-    <string name="_sys_agents_assistants_chats_btn_unstore"><![CDATA[Remove from store]]></string>
     <string name="_sys_agents_agents_act_wipe_chat_history"><![CDATA[Wipe chat history]]></string>
     <string name="_sys_agents_agents_act_message"><![CDATA[Message]]></string>
-    <string name="_sys_agents_assistants_chats_field_name"><![CDATA[Name]]></string>
-    <string name="_sys_agents_assistants_chats_field_description"><![CDATA[Description]]></string>
-    <string name="_sys_agents_assistants_chats_popup_add"><![CDATA[Add chat]]></string>
-    <string name="_sys_agents_assistants_chats_popup_edit"><![CDATA[Edit chat]]></string>
     <string name="_sys_agents_assistants_chats_msg_loading"><![CDATA[I'm thinking. Please wait.]]></string>
-    <string name="_sys_agents_assistants_chats_err_no_response"><![CDATA[Cannot get response. Check log file for more info.]]></string>
-    <string name="_sys_agents_assistants_files_txt_name"><![CDATA[Name]]></string>
-    <string name="_sys_agents_assistants_files_txt_size"><![CDATA[Size]]></string>
-    <string name="_sys_agents_assistants_files_txt_status"><![CDATA[Status]]></string>
-    <string name="_sys_agents_assistants_files_txt_status_in_progress"><![CDATA[In progress]]></string>
-    <string name="_sys_agents_assistants_files_txt_status_completed"><![CDATA[Completed]]></string>
-    <string name="_sys_agents_assistants_files_txt_status_failed"><![CDATA[Failed]]></string>
-    <string name="_sys_agents_assistants_files_txt_status_cancelled"><![CDATA[Cancelled]]></string>
-    <string name="_sys_agents_assistants_files_txt_added"><![CDATA[Added]]></string>
-    <string name="_sys_agents_assistants_files_btn_add"><![CDATA[Add File]]></string>
-    <string name="_sys_agents_assistants_files_btn_sync"><![CDATA[Sync]]></string>
-    <string name="_sys_agents_assistants_files_btn_delete"><![CDATA[Delete file]]></string>
-    <string name="_sys_agents_assistants_files_field_file"><![CDATA[File]]></string>
-    <string name="_sys_agents_assistants_files_popup_add"><![CDATA[Add File]]></string>
     <string name="_sys_agents_assistants_field_kwg_type"><![CDATA[Type]]></string>
     <string name="_sys_agents_assistants_field_kwg_type_text"><![CDATA[Text]]></string>
     <string name="_sys_agents_assistants_field_kwg_type_faq"><![CDATA[Question with answer]]></string>
@@ -3087,17 +2970,6 @@
         <string name="_sys_form_account_input_publish"><![CDATA[Create new account]]></string>
         <string name="_sys_form_account_input_caption_system_picture"><![CDATA[Account Picture]]></string>
         <string name="_sys_form_account_input_picture"><![CDATA[Upload an account picture]]></string>
-        <string name="_sys_form_agents_comment"><![CDATA[Agents: Message]]></string>
-        <string name="_sys_form_display_agents_comment_post"><![CDATA[Agents: Message Send]]></string>
-        <string name="_sys_form_agents_comment_input_caption_system_action"><![CDATA[Action]]></string>
-        <string name="_sys_form_agents_comment_input_caption_system_cmt_image"><![CDATA[Image]]></string>
-	<string name="_sys_form_agents_comment_input_caption_system_cmt_parent_id"><![CDATA[Parent ID]]></string>
-	<string name="_sys_form_agents_comment_input_caption_system_cmt_submit"><![CDATA[Send]]></string>
-        <string name="_sys_form_agents_comment_input_caption_cmt_submit"><![CDATA[Send]]></string>
-	<string name="_sys_form_agents_comment_input_caption_system_cmt_text"><![CDATA[Text]]></string>
-        <string name="_sys_form_agents_comment_input_caption_cmt_text_error"><![CDATA[Empty message isn't allowed. Write something to send to assistant.]]></string>
-        <string name="_sys_form_agents_comment_input_caption_system_id"><![CDATA[Object ID]]></string>
-        <string name="_sys_form_agents_comment_input_caption_system_sys"><![CDATA[Object Type]]></string>
 	<string name="_sys_form_checkbox_value_off"><![CDATA[No]]></string>
 	<string name="_sys_form_checkbox_value_on"><![CDATA[Yes]]></string>
         <string name="_sys_form_input_caption_author"><![CDATA[Author]]></string>
@@ -3976,8 +3848,6 @@
     <string name="_sys_page_block_title_create_account"><![CDATA[Sign up]]></string>
     <string name="_sys_page_block_title_sys_author"><![CDATA[Author]]></string>
     <string name="_sys_page_block_title_author"><![CDATA[Author]]></string>
-    <string name="_sys_page_block_title_sys_ask_aqssistant"><![CDATA[Ask AI]]></string>
-    <string name="_sys_page_block_title_ask_aqssistant"><![CDATA[Ask AI]]></string>
     <string name="_sys_page_block_title_sys_ai_agent"><![CDATA[AI Agent]]></string>
     <string name="_sys_page_block_title_ai_agent"><![CDATA[AI Agent]]></string>
     <string name="_sys_page_block_title_con_friends"><![CDATA[Friends]]></string>
@@ -4297,7 +4167,6 @@
     <string name="_sys_select"><![CDATA[Select]]></string>
     <string name="_sys_send"><![CDATA[Send]]></string>
     <string name="_sys_search_placeholder"><![CDATA[Search...]]></string>
-    <string name="_sys_search_or_ask_placeholder"><![CDATA[Search or ask...]]></string>
     <string name="_sys_show_less"><![CDATA[Show less]]></string>    
     <string name="_sys_show_more"><![CDATA[Show more]]></string>
     <string name="_sys_skip_to_main_content"><![CDATA[Skip to main content]]></string>

+ 1 - 1
modules/boonex/notifications/classes/BxNtfsTemplate.php

@@ -264,7 +264,7 @@ class BxNtfsTemplate extends BxBaseModNotificationsTemplate
         if($bEventCanceled)
             return '';
 
-        if(($sGroupedByMac = $aEvent['grouped_by_mac'] ?? false)) {
+        if(($sGroupedByMac = $aEvent['grouped_by_mac'] ?? false) && strpos($sGroupedByMac, ',') !== false) {
             $aGroupedByMac = explode(',', $sGroupedByMac);
 
             $sContentParsed = '';

+ 1 - 1
modules/boonex/notifications/install/sql/enable.sql

@@ -110,7 +110,7 @@ INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `info`, `t
 ('bx_notifications_summary_chars', '200', @iCategId, '_bx_ntfs_option_summary_chars', '', 'digit', '', '', '', '', 50),
 ('bx_notifications_owner_name_chars', '21', @iCategId, '_bx_ntfs_option_owner_name_chars', '', 'digit', '', '', '', '', 51),
 ('bx_notifications_content_chars', '32', @iCategId, '_bx_ntfs_option_content_chars', '', 'digit', '', '', '', '', 52),
-('bx_notifications_email_subject_chars', '30', @iCategId, '_bx_ntfs_option_email_subject_chars', '', 'digit', '', '', '', '', 53),
+('bx_notifications_email_subject_chars', '90', @iCategId, '_bx_ntfs_option_email_subject_chars', '', 'digit', '', '', '', '', 53),
 ('bx_notifications_push_message_chars', '190', @iCategId, '_bx_ntfs_option_push_message_chars', '', 'digit', '', '', '', '', 54);
 
 

+ 33 - 164
modules/boonex/russian/data/langs/system/ru.xml

@@ -1950,9 +1950,6 @@
     <string name="_adm_stg_cpt_option_sys_agents_inspector_key"><![CDATA[Ключ Inspector]]></string>
     <string name="_adm_stg_cpt_option_sys_agents_inspector_key_info"><![CDATA[Получить ключ на https://app.inspector.dev/]]></string>
     <string name="_adm_stg_cpt_option_sys_agents_asst_chats_trans_del"><![CDATA[Автоматическое удаление временных чатов каждый день]]></string>
-    <string name="_adm_stg_cpt_option_sys_agents_sa"><![CDATA[Помощник по студии]]></string>
-    <string name="_adm_stg_cpt_option_sys_agents_lsa"><![CDATA[Помощник для интерактивного поиска]]></string>
-    <string name="_adm_stg_cpt_option_sys_agents_aba"><![CDATA[Помощник для блока "Спросите ИИ"]]></string>
     <string name="_adm_stg_cpt_option_sys_viewport_meta_tag"><![CDATA[Метатег области просмотра]]></string>
 
     <string name="_adm_stg_cpt_category_pwa_manifest"><![CDATA[Manifest]]></string>
@@ -2369,23 +2366,14 @@
         <string name="_sys_agents_json_field_err"><![CDATA[Неверный формат JSON.]]></string>
         <string name="_sys_agents_unauthorized"><![CDATA[Вам не разрешено взаимодействовать с этим агентом.]]></string>
         <string name="_sys_agents_agent_not_found"><![CDATA[Агент не найден или неактивен.]]></string>
-        <string name="_sys_agents_automators_txt_active"><![CDATA[Активный]]></string>
-        <string name="_sys_agents_automators_txt_name"><![CDATA[Название]]></string>
-        <string name="_sys_agents_automators_txt_type"><![CDATA[Тип]]></string>
-        <string name="_sys_agents_automators_txt_alert_unit"><![CDATA[Тип события]]></string>
-        <string name="_sys_agents_automators_txt_alert_action"><![CDATA[Alert - действие события]]></string>
-        <string name="_sys_agents_automators_txt_scheduler_time"><![CDATA[Время для планировщика]]></string>
-        <string name="_sys_agents_automators_txt_model_id"><![CDATA[Модель]]></string>
-        <string name="_sys_agents_automators_txt_profile_id"><![CDATA[Профиль]]></string>
-        <string name="_sys_agents_automators_txt_message_id"><![CDATA[Инструкции]]></string>
-        <string name="_sys_agents_automators_txt_messages"><![CDATA[Сообщения]]></string>
-        <string name="_sys_agents_automators_txt_code"><![CDATA[Код]]></string>
-        <string name="_sys_agents_automators_txt_added"><![CDATA[Добавлено]]></string>
-        <string name="_sys_agents_automators_txt_status"><![CDATA[Статус]]></string>
         <string name="_sys_agents_agents_txt_name"><![CDATA[Название]]></string>
         <string name="_sys_agents_agents_txt_model"><![CDATA[Модель]]></string>
         <string name="_sys_agents_agents_txt_trigger"><![CDATA[Триггер]]></string>
         <string name="_sys_agents_agents_txt_profile_id"><![CDATA[Профиль]]></string>
+        <string name="_sys_agents_agents_txt_chats"><![CDATA[Чаты]]></string>
+        <string name="_sys_agents_agents_txt_my_chat"><![CDATA[Мой чат]]></string>
+        <string name="_sys_agents_agents_txt_guest_chat"><![CDATA[Гость]]></string>
+        <string name="_sys_agents_agents_txt_readonly"><![CDATA[Только просмотр]]></string>
         <string name="_sys_agents_agents_popup_add"><![CDATA[Добавить агента]]></string>
         <string name="_sys_agents_agents_empty"><![CDATA[Пока нет агентов]]></string>
         <string name="_sys_agents_agents_empty_info"><![CDATA[Агенты выполняют автоматические задачи на сайте. Добавьте первого, чтобы начать.]]></string>
@@ -2394,10 +2382,6 @@
 
         <string name="_Run"><![CDATA[Запустить]]></string>
 
-        <string name="_sys_agents_automators_btn_add"><![CDATA[Добавить]]></string>
-        <string name="_sys_agents_automators_btn_tune"><![CDATA[Настройка]]></string>
-        <string name="_sys_agents_automators_btn_approve"><![CDATA[Одобрить]]></string>
-        <string name="_sys_agents_automators_popup_add"><![CDATA[Добавить автоматизатор]]></string>
         <string name="_sys_agents_count_today"><![CDATA[{0} вызовов сегодня]]></string>
         <string name="_sys_agents_count_per_req"><![CDATA[{0} вызовов на запрос]]></string>
         <string name="_sys_agents_popup_edit"><![CDATA[Редактировать автоматизатор]]></string>
@@ -2405,16 +2389,10 @@
         <string name="_sys_agents_field_async_inf"><![CDATA[Запустить агента позже, чтобы не блокировать/замедлять выполнение основного кода. Не применяется для 'manual' и 'scheduler'. Для 'alert' — переопределение значений не сработает. Для 'messenger' — сообщения не появятся автоматически.]]></string>
         <string name="_sys_agents_field_model_id"><![CDATA[Модель]]></string>
         <string name="_sys_agents_field_model_id_info"><![CDATA[Модель для использования этим агентом.]]></string>
-        <string name="_sys_agents_automators_field_profile_id"><![CDATA[Профиль]]></string>
-        <string name="_sys_agents_automators_field_profile_id_inf"><![CDATA[Оставьте пустым, чтобы использовать значение по умолчанию.]]></string>
         <string name="_sys_agents_field_profile_id"><![CDATA[Профиль]]></string>
         <string name="_sys_agents_field_profile_id_inf"><![CDATA[Профайл, от имени которого ИИ агент выполняет действия.]]></string>
         <string name="_sys_agents_field_acl_levels"><![CDATA[Кто может взаимодействовать]]></string>
         <string name="_sys_agents_field_acl_levels_info"><![CDATA[Уровни членства, которые могут общаться с этим агентом, писать ему в мессенджере или заполнять поля форм с его помощью. Оставьте пустым — никто. Операторы Studio всегда могут использовать агента для отладки. «Неаутентифицированный» разрешает гостям.]]></string>
-        <string name="_sys_agents_automators_field_type"><![CDATA[Тип]]></string>
-        <string name="_sys_agents_automators_field_type_event"><![CDATA[Событие]]></string>
-        <string name="_sys_agents_automators_field_type_scheduler"><![CDATA[Планировщик]]></string>
-        <string name="_sys_agents_automators_field_type_webhook"><![CDATA[Вебхук]]></string>
         <string name="_sys_agents_field_trigger"><![CDATA[Триггер]]></string>
         <string name="_sys_agents_field_trigger_info"><![CDATA[Триггер, который запускает агента. Alert — системное событие. Scheduler — запуск по расписанию. Webhook — HTTP внешний вызов. Manual — ручной запуск. Agent — запуск другим агентом. Message — сообщение пользователя.]]></string>
         <string name="_sys_agents_field_trigger_alert"><![CDATA[Сигнал]]></string>
@@ -2427,16 +2405,38 @@
         <string name="_sys_agents_field_alert_sample"><![CDATA[Alert - пример данных]]></string>
         <string name="_sys_agents_waiting_for_sample_data_alert"><![CDATA[Пожалуйста, выберите алерт, чтобы увидеть его данные.]]></string>
         <string name="_sys_agents_waiting_for_sample_data_webhook"><![CDATA[Пожалуйста, активируйте вебхук, чтобы увидеть его данные.]]></string>       
-        <string name="_sys_agents_field_alert"><![CDATA[Alert - событие]]></string>
+        <string name="_sys_agents_block_header_alert"><![CDATA[Триггер - Событие]]></string>
+        <string name="_sys_agents_field_alert"><![CDATA[Событие]]></string>
         <string name="_sys_agents_field_alert_info"><![CDATA[Событие, запускающее агента. Обычно начинается с system или module_name. Показаны только уже произошедшие события. Во втором столбце отображается статистика уведомлений. Если указано «н/д», значит соответствующее уведомление ещё не срабатывало. Если «н/д» указано везде, то опция статистики уведомлений отключена в модуле «Developer».]]></string>
-        <string name="_sys_agents_field_alert_action"><![CDATA[Alert - действие события]]></string>
-        <string name="_sys_agents_field_scheduler_cron"><![CDATA[Scheduler - Планировщик (CRON)]]></string>
-        <string name="_sys_agents_field_scheduler_cron_info"><![CDATA[Scheduler - CRON строка для планирования агента.]]></string>
+        <string name="_sys_agents_field_alert_action"><![CDATA[Действие события]]></string>
+        <string name="_sys_agents_block_header_scheduler"><![CDATA[Триггер - Планировщик]]></string>
+        <string name="_sys_agents_field_scheduler_cron"><![CDATA[Планировщик (CRON)]]></string>
+        <string name="_sys_agents_field_scheduler_cron_info"><![CDATA[CRON строка для планирования агента.]]></string>
+        <string name="_sys_agents_block_header_webhook"><![CDATA[Триггер - webhook]]></string>
         <string name="_sys_agents_field_webhook_key"><![CDATA[Webhook: ключ]]></string>
         <string name="_sys_agents_field_webhook_key_info"><![CDATA[Уникальный секретный ключ для вебхука, используется для заголовка Bearer Authentication]]></string>
         <string name="_sys_agents_field_webhook_sample"><![CDATA[Webhook: пример данных]]></string>
-        <string name="_sys_agents_field_message_profile"><![CDATA[Message - профиль для отправки сообщений]]></string>
+        <string name="_sys_agents_block_header_message"><![CDATA[Триггер - Сообщение]]></string>
+        <string name="_sys_agents_field_message_profile"><![CDATA[Профиль для отправки сообщений]]></string>
         <string name="_sys_agents_field_message_profile_info"><![CDATA[Профиль для для отправке сообщений агенту, сообщения от других пользователей игногируются.]]></string>
+        <string name="_sys_agents_block_header_form_input"><![CDATA[Триггер - ввод данных в форму]]></string>
+        <string name="_sys_agents_block_header_manual"><![CDATA[Триггер - ручной запуск]]></string>
+        <string name="_sys_agents_field_manual_max_turns"><![CDATA[Максимальное количество ходов]]></string>
+        <string name="_sys_agents_field_manual_max_turns_info"><![CDATA[Жесткий лимит на сообщения посетителя в одной сессии. Когда он достигнут, чат показывает сообщение о достижении лимита и прекращает принимать ввод. Установите '0' для отсутствия лимита.]]></string>
+        <string name="_sys_agents_field_manual_chat_ttl_min"><![CDATA[Время жизни чата (минуты)]]></string>
+        <string name="_sys_agents_field_manual_chat_ttl_min_info"><![CDATA[0 = нет ограничения по времени. После этого времени бездействия разговор закрывается.]]></string>
+        <string name="_sys_agents_field_manual_limit_message"><![CDATA[Сообщение о достижении лимита]]></string>
+        <string name="_sys_agents_field_manual_limit_message_info"><![CDATA[Отображается, когда сессия достигает максимального количества ходов. Посетитель не может отправить еще одно сообщение после этого.]]></string>
+        <string name="_sys_agents_field_manual_max_input_chars"><![CDATA[Максимальное количество символов ввода]]></string>
+        <string name="_sys_agents_field_manual_max_input_chars_info"><![CDATA[Максимальное количество символов, разрешенных в одном сообщении ввода. Установите '0' для отсутствия лимита.]]></string>
+        <string name="_sys_agents_field_manual_max_tokens"><![CDATA[Максимальное количество токенов]]></string>
+        <string name="_sys_agents_field_manual_max_tokens_info"><![CDATA[Потолок на одно ответ модели, передается в API как max_tokens. Установите '0' для отсутствия лимита.]]></string>
+        <string name="_sys_agents_field_manual_max_sessions_per_hour"><![CDATA[Максимальное количество сессий за час]]></string>
+        <string name="_sys_agents_field_manual_max_sessions_per_hour_info"><![CDATA[Новые темы чата от одного IP за час. Установите '0' для отсутствия лимита.]]></string>
+        <string name="_sys_agents_field_manual_max_sessions_per_day"><![CDATA[Максимальное количество сессий за день]]></string>
+        <string name="_sys_agents_field_manual_max_sessions_per_day_info"><![CDATA[Новые темы чата от одного IP за день. Установите '0' для отсутствия лимита.]]></string>
+        <string name="_sys_agents_field_manual_hidden_first_message"><![CDATA[Скрытое первое сообщение]]></string>
+        <string name="_sys_agents_field_manual_hidden_first_message_info"><![CDATA[Пусто = не отправлять. Имя является ключом запроса (org = ?org=). Префиксы: query:, value:, data:.]]></string>
         <string name="_sys_agents_field_vector_store_id"><![CDATA[Хранилище векторов]]></string>
         <string name="_sys_agents_field_vector_store_id_info"><![CDATA[Хранилище векторов для получения знаний.]]></string>
         <string name="_sys_agents_field_tools"><![CDATA[Инструменты]]></string>
@@ -2462,68 +2462,10 @@
         <string name="_sys_agents_tools_field_params_info"><![CDATA[Параметры для конфигурации этого инструмента, свои для каждого инструмента]]></string>
         <string name="_sys_agents_field_chat_history_context"><![CDATA[Размер контекста истории чата]]></string>
         <string name="_sys_agents_field_chat_history_context_info"><![CDATA[Автоматически обрезать историю диалога на основе размера контекста, 0 - отключить историю чата]]></string>
-        <string name="_sys_agents_automators_field_providers"><![CDATA[Поставщики]]></string>
-        <string name="_sys_agents_automators_field_providers_add"><![CDATA[Добавить]]></string>
-        <string name="_sys_agents_automators_field_helpers"><![CDATA[Помощники]]></string>
-        <string name="_sys_agents_automators_field_helpers_add"><![CDATA[Добавить]]></string>
-        <string name="_sys_agents_automators_field_assistants"><![CDATA[Ассистенты]]></string>
-        <string name="_sys_agents_automators_field_assistants_add"><![CDATA[Добавить]]></string>
-        <string name="_sys_agents_automators_field_message"><![CDATA[Инструкции]]></string>
-        <string name="_sys_agents_automators_field_message_err"><![CDATA[Инструкции имеют важное значение. Пожалуйста, напишите что-нибудь здесь.]]></string>
-        <string name="_sys_agents_automators_field_status_auto"><![CDATA[Авто]]></string>
-        <string name="_sys_agents_automators_field_status_manual"><![CDATA[Ручной]]></string>
-        <string name="_sys_agents_automators_field_status_ready"><![CDATA[Готовый]]></string>
-        <string name="_sys_agents_automators_err_event_not_found"><![CDATA[Событие не найдено. Перефразируйте запрос.]]></string>
-        <string name="_sys_agents_automators_err_cannot_get_code"><![CDATA[Невозможно сгенерировать код. Перефразируйте запрос.]]></string>
-        <string name="_sys_agents_automators_txt_eval_response"><![CDATA[Ответ:<br />]]></string>
-        <string name="_sys_agents_providers_txt_active"><![CDATA[Активный]]></string>
-        <string name="_sys_agents_providers_txt_provider_name"><![CDATA[Название]]></string>
-        <string name="_sys_agents_providers_txt_provider_type"><![CDATA[Тип]]></string>
-        <string name="_sys_agents_providers_txt_added"><![CDATA[Добавлено]]></string>
-        <string name="_sys_agents_providers_btn_add"><![CDATA[Добавить]]></string>
-        <string name="_sys_agents_providers_btn_info"><![CDATA[Инфо]]></string>
-        <string name="_sys_agents_providers_btn_edit"><![CDATA[Редактировать]]></string>
-        <string name="_sys_agents_providers_btn_delete"><![CDATA[Удалить]]></string>
-        <string name="_sys_agents_providers_popup_add"><![CDATA[Добавить поставщика]]></string>
-        <string name="_sys_agents_providers_popup_edit"><![CDATA[Редактировать поставщика]]></string>
-        <string name="_sys_agents_providers_popup_view"><![CDATA[Посмотреть поставщика]]></string>
-        <string name="_sys_agents_providers_field_type_id"><![CDATA[Тип]]></string>
-        <string name="_sys_agents_providers_field_type_id_err"><![CDATA[Тип имеет важное значение. Пожалуйста, выберите значение.]]></string>
-        <string name="_sys_agents_providers_field_name"><![CDATA[Название]]></string>
-        <string name="_sys_agents_pvd_cpt_shopify_admin"><![CDATA[Shopify (Admin API)]]></string>
-        <string name="_sys_agents_pvd_opt_cpt_active"><![CDATA[Активировать]]></string>
-        <string name="_sys_agents_pvd_opt_dsc_active"><![CDATA[Активировать(деактивировать) источник.]]></string>
-        <string name="_sys_agents_pvd_opt_cpt_shop_domain"><![CDATA[Домен магазина]]></string>
-        <string name="_sys_agents_pvd_opt_dsc_shop_domain"><![CDATA[]]></string>
-        <string name="_sys_agents_pvd_opt_cpt_access_token"><![CDATA[Токен доступа]]></string>
-        <string name="_sys_agents_pvd_opt_dsc_access_token"><![CDATA[]]></string>
-        <string name="_sys_agents_pvd_opt_cpt_secret_key"><![CDATA[Секретный ключ]]></string>
-        <string name="_sys_agents_pvd_opt_dsc_secret_key"><![CDATA[]]></string>
-        <string name="_sys_agents_pvd_opt_cpt_webhook_url"><![CDATA[URL вебхука]]></string>
-        <string name="_sys_agents_pvd_opt_dsc_webhook_url"><![CDATA[]]></string>
-        <string name="_sys_agents_helpers_txt_active"><![CDATA[Активный]]></string>
-        <string name="_sys_agents_helpers_txt_name"><![CDATA[Название]]></string>
-        <string name="_sys_agents_helpers_txt_model_id"><![CDATA[Модель]]></string>
-        <string name="_sys_agents_helpers_txt_profile_id"><![CDATA[Профиль]]></string>
-        <string name="_sys_agents_helpers_txt_prompt"><![CDATA[Инструкции]]></string>
-        <string name="_sys_agents_helpers_txt_added"><![CDATA[Добавлено]]></string>
-        <string name="_sys_agents_helpers_btn_add"><![CDATA[Добавить помощника]]></string>
-        <string name="_sys_agents_helpers_btn_tune"><![CDATA[Настройка]]></string>
-        <string name="_sys_agents_helpers_field_name"><![CDATA[Название]]></string>
-        <string name="_sys_agents_helpers_field_model_id"><![CDATA[Модель]]></string>
-        <string name="_sys_agents_helpers_field_profile_id"><![CDATA[Профиль]]></string>
-        <string name="_sys_agents_helpers_field_profile_id_inf"><![CDATA[Оставьте пустым, чтобы использовать значение по умолчанию.]]></string>
-        <string name="_sys_agents_helpers_field_description"><![CDATA[Описание]]></string>
-        <string name="_sys_agents_helpers_field_prompt"><![CDATA[Инструкции]]></string>
-        <string name="_sys_agents_helpers_field_prompt_err"><![CDATA[Инструкции имеют важное значение. Пожалуйста, напишите что-нибудь здесь.]]></string>
-        <string name="_sys_agents_helpers_field_message"><![CDATA[Cообщение]]></string>
-        <string name="_sys_agents_helpers_field_message_err"><![CDATA[Cообщение имеет важное значение. Пожалуйста, напишите что-нибудь здесь.]]></string>
-        <string name="_sys_agents_helpers_field_result"><![CDATA[Результат]]></string>
-        <string name="_sys_agents_helpers_popup_add"><![CDATA[Добавить помощника]]></string>
-        <string name="_sys_agents_helpers_popup_edit"><![CDATA[Редактировать помощника]]></string>
         <string name="_sys_agents_models_txt_active"><![CDATA[Активный]]></string>
         <string name="_sys_agents_models_txt_name"><![CDATA[Имя]]></string>
         <string name="_sys_agents_models_txt_title"><![CDATA[Название]]></string>
+        <string name="_sys_agents_models_txt_type"><![CDATA[Тип]]></string>
         <string name="_sys_agents_models_txt_key"><![CDATA[Ключ API]]></string>
         <string name="_sys_agents_models_txt_params"><![CDATA[Параметры]]></string>
         <string name="_sys_agents_models_txt_added"><![CDATA[Добавлено]]></string>
@@ -2563,69 +2505,10 @@
         <string name="_sys_agents_vector_store_field_files"><![CDATA[Файлы]]></string>
         <string name="_sys_agents_vector_store_field_files_info"><![CDATA[Файлы данных для загрузки в хранилище векторов.]]></string>
         <string name="_sys_agents_vector_store_data_queued"><![CDATA[Данные поставлены в очередь для обработки хранилища векторов]]></string>
-        
-        <string name="_sys_agents_assistants_txt_active"><![CDATA[Активный]]></string>
-        <string name="_sys_agents_assistants_txt_name"><![CDATA[Название]]></string>
-        <string name="_sys_agents_assistants_txt_model_id"><![CDATA[Модель]]></string>
-        <string name="_sys_agents_assistants_txt_profile_id"><![CDATA[Профиль]]></string>
-        <string name="_sys_agents_assistants_txt_prompt"><![CDATA[Инструкции]]></string>
-        <string name="_sys_agents_assistants_txt_added"><![CDATA[Добавлено]]></string>
-        <string name="_sys_agents_assistants_txt_assistant"><![CDATA[Ассистент]]></string>
-        <string name="_sys_agents_assistants_txt_codes_onclick"><![CDATA[onClick код для ссылки]]></string>
-        <string name="_sys_agents_assistants_txt_codes_button"><![CDATA[HTML-код кнопки]]></string>
-        <string name="_sys_agents_assistants_txt_add_knowledge"><![CDATA[Добавить знания]]></string>
-        <string name="_sys_agents_assistants_txt_ask"><![CDATA[Спросить]]></string>
-        <string name="_sys_agents_assistants_btn_add"><![CDATA[Добавить ассистента]]></string>
-        <string name="_sys_agents_assistants_btn_chats"><![CDATA[Чаты]]></string>
-        <string name="_sys_agents_assistants_btn_files"><![CDATA[Файлы]]></string>
-        <string name="_sys_agents_assistants_btn_codes"><![CDATA[Получить коды]]></string>
-        <string name="_sys_agents_assistants_field_name"><![CDATA[Название]]></string>
-        <string name="_sys_agents_assistants_field_model_id"><![CDATA[Модель]]></string>
-        <string name="_sys_agents_assistants_field_profile_id"><![CDATA[Профиль]]></string>
-        <string name="_sys_agents_assistants_field_profile_id_inf"><![CDATA[Оставьте пустым, чтобы использовать значение по умолчанию.]]></string>
-        <string name="_sys_agents_assistants_field_description"><![CDATA[Описание]]></string>
-        <string name="_sys_agents_assistants_field_prompt"><![CDATA[Инструкции]]></string>
-        <string name="_sys_agents_assistants_field_prompt_err"><![CDATA[Инструкции имеют важное значение. Пожалуйста, напишите что-нибудь здесь.]]></string>
-        <string name="_sys_agents_assistants_field_result"><![CDATA[Результат]]></string>
-        <string name="_sys_agents_assistants_popup_add"><![CDATA[Добавить ассистента]]></string>
-        <string name="_sys_agents_assistants_popup_edit"><![CDATA[Редактировать ассистента]]></string>
-        <string name="_sys_agents_assistants_chat_dsc_studio"><![CDATA[Помощник в студии]]></string>
-        <string name="_sys_agents_assistants_chat_dsc_block"><![CDATA[Помощник «Спроси ИИ»]]></string>
-        <string name="_sys_agents_assistants_chat_dsc_live_search"><![CDATA[Помощник в интерактивном поиске]]></string>
-        <string name="_sys_agents_assistants_chats_txt_name"><![CDATA[Название]]></string>
-        <string name="_sys_agents_assistants_chats_txt_type"><![CDATA[Тип]]></string>
-        <string name="_sys_agents_assistants_chats_txt_type_1"><![CDATA[Постоянный]]></string>
-        <string name="_sys_agents_assistants_chats_txt_type_10"><![CDATA[Временный]]></string>
-        <string name="_sys_agents_assistants_chats_txt_description"><![CDATA[Описание]]></string>
-        <string name="_sys_agents_assistants_chats_txt_messages"><![CDATA[Сообщений]]></string>
-        <string name="_sys_agents_assistants_chats_txt_added"><![CDATA[Добавлено]]></string>
-        <string name="_sys_agents_assistants_chats_txt_stored"><![CDATA[Сохранено]]></string>
-        <string name="_sys_agents_assistants_chats_txt_chat"><![CDATA[Чат]]></string>
-        <string name="_sys_agents_assistants_chats_btn_add"><![CDATA[Добавить чат]]></string>
-        <string name="_sys_agents_assistants_chats_btn_chat"><![CDATA[Беседовать]]></string>
-        <string name="_sys_agents_assistants_chats_btn_store"><![CDATA[Сохранить для будущего использования]]></string>
-        <string name="_sys_agents_assistants_chats_btn_unstore"><![CDATA[Удалить из хранилища]]></string>
+
         <string name="_sys_agents_agents_act_wipe_chat_history"><![CDATA[Очистить историю чата]]></string>
         <string name="_sys_agents_agents_act_message"><![CDATA[Сообщение]]></string>
-        <string name="_sys_agents_assistants_chats_field_name"><![CDATA[Название]]></string>
-        <string name="_sys_agents_assistants_chats_field_description"><![CDATA[Описание]]></string>
-        <string name="_sys_agents_assistants_chats_popup_add"><![CDATA[Добавить чат]]></string>
-        <string name="_sys_agents_assistants_chats_popup_edit"><![CDATA[Редактировать чат]]></string>
         <string name="_sys_agents_assistants_chats_msg_loading"><![CDATA[Я думаю. Пожалуйста, подождите.]]></string>
-        <string name="_sys_agents_assistants_chats_err_no_response"><![CDATA[Невозможно получить ответ. Проверьте файл журнала для получения дополнительной информации.]]></string>
-        <string name="_sys_agents_assistants_files_txt_name"><![CDATA[Название]]></string>
-        <string name="_sys_agents_assistants_files_txt_size"><![CDATA[Размер]]></string>
-        <string name="_sys_agents_assistants_files_txt_status"><![CDATA[Статус]]></string>
-        <string name="_sys_agents_assistants_files_txt_status_in_progress"><![CDATA[В ходе выполнения]]></string>
-        <string name="_sys_agents_assistants_files_txt_status_completed"><![CDATA[Завершенный]]></string>
-        <string name="_sys_agents_assistants_files_txt_status_failed"><![CDATA[Неудавшийся]]></string>
-        <string name="_sys_agents_assistants_files_txt_status_cancelled"><![CDATA[Отменённый]]></string>
-        <string name="_sys_agents_assistants_files_txt_added"><![CDATA[Добавлен]]></string>
-        <string name="_sys_agents_assistants_files_btn_add"><![CDATA[Добавить файл]]></string>
-        <string name="_sys_agents_assistants_files_btn_sync"><![CDATA[Cинхронизировать]]></string>
-        <string name="_sys_agents_assistants_files_btn_delete"><![CDATA[Удалить файл]]></string>
-        <string name="_sys_agents_assistants_files_field_file"><![CDATA[Файл]]></string>
-        <string name="_sys_agents_assistants_files_popup_add"><![CDATA[Добавить файл]]></string>
         <string name="_sys_agents_assistants_field_kwg_type"><![CDATA[Тип]]></string>
         <string name="_sys_agents_assistants_field_kwg_type_text"><![CDATA[Текст]]></string>
         <string name="_sys_agents_assistants_field_kwg_type_faq"><![CDATA[Вопрос с ответом]]></string>
@@ -3059,17 +2942,6 @@
         <string name="_sys_form_account_input_publish"><![CDATA[Создать новую учетную запись]]></string>
         <string name="_sys_form_account_input_caption_system_picture"><![CDATA[Изображение аккаунта]]></string>
         <string name="_sys_form_account_input_picture"><![CDATA[Загрузить изображение аккаунта]]></string>
-        <string name="_sys_form_agents_comment"><![CDATA[Агенты: Сообщение]]></string>
-        <string name="_sys_form_display_agents_comment_post"><![CDATA[Агенты: Отправить сообщение]]></string>
-        <string name="_sys_form_agents_comment_input_caption_system_action"><![CDATA[Действие]]></string>
-        <string name="_sys_form_agents_comment_input_caption_system_cmt_image"><![CDATA[Изображение]]></string>
-	<string name="_sys_form_agents_comment_input_caption_system_cmt_parent_id"><![CDATA[Родительский ID]]></string>
-	<string name="_sys_form_agents_comment_input_caption_system_cmt_submit"><![CDATA[Отправить]]></string>
-        <string name="_sys_form_agents_comment_input_caption_cmt_submit"><![CDATA[Отправить]]></string>
-	<string name="_sys_form_agents_comment_input_caption_system_cmt_text"><![CDATA[Текст]]></string>
-        <string name="_sys_form_agents_comment_input_caption_cmt_text_error"><![CDATA[Пустое сообщение не допускается. Напишите что-нибудь для отправки помощнику.]]></string>
-        <string name="_sys_form_agents_comment_input_caption_system_id"><![CDATA[ID объекта]]></string>
-        <string name="_sys_form_agents_comment_input_caption_system_sys"><![CDATA[Тип объекта]]></string>
 	<string name="_sys_form_checkbox_value_off"><![CDATA[Нет]]></string>
 	<string name="_sys_form_checkbox_value_on"><![CDATA[Да]]></string>
         <string name="_sys_form_input_caption_author"><![CDATA[Автор]]></string>
@@ -3932,8 +3804,6 @@
 	<string name="_sys_page_block_title_create_account"><![CDATA[Присоединиться]]></string>
         <string name="_sys_page_block_title_sys_author"><![CDATA[Автор]]></string>
 	<string name="_sys_page_block_title_author"><![CDATA[Автор]]></string>
-        <string name="_sys_page_block_title_sys_ask_aqssistant"><![CDATA[Спросите ИИ]]></string>
-        <string name="_sys_page_block_title_ask_aqssistant"><![CDATA[Спросите ИИ]]></string>
         <string name="_sys_page_block_title_sys_ai_agent"><![CDATA[ИИ агент]]></string>
         <string name="_sys_page_block_title_ai_agent"><![CDATA[ИИ агент]]></string>
         <string name="_sys_page_block_title_con_friends"><![CDATA[Друзья]]></string>
@@ -4225,7 +4095,6 @@
     <string name="_sys_select"><![CDATA[Выбрать]]></string>
     <string name="_sys_send"><![CDATA[Отправить]]></string>
     <string name="_sys_search_placeholder"><![CDATA[Поиск...]]></string>
-    <string name="_sys_search_or_ask_placeholder"><![CDATA[Поиск или вопрос...]]></string>
     <string name="_sys_show_less"><![CDATA[Показать меньше]]></string>
     <string name="_sys_show_more"><![CDATA[Показать ещё]]></string>
     <string name="_sys_skip_to_main_content"><![CDATA[Перейти к основному содержимому]]></string>

+ 4 - 3
package.json

@@ -51,7 +51,7 @@
     "jstree": "^3.3.17",
     "jush": "https://github.com/vrana/jush",
     "leaflet": "^1.9.4",
-    "leaflet-providers": "^3.0.0",
+    "leaflet-providers": "^4.0.0",
     "lottie-web": "^5.12.2",
     "marka": "github:fians/marka",
     "masonry-layout": "^4.2.2",
@@ -118,7 +118,8 @@
     "build:spin.js": "mkdir -p plugins_public/spin.js; cd plugins_public/spin.js; cp -r ../../node_modules/spin.js/spin.* .; grep -v 'export { Spinner };' spin.js > tmp && mv tmp spin.js",
     "build:stackedit.js": "mkdir -p plugins_public/stackedit.js; cd plugins_public/stackedit.js; cp -r ../../node_modules/stackedit-js/docs/lib/* .",
     "build:lucide": "mkdir -p plugins_public/lucide/icons; cp node_modules/lucide-static/icons/*.svg plugins_public/lucide/icons; cp node_modules/lucide/dist/umd/lucide.min.js plugins_public/lucide",
-    "build:tailwind-max": "NODE_ENV=production tailwindcss -i ./template/css/tailwind.css -c ./tailwind.config.full.js -o ./plugins_public/tailwind/css/tailwind.max.css --minify",
+    "generate:tailwind-max": "NODE_ENV=production tailwindcss -i ./template/css/tailwind.css -c ./tailwind.config.full.js -o ./scripts/tailwind.max.css --minify",
+    "build:tailwind-max": "mkdir -p plugins_public/tailwind/css && cp scripts/tailwind.max.css plugins_public/tailwind/css/tailwind.max.css",
     "build:tailwind-min": "NODE_ENV=production tailwindcss -i ./template/css/tailwind.css -c ./tailwind.config.js -o ./plugins_public/tailwind/css/tailwind.min.css --minify",
     "build:theia-sticky-sidebar": "mkdir -p plugins_public/theia-sticky-sidebar; cd plugins_public/theia-sticky-sidebar; cp -r ../../node_modules/theia-sticky-sidebar/dist/* .",
     "build:timepicker-addon": "mkdir -p plugins_public/timepicker-addon; cd plugins_public/timepicker-addon; cp -r ../../node_modules/jquery-ui-timepicker-addon/dist/* .",
@@ -158,7 +159,7 @@
     "@tailwindcss/container-queries": "^0.1.1",
     "@tailwindcss/typography": "^0.5.19",
     "@tanstack/ai": "^0.49.1",
-    "@tanstack/ai-client": "^0.29.2",
+    "@tanstack/ai-client": "^0.31.0",
     "@tanstack/ai-event-client": "^0.11.1",
     "@tanstack/ai-react": "^0.22.1",
     "@tanstack/ai-utils": "^0.4.0",

+ 88 - 6
scripts/Jenkinsfile

@@ -23,7 +23,7 @@ pipeline {
 
     environment {
         // Node.js configuration
-        PATH = "/var/lib/jenkins/.nvm/versions/node/v20.19.3/bin:$PATH"
+        PATH = "/var/lib/jenkins/.nvm/versions/node/v24.16.0/bin:$PATH"
         
         // Binary paths
         PHP = "/usr/bin/php8.3"
@@ -76,7 +76,7 @@ pipeline {
         DEMO_S3_SECRET = "*****"
         DEMO_GOOGLE_MAPS_API_KEY = "*****"
 
-        DEMO_MESSENGER_URL = "https://una.io:5443"
+        DEMO_MESSENGER_URL = "https://ci.una.io"
 
         DEMO_API_OAUTH_ID = "*****"
         DEMO_API_OAUTH_SECRET = "*****"
@@ -128,6 +128,7 @@ pipeline {
         
         stage('Clone additional UNA modules') {
             parallel {
+                /*
                 stage('Clone: Media Manager') {
                     steps {
                         dir('modules/boonex/media') {
@@ -137,12 +138,13 @@ pipeline {
                         }
                     }
                 }
+                */
                 stage('Clone: Messenger') {
                     steps {
                         dir('modules/boonex/messenger') {
                             git branch: 'master',
                                 url: 'git@github.com:unacms/jot-client-una.git',
-                                credentialsId: '0043feeb-9df7-4494-9b2b-d6c8a1b8ca08'
+                                credentialsId: '628aae6d-1c6a-437f-aa13-6f74c0167dfc'
                         }
                     }
                 }
@@ -399,6 +401,21 @@ ${PHP} -d memory_limit=512M /usr/bin/phpcpd.phar --min-lines 18 --log-pmd logs/p
             }
         }
 
+        stage('Install Composer production dependencies') {
+            steps {
+                script {
+                    echo "Installing Composer production dependencies"
+                    sh '''
+                        ${COMPOSER_PHAR} install \
+                            --no-dev \
+                            --no-interaction \
+                            --prefer-dist \
+                            --optimize-autoloader
+                    '''
+                }
+            }
+        }
+
         stage('Package Build') {
             steps {
                 script {
@@ -448,6 +465,12 @@ ${PHP} -d memory_limit=512M /usr/bin/phpcpd.phar --min-lines 18 --log-pmd logs/p
             steps {
                 script {
                     echo "Configuring database for UI tests"
+
+                    // Enable Lucid icons
+                    executeDbQuery(
+                        "UPDATE sys_options SET value = 'sys_lucide' WHERE name = 'sys_iconset_default'",
+                        "Enable Lucid icons"
+                    )
                     
                     // Maps API key
                     executeDbQuery(
@@ -461,9 +484,23 @@ ${PHP} -d memory_limit=512M /usr/bin/phpcpd.phar --min-lines 18 --log-pmd logs/p
                         "Configuring messenger server URL"
                     )
                     
+                    // SMTP mailer
+                    executeDbQuery(
+                        "UPDATE sys_options SET value = 'on' WHERE name = 'bx_smtp_on'",
+                        "Enabling SMTP mailer"
+                    )
+                    executeDbQuery(
+                        "UPDATE sys_options SET value = 'localhost' WHERE name = 'bx_smtp_host'",
+                        "Configuring SMTP mailer host"
+                    )
+                    executeDbQuery(
+                        "UPDATE sys_options SET value = '1025' WHERE name = 'bx_smtp_port'",
+                        "Configuring SMTP mailer port"
+                    )
+                    
                     // Create test users
                     echo "Creating test users"
-                    def users = ['user', 'lily', 'eva', 'linda', 'emma', 'karen', 'ella', 'viky', 'mila', 'eric', 'luck', 'jack', 'rob', 'dave', 'tom', 'andy', 'ivan']
+                    def users = ['agent', 'user', 'lily', 'eva', 'linda', 'emma', 'karen', 'ella', 'viky', 'mila', 'eric', 'luck', 'jack', 'rob', 'dave', 'tom', 'andy', 'ivan']
                     
                     users.each { user ->
                         executeDbMultiQuery("""
@@ -514,6 +551,12 @@ ${PHP} -d memory_limit=512M /usr/bin/phpcpd.phar --min-lines 18 --log-pmd logs/p
                         "UPDATE sys_objects_privacy SET spaces = 'bx_events,bx_spaces,bx_groups' WHERE object = 'bx_groups_allow_view_to'",
                         "Enabling subgroups functionality"
                     )
+                    
+                    // Tmp fix for search via API
+                    executeDbQuery(
+                        "UPDATE sys_objects_search SET GlobalSearch = '0' WHERE ObjectName IN('sys_pages', 'bx_timeline')",
+                        "Tmp fix for search via API"
+                    )
                 }
             }
         }
@@ -551,7 +594,7 @@ ${PHP} -d memory_limit=512M /usr/bin/phpcpd.phar --min-lines 18 --log-pmd logs/p
                                 ('https://hihi.com', 7),
                                 ('https://neo-ci.una.io', 8);
                                 UPDATE sys_options SET value = 'on' WHERE name = 'sys_api_enable';
-                                UPDATE sys_options SET value = 'on' WHERE name = 'sys_api_access_by_origin';
+                                UPDATE sys_options SET value = '' WHERE name = 'sys_api_access_by_origin';
                                 UPDATE sys_options SET value = 'on' WHERE name = 'sys_api_access_by_key';
                                 UPDATE sys_options SET value = 'on' WHERE name = 'sys_session_auth';
                             """, "Setting up new API keys, origins and enabling API features")
@@ -573,6 +616,45 @@ ${PHP} -d memory_limit=512M /usr/bin/phpcpd.phar --min-lines 18 --log-pmd logs/p
                         }
                     }
                 }
+                
+                stage('AI Agents Setup') {
+                    steps {
+                        script {
+                            echo "Creating vector store file"
+                            sh '''
+                                mkdir -p -m 0777 "${DEPLOY_PATH}/storage/vector_stores/1"
+                                cp /var/lib/jenkins/neuron.store ${DEPLOY_PATH}/storage/vector_stores/1/data.store
+                                chmod 0666 "${DEPLOY_PATH}/storage/vector_stores/1/data.store"
+                            '''
+                            
+                            echo "Configuring AI agents"
+                            executeDbMultiQuery("""
+                                                            
+    
+                                UPDATE `sys_agents_models` SET `active` = 1, `changed` = UNIX_TIMESTAMP() WHERE `type` = 'ollama' AND `model` = 'llama3.2:1b';
+                                UPDATE `sys_agents_models` SET `active` = 1, `changed` = UNIX_TIMESTAMP() WHERE `type` = 'ollama-embeddings' AND `model` = 'all-minilm';
+                                
+                                SET @model_embedding = (SELECT `id` FROM `sys_agents_models` WHERE `type` = 'ollama-embeddings' AND `model` = 'all-minilm');
+                                
+                                UPDATE `sys_agents_vector_store` SET `active` = 1, `changed` = UNIX_TIMESTAMP(), `embedding_provider_id` = @model_embedding WHERE `type` = 'file';
+                                
+                                SET @vector_store = (SELECT `id` FROM `sys_agents_vector_store` WHERE `type` = 'file');
+                                
+                                INSERT INTO `sys_agents_vector_store_data` (`id`, `vector_store_id`, `type`, `name`, `size`, `metadata`, `settings`, `content`, `status`, `added`) VALUES
+                                (NULL, @vector_store, 'custom', 'data_for_vector_store.md', 189, '', '{\"chunk_size\":512,\"delimeter\":\".\",\"overlap\":2}', 'Documentation is very important. UNACMS is modular CMS written in PHP and backed by MySQL database. UNA CMS features such modules as Posts, Discussions, Notifications, Timeline and others.\n', 'ready', UNIX_TIMESTAMP());
+
+                                SET @model = (SELECT `id` FROM `sys_agents_models` WHERE `type` = 'ollama' AND `model` = 'llama3.2:1b');
+                                SET @profile = (SELECT `p`.`id` FROM `sys_profiles` AS `p` INNER JOIN `sys_accounts` AS `a` ON(`p`.`account_id` = `a`.id) WHERE `email` = '');
+                                
+                                INSERT INTO `sys_agents_agents` (`id`, `name`, `title`, `description`, `icon`, `model_id`, `profile_id`, `acl_levels`, `prompt_system`, `prompt_steps`, `prompt_output`, `prompt_tools`, `tools`, `tools_max_run`, `limit_message`, `chat_history_context`, `vector_store_id`, `trigger`, `async`, `alert`, `scheduler_cron`, `webhook_key`, `webhook_sample`, `form_object`, `form_input`, `agent_sample`, `message_profile_id`, `added`, `active`) VALUES
+                                (NULL, 'john-agent', 'John Agent', '', '', @model, @profile, 2147483647, 'You are real person. Your name is John. You can talk freely with anyone.', '', '', '', '', 0, '', 50000, @vector_store, 'message', 0, '', '', '', '', '', '', '', 0, UNIX_TIMESTAMP(), 1),
+                                (NULL, 'profile-description-agent', 'Profile Description Agent', '', '', @model, @profile, 2147483646, 'Write a biography from first person. ', '', '', '', '', 0, '', 0, @vector_store, 'form-input', 0, '', '', '', '', 'bx_person', 'description', '', 0, UNIX_TIMESTAMP(), 1);
+
+
+                            """, "Configuring local Ollama AI providers, embedder and agents")
+                        }
+                    }
+                }
             }
         }
         
@@ -591,7 +673,7 @@ ${PHP} -d memory_limit=512M /usr/bin/phpcpd.phar --min-lines 18 --log-pmd logs/p
                             echo "Clone repository with tests"
                             git branch: 'main',
                                 url: 'git@github.com:unacms/una-ui-test3.git',
-                                credentialsId: '0043feeb-9df7-4494-9b2b-d6c8a1b8ca08'
+                                credentialsId: '628aae6d-1c6a-437f-aa13-6f74c0167dfc'
                             
                             // Create environment file
                             sh '''

+ 39 - 0
scripts/railway/Dockerfile

@@ -0,0 +1,39 @@
+FROM php:8.3-apache
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+        libfreetype6-dev \
+        libjpeg62-turbo-dev \
+        libpng-dev \
+        libzip-dev \
+    && docker-php-ext-configure gd --with-freetype --with-jpeg \
+    && docker-php-ext-install -j$(nproc) gd mysqli pdo_mysql zip exif opcache \
+    && a2enmod rewrite \
+    && rm -rf /var/lib/apt/lists/*
+
+RUN printf '%s\n' \
+        'short_open_tag=On' \
+        'memory_limit=256M' \
+        'upload_max_filesize=64M' \
+        'post_max_size=64M' \
+        'max_input_vars=5000' \
+        'variables_order=EGPCS' \
+        > /usr/local/etc/php/conf.d/una.ini
+
+COPY --chown=www-data:www-data . /var/www/html
+
+RUN mv /var/www/html/entrypoint.sh /usr/local/bin/entrypoint.sh \
+    && chmod +x /usr/local/bin/entrypoint.sh \
+    && rm -f /var/www/html/Dockerfile /var/www/html/railway.toml \
+    && mkdir -p /var/www/html/cache /var/www/html/cache_public /var/www/html/logs /var/www/html/tmp /var/www/html/storage \
+    && chown -R www-data:www-data \
+        /var/www/html/inc \
+        /var/www/html/cache \
+        /var/www/html/cache_public \
+        /var/www/html/logs \
+        /var/www/html/tmp \
+        /var/www/html/storage \
+    && if [ -f /var/www/html/plugins/ffmpeg/ffmpeg.exe ]; then chmod 0755 /var/www/html/plugins/ffmpeg/ffmpeg.exe; fi
+
+WORKDIR /var/www/html
+ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+CMD ["apache2-foreground"]

+ 128 - 0
scripts/railway/entrypoint.sh

@@ -0,0 +1,128 @@
+#!/bin/sh
+set -eu
+
+cd /var/www/html
+
+PORT="${PORT:-80}"
+if [ "$PORT" != "80" ]; then
+    sed -i "s/Listen 80/Listen ${PORT}/" /etc/apache2/ports.conf
+    sed -i "s/:80/:${PORT}/g" /etc/apache2/sites-available/000-default.conf
+fi
+a2dismod mpm_event mpm_worker >/dev/null 2>&1 || true
+a2enmod mpm_prefork rewrite >/dev/null 2>&1 || true
+
+mkdir -p cache cache_public logs tmp storage
+chmod a+rwX inc cache cache_public logs tmp storage
+if [ -f plugins/ffmpeg/ffmpeg.exe ]; then
+    chmod 0755 plugins/ffmpeg/ffmpeg.exe
+fi
+
+parse_mysql_url() {
+    _url="$1"
+    _url="${_url#mysql://}"
+    _url="${_url#mariadb://}"
+    DB_USER="${_url%%:*}"
+    _url="${_url#*:}"
+    DB_PASSWORD="${_url%%@*}"
+    _url="${_url#*@}"
+    _hostport="${_url%%/*}"
+    DB_NAME="${_url#*/}"
+    DB_NAME="${DB_NAME%%\?*}"
+    case "$_hostport" in
+        *:*)
+            DB_HOST="${_hostport%:*}"
+            DB_PORT="${_hostport##*:}"
+            ;;
+        *)
+            DB_HOST="$_hostport"
+            DB_PORT="3306"
+            ;;
+    esac
+}
+
+DB_URL="${DATABASE_URL:-${MYSQL_URL:-${MARIADB_PRIVATE_URL:-${MARIADB_URL:-}}}}"
+if [ -n "$DB_URL" ]; then
+    parse_mysql_url "$DB_URL"
+else
+    DB_HOST="${MYSQLHOST:-${MARIADB_HOST:-${MARIADBHOST:-mariadb.railway.internal}}}"
+    DB_PORT="${MYSQLPORT:-${MARIADB_PORT:-${MARIADBPORT:-3306}}}"
+    DB_NAME="${MYSQLDATABASE:-${MARIADB_DATABASE:-una}}"
+    DB_USER="${MYSQLUSER:-${MARIADB_USER:-una}}"
+    DB_PASSWORD="${MYSQLPASSWORD:-${MARIADB_PASSWORD:-una}}"
+fi
+
+export DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD
+export UNA_DB_HOST="$DB_HOST"
+export UNA_DB_PORT="$DB_PORT"
+export UNA_DB_NAME="$DB_NAME"
+export UNA_DB_USER="$DB_USER"
+export UNA_DB_PWD="$DB_PASSWORD"
+export UNA_ROOT_DIR=/var/www/html/
+export UNA_AUTO_HOSTNAME=1
+export UNA_SKIP_INSTALL_FOLDER_CHECK=1
+export UNA_SKIP_REDIRECT=1
+
+db_status() {
+    php -r '
+        $m = @new mysqli(
+            getenv("DB_HOST"),
+            getenv("DB_USER"),
+            getenv("DB_PASSWORD"),
+            getenv("DB_NAME"),
+            (int)(getenv("DB_PORT") ?: 3306)
+        );
+        if ($m->connect_error) {
+            fwrite(STDERR, $m->connect_error . "\n");
+            exit(1);
+        }
+        $r = $m->query("SHOW TABLES LIKE \"sys_options\"");
+        exit($r && $r->num_rows ? 0 : 2);
+    '
+}
+
+echo "Waiting for MariaDB at ${DB_HOST}:${DB_PORT}..."
+i=0
+rc=1
+set +e
+while [ "$i" -lt 60 ]; do
+    db_status
+    rc=$?
+    if [ "$rc" -eq 0 ] || [ "$rc" -eq 2 ]; then
+        break
+    fi
+    i=$((i + 1))
+    sleep 2
+done
+set -e
+
+if [ "$rc" -eq 1 ]; then
+    echo "MariaDB is not reachable" >&2
+    exit 1
+fi
+
+if [ "$rc" -eq 2 ]; then
+    echo "Installing UNA with cmd.php..."
+    php install/cmd.php \
+        -m bx_posts,bx_timeline,bx_contact,bx_developer,bx_antispam,bx_convos,bx_organizations,bx_notifications,bx_ocean,bx_ether,bx_accounts,bx_albums,bx_facebook,bx_tricon,bx_oauth,bx_chat_plus,bx_datafox,bx_payment,bx_market,bx_groups,bx_intercom,bx_googlecon,bx_linkedin,bx_twitter,bx_mailchimp,bx_forum,bx_acl,bx_events,bx_files,bx_glossary,bx_googletagman,bx_photos,bx_polls,bx_profiler,bx_quoteofday,bx_smtp,bx_shopify,bx_snipcart,bx_se_migration,bx_spaces,bx_stripe_connect,bx_unacon,bx_videos,bx_charts,bx_dolcon,bx_fontawesome,bx_channels,bx_attendant,bx_massmailer,bx_directory,bx_plyr,bx_ads,bx_decorous,bx_lucid,bx_credits,bx_wiki,bx_tasks,bx_courses,bx_classes,bx_artificer,bx_donations,bx_feedback,bx_help_tours,bx_stream,bx_attendant,bx_analytics,bx_reputation,bx_ru \
+        --db_host="$DB_HOST" \
+        --db_port="$DB_PORT" \
+        --db_name="$DB_NAME" \
+        --db_user="$DB_USER" \
+        --db_password="$DB_PASSWORD" \
+        --server_http_host="${RAILWAY_PUBLIC_DOMAIN:-localhost}" \
+        --server_php_self=/install/index.php \
+        --server_doc_root=/var/www/html/ \
+        --site_title="${UNA_SITE_TITLE:-UNA}" \
+        --admin_username="${UNA_ADMIN_USERNAME:-admin}" \
+        --admin_password="${UNA_ADMIN_PASSWORD:-unauna}"
+else
+    echo "UNA already installed; writing header from environment."
+    if [ ! -f inc/header.inc.php ]; then
+        cp install/patterns/header.inc.php inc/header.inc.php
+        chmod 0666 inc/header.inc.php
+    fi
+fi
+
+chown -R www-data:www-data inc cache cache_public logs tmp storage
+
+exec "$@"

+ 7 - 0
scripts/railway/railway.toml

@@ -0,0 +1,7 @@
+[build]
+builder = "DOCKERFILE"
+dockerfilePath = "Dockerfile"
+
+[deploy]
+restartPolicyType = "ON_FAILURE"
+restartPolicyMaxRetries = 5

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
scripts/tailwind.max.css


+ 1 - 1
studio/agents.php

@@ -17,7 +17,7 @@ bx_import('BxDolLanguages');
 bx_require_authentication(true);
 
 $sPage = bx_get('page');
-$sPage = $sPage !== false ? bx_process_input($sPage) : 'agents';
+$sPage = $sPage !== false ? bx_process_input($sPage) : '';
 
 $oPage = new BxTemplStudioAgents($sPage);
 BxDolStudioTemplate::getInstance()->displayPage($oPage);

+ 4 - 14
studio/classes/BxDolStudioAgents.php

@@ -7,22 +7,12 @@
  * @{
  */
 
-define('BX_DOL_STUDIO_AGENTS_TYPE_SETTINGS', 'settings');
-define('BX_DOL_STUDIO_AGENTS_TYPE_ASSISTANTS', 'assistants');
-
+define('BX_DOL_STUDIO_AGENTS_TYPE_AGENTS', 'agents');
 define('BX_DOL_STUDIO_AGENTS_TYPE_AI_PROVIDERS', 'ai_providers');
 define('BX_DOL_STUDIO_AGENTS_TYPE_TOOLS', 'tools');
 define('BX_DOL_STUDIO_AGENTS_TYPE_VECTOR_STORE', 'vector_store');
-define('BX_DOL_STUDIO_AGENTS_TYPE_AGENTS', 'agents');
-
-/*
- * Isn't used for now. Most probably they will be removed.
- */
-define('BX_DOL_STUDIO_AGENTS_TYPE_AUTOMATORS', 'automators');
-define('BX_DOL_STUDIO_AGENTS_TYPE_PROVIDERS', 'providers');
-define('BX_DOL_STUDIO_AGENTS_TYPE_HELPERS', 'helpers');
-
-define('BX_DOL_STUDIO_AGENTS_TYPE_DEFAULT', BX_DOL_STUDIO_AGENTS_TYPE_SETTINGS);
+define('BX_DOL_STUDIO_AGENTS_TYPE_SETTINGS', 'settings');
+define('BX_DOL_STUDIO_AGENTS_TYPE_DEFAULT', BX_DOL_STUDIO_AGENTS_TYPE_AGENTS);
 
 class BxDolStudioAgents extends BxTemplStudioWidget
 {
@@ -34,7 +24,7 @@ class BxDolStudioAgents extends BxTemplStudioWidget
     {
         parent::__construct('agents');
 
-        $this->oDbAi = new BxDolAIQuery();
+        $this->oDbAi = new BxDolAiQuery();
 
         $this->sPage = BX_DOL_STUDIO_AGENTS_TYPE_DEFAULT;
         if(is_string($sPage) && !empty($sPage))

+ 1 - 7
studio/classes/BxDolStudioAgentsAgents.php

@@ -9,19 +9,13 @@
 
 class BxDolStudioAgentsAgents extends BxTemplStudioGridAgents
 {
-    protected $_oDb;
-
     protected $_iProfileIdAi;
 
     public function __construct ($aOptions, $oTemplate = false)
     {
         parent::__construct ($aOptions, $oTemplate);
 
-        $this->_sDefaultSortingOrder = 'DESC';
-
-        $this->_oDb = new BxDolStudioAgentsQuery();
-
-        $this->_iProfileIdAi = BxDolAI::getInstance()->getProfileId();
+        $this->_iProfileIdAi = BxDolAi::getInstance()->getProfileId();
     }
 }
 

+ 0 - 40
studio/classes/BxDolStudioAgentsAssistants.php

@@ -1,40 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudio UNA Studio
- * @{
- */
-
-class BxDolStudioAgentsAssistants extends BxTemplStudioGridAgents
-{
-    protected $_oDb;
-
-    protected $_iProfileIdAi;
-
-    protected $_bShowHidden;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sDefaultSortingOrder = 'DESC';
-
-        $this->_oDb = new BxDolStudioAgentsQuery();
-
-        $this->_iProfileIdAi = BxDolAI::getInstance()->getProfileId();
-
-        $this->_bShowHidden = false;
-    }
-
-    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
-    {
-        if(!$this->_bShowHidden)
-            $this->_aOptions['source'] .= " AND `hidden`=0 ";
-
-        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
-    }
-}
-
-/** @} */

+ 0 - 61
studio/classes/BxDolStudioAgentsAsstChats.php

@@ -1,61 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudio UNA Studio
- * @{
- */
-
-class BxDolStudioAgentsAsstChats extends BxTemplStudioGridAgents
-{
-    protected $_oDb;
-
-    protected $_sCmts;
-
-    protected $_iProfileIdAi;
-    protected $_iAssistantId;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sDefaultSortingOrder = 'DESC';
-
-        $this->_oDb = new BxDolStudioAgentsQuery();
-
-        $this->_sCmts = 'sys_agents_assistants_chats';
-
-        $this->_iProfileIdAi = BxDolAI::getInstance()->getProfileId();
-
-        if(($iAssistantId = bx_get('aid')) !== false) 
-            $this->setAssistantId($iAssistantId);
-    }
-
-    public function setAssistantId($iAssistantId)
-    {
-        $this->_iAssistantId = bx_process_input($iAssistantId, BX_DATA_INT);
-        $this->_aQueryAppend['aid'] = $this->_iAssistantId;
-    }
-
-    protected function _delete ($mixedId)
-    {
-        $mixedResult = parent::_delete($mixedId);
-        if($mixedResult !== false && ($oCmts = BxDolCmts::getObjectInstance($this->_sCmts, $mixedId)) !== null) {
-            $oCmts->onObjectDelete($mixedId);
-        }
-
-        return $mixedResult;
-    }
-
-    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
-    {
-        if(empty($this->_iAssistantId))
-            return [];
-
-        $this->_aOptions['source'] .= $this->_oDb->prepareAsString(" AND `assistant_id`=? ", $this->_iAssistantId);
-        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
-    }
-}
-
-/** @} */

+ 0 - 323
studio/classes/BxDolStudioAgentsAsstChatsCmts.php

@@ -1,323 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudio UNA Studio
- * @{
- */
-
-class BxDolStudioAgentsAsstChatsCmts extends BxDolStudioAgentsCmts
-{
-    protected static $_sPrefixLoad = '#-#';
-    protected static $_sPrefixRetrieve = '|-|';
-    protected static $_sParamAllowDelete = 'allow_delete';
-
-    protected $_aChat;
-
-    protected $_aAssistant;
-    protected $_iAssistantId;
-    protected $_sAssistantUrl;
-
-    public function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
-    {
-        $this->_sTableImages = 'sys_agents_assistants_chats_files';
-
-        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
-
-        $this->_aChat = $this->_oQueryAgents->getChatsBy([
-            'sample' => 'id', 
-            'id' => (int)$iId
-        ]);
-
-        $this->_iAssistantId = 0;
-        $this->_sAssistantUrl = BX_DOL_URL_STUDIO . bx_append_url_params('agents.php', ['page' => 'assistants', 'spage' => 'chats']);
-
-        if(($iAssistantId = bx_get('aid')) !== false)
-            $this->setAssistantId(bx_process_input($iAssistantId, BX_DATA_INT));
-        else if(($iAssistantId = $this->_aChat['assistant_id'] ?? false))
-            $this->setAssistantId($iAssistantId);
-
-        if(!$this->isParam(self::$_sParamAllowDelete))
-            $this->setAllowDelete(true);
-    }
-
-    public function setAssistantId($iAssistantId)
-    {
-        $this->_iAssistantId = (int)$iAssistantId;
-        $this->_sAssistantUrl = BX_DOL_URL_STUDIO . bx_append_url_params('agents.php', ['page' => 'assistants', 'spage' => 'chats', 'aid' => $this->_iAssistantId]);
-
-        $this->_aAssistant = $this->_oQueryAgents->getAssistantsBy([
-            'sample' => 'id', 
-            'id' => $this->_iAssistantId
-        ]);
-
-        if(!empty($this->_aAssistant) && is_array($this->_aAssistant) && ($iAssistantPid = $this->_aAssistant['profile_id'] ?: false))
-            $this->_iProfileIdAi = (int)$iAssistantPid;
-
-        $this->_aMarkers['assistant_id'] = $this->_iAssistantId;
-    }
-
-    public function getPageJsObject()
-    {
-        return 'oBxDolStudioPageAgents';
-    }
-
-    public function getCommentsBlock($aBp = [], $aDp = [])
-    {
-        $aComments = parent::getCommentsBlock($aBp, array_merge($aDp, ['in_designbox' => false]));
-        if(empty($aComments['content']))
-            return MsgBox(_t('_error occured'));
-
-        if(empty($this->_aChat) || !is_array($this->_aChat))
-            return MsgBox(_t('_error occured'));
-        
-        if(empty($this->_aAssistant) || !is_array($this->_aAssistant))
-            return MsgBox(_t('_error occured'));
-
-        return $aComments['content'];
-    }
-
-    public function getComment($mixedCmt, $aBp = [], $aDp = [])
-    {
-        $aCmt = !is_array($mixedCmt) ? $this->getCommentRow((int)$mixedCmt) : $mixedCmt;
-        if(!$aCmt)
-            return '';
-
-        $iObjId = (int)$this->getId();
-        $iCmtId = (int)$aCmt['cmt_id'];
-        if($this->_isShowAnswerLoad($aCmt))
-            $this->_oQuery->updateComments(['cmt_text' => str_replace(self::$_sPrefixLoad, self::$_sPrefixRetrieve, $aCmt['cmt_text'])], ['cmt_id' => $iCmtId]);
-        else if($this->_isShowAnswerRetrieve($aCmt) && ($mixedAiData = $this->_getAiData()) !== false) {
-            list($iModelId, $sAssistantId, $sThreadId) = $mixedAiData;
-
-            $aCmtRequest = $this->getCommentRow((int)str_replace(self::$_sPrefixRetrieve, '', $aCmt['cmt_text']));
-            if(!empty($aCmtRequest) && is_array($aCmtRequest)) {
-                if(($oAIModel = $this->_oAI->getModelObject($iModelId)) !== false) {
-                    $oMessage = new BxDolAIMessageArray('hb');
-                    if(!empty($aCmtRequest['cmt_text']))
-                        $oMessage->addText($aCmtRequest['cmt_text']);
-
-                    if($this->isAttachImageEnabled()) {
-                        $aFiles = $this->_oQuery->getFiles($this->_aSystem['system_id'], $aCmtRequest['cmt_id']);
-                        if(!empty($aFiles) && is_array($aFiles)) {
-                            $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
-                            $oTranscoder = BxDolTranscoderImage::getObjectInstance($this->getTranscoderPreviewName());
-
-                            foreach($aFiles as $aFile) {
-                                $sFileUrl = $oStorage->getFileUrlById($aFile['image_id']);
-                                if(!$sFileUrl)
-                                    continue;
-
-                                //--- Add image as part of message's content.
-                                if($oTranscoder && $oTranscoder->isMimeTypeSupported($aFile['mime_type'])) {
-                                    $oMessage->addImageUrl($sFileUrl);
-                                    continue;
-                                }
-
-                                //--- Add non-image file as attchment.
-                                if(($sFileContent = bx_file_get_contents($sFileUrl))) {
-                                    $aFile = $oAIModel->callFiles([
-                                        'content' => $sFileContent, 
-                                        'name' => $aFile['file_name'], 
-                                        'mime' => $aFile['mime_type']
-                                    ]);
-                                    if($aFile !== false)
-                                        $oMessage->addAttachments($aFile['id']);
-                                }
-                            }
-                        }
-                    }
-
-                    if(empty($sThreadId) && ($aResponseInit = $oAIModel->getResponseInit(BX_DOL_AI_ASSISTANT, $oMessage, ['assistant_id' => $sAssistantId])) !== false) {
-                        $sThreadId = $aResponseInit['params']['thread_id'];
-
-                        $this->_oAI->updateAssistantChatById($iObjId, [
-                            'ai_thread_id' => $sThreadId
-                        ]);
-                    }
-
-                    if(!empty($sThreadId)) {
-                        $sResponse = $oAIModel->getResponse(BX_DOL_AI_ASSISTANT, $oMessage, [
-                            'thread_id' => $sThreadId, 
-                            'assistant_id' => $sAssistantId
-                        ]);
-
-                        if($sResponse === false && $oAIModel->isError())
-                            $sResponse = $oAIModel->getErrorMessage();
-
-                        if($sResponse) {
-                            $oParsedown = new Parsedown();
-                            $sResponse = $oParsedown->text($sResponse);
-
-                            $this->_oQuery->updateComments(['cmt_text' => $sResponse], ['cmt_id' => $iCmtId]);
-
-                            $aCmt['cmt_text'] = $sResponse;
-                        }
-                    }
-                }
-            }
-        }
-
-        return parent::getComment($aCmt, $aBp, array_merge($aDp, ['class_comment' => $this->_sStylePrefix . '-agents']));
-    }
-
-    public function isAttachImageEnabled()
-    {
-        return true;
-    }
-
-    public function isAllowDelete()
-    {
-        return !$this->isParam(self::$_sParamAllowDelete) || (int)$this->getParam(self::$_sParamAllowDelete) != 0;
-    }
-
-    public function setAllowDelete($bAllow)
-    {
-        $this->setParam(self::$_sParamAllowDelete, $bAllow ? 1 : 0);
-    }
-
-    public function addAuto($aValues, $bUnsetForm = false)
-    {
-        $this->_bAuto = true;
-        $mixedResult = $this->add($aValues);
-        $this->_bAuto = false;
-
-        if($bUnsetForm)
-            $this->_unsetFormObject();
-
-        return $mixedResult;
-    }
-
-    public function onPostAfter($iCmtId, $aDp = [])
-    {
-        $mixedResult = parent::onPostAfter($iCmtId, $aDp);
-        if($this->_bAuto || $mixedResult === false) 
-            return $mixedResult;
-
-        $mixedResultAuto = $this->addAuto([
-            'cmt_author_id' => $this->_iProfileIdAi,
-            'cmt_parent_id' => 0,
-            'cmt_text' => self::$_sPrefixLoad . $iCmtId
-        ], true);
-
-        if($mixedResultAuto !== false)
-            $mixedResult['id'] .= ',' . $mixedResultAuto['id'];
-
-        return $mixedResult;
-    }
-
-    public function registerTranscoders()
-    {
-        parent::registerTranscoders();
-
-        $aTranscoders = [
-            $this->getTranscoderPreviewName()
-        ];
-
-        BxDolTranscoderImage::registerHandlersArray($aTranscoders);
-    }
-
-    public function unregisterTranscoders()
-    {
-        parent::unregisterTranscoders();
-
-        $aTranscoders = [
-            $this->getTranscoderPreviewName()
-        ];
-
-        BxDolTranscoderImage::unregisterHandlersArray($aTranscoders);
-        BxDolTranscoderImage::cleanupObjectsArray($aTranscoders);
-    }
-
-    protected function _getActionsBox(&$aCmt, $aBp = [], $aDp = [])
-    {
-        if(!$this->isAllowDelete())
-            return '';
-
-        return $this->_oTemplate->parseHtmlByName('agents_comment_actions_assistant.html', [
-            'js_object' => $this->_sJsObjName,
-            'id' => $aCmt['cmt_id']
-        ]);
-    }
-    
-    protected function _getForm($sAction, $iId, $aDp = [])
-    {
-        $oForm = parent::_getForm($sAction, $iId, $aDp);
-
-        if(isset($oForm->aInputs['cmt_text']))
-            $oForm->aInputs['cmt_text']['caption'] = '';
-
-        if(isset($oForm->aInputs['cmt_submit']))
-            $oForm->aInputs['cmt_submit']['value'] = $this->_oTemplate->parseIcon('arrow-right');
-
-        return $oForm;
-    }
-
-    protected function _getFormObject($sAction = BX_CMT_ACTION_POST)
-    {
-        $oResult = parent::_getFormObject($sAction);
-        if(!isset($oResult->aInputs['cmt_image']))
-            return $oResult;
-
-        $oResult->aInputs['cmt_image'] = array_merge($oResult->aInputs['cmt_image'], [
-            'storage_object' => 'sys_agents_assistants_chats_files',
-            'images_transcoder' => 'sys_agents_assistants_chats_files_preview',
-            'upload_buttons_titles' => ['HTML5' => 'paperclip']
-        ]);
-
-        return $oResult;
-    }
-
-    protected function _getTmplVarsText($aCmt)
-    {
-        $bLoad = $this->_isShowAnswerLoad($aCmt);
-        $bRetrieve = $this->_isShowAnswerRetrieve($aCmt);
-
-        $iId = (int)$aCmt['cmt_id'];
-        $sText = $aCmt['cmt_text'];
-        if(!$bLoad && !$bRetrieve)
-            $sText = $this->_prepareTextForOutput($sText, $iId);
-
-        if($bLoad)
-            $sText = $this->_oTemplate->parseHtmlByName('agents_comment_loading.html', [
-                'style_prefix' => $this->_sStylePrefix,
-                'js_object' => $this->_sJsObjName,
-                'html_id' => 'cmt' . $iId . '-loading',
-                'id' => $iId
-            ]);
-        
-        if($bRetrieve)
-            $sText = _t('_sys_agents_assistants_chats_err_no_response');
-
-        return [
-            'style_prefix' => $this->_sStylePrefix,
-            'text' => $sText
-        ];
-    }
-
-    protected function _getAiData()
-    {
-        $aChat = $this->_oAI->getAssistantChatById((int)$this->getId());
-        if(empty($aChat) || !is_array($aChat))
-            return false;
-
-        $aAssistant = $this->_oAI->getAssistantById($aChat['assistant_id']);
-        if(empty($aAssistant) || !is_array($aAssistant))
-            return false;
-
-        return [$aAssistant['model_id'], $aAssistant['ai_asst_id'], $aChat['ai_thread_id']];
-    }
-
-    protected function _isShowAnswerLoad($aCmt)
-    {
-        return $aCmt['cmt_author_id'] == $this->_iProfileIdAi && strpos($aCmt['cmt_text'], self::$_sPrefixLoad) !== false;
-    }
-    
-    protected function _isShowAnswerRetrieve($aCmt)
-    {
-        return $aCmt['cmt_author_id'] == $this->_iProfileIdAi && strpos($aCmt['cmt_text'], self::$_sPrefixRetrieve) !== false;
-    }
-}
-
-/** @} */

+ 0 - 47
studio/classes/BxDolStudioAgentsAsstFiles.php

@@ -1,47 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudio UNA Studio
- * @{
- */
-
-class BxDolStudioAgentsAsstFiles extends BxTemplStudioGridAgents
-{
-    protected $_oDb;
-
-    protected $_iAssistantId;
-    protected $_aAssistantInfo;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sDefaultSortingOrder = 'DESC';
-
-        $this->_oDb = new BxDolStudioAgentsQuery();
-
-        if(($iAssistantId = bx_get('aid')) !== false) 
-            $this->setAssistantId($iAssistantId);
-    }
-
-    public function setAssistantId($iAssistantId)
-    {
-        $this->_iAssistantId = bx_process_input($iAssistantId, BX_DATA_INT);
-        $this->_aAssistantInfo = $this->_oDb->getAssistantsBy(['sample' => 'id', 'id' => $this->_iAssistantId]);
-
-        $this->_aQueryAppend['aid'] = $this->_iAssistantId;
-    }
-
-    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
-    {
-        if(empty($this->_iAssistantId))
-            return [];
-
-        $this->_aOptions['source'] .= $this->_oDb->prepareAsString(" AND `assistant_id`=? ", $this->_iAssistantId);
-        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
-    }
-}
-
-/** @} */

+ 0 - 28
studio/classes/BxDolStudioAgentsAutomators.php

@@ -1,28 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudio UNA Studio
- * @{
- */
-
-class BxDolStudioAgentsAutomators extends BxTemplStudioGridAgents
-{
-    protected $_oDb;
-
-    protected $_iProfileIdAi;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sDefaultSortingOrder = 'DESC';
-
-        $this->_oDb = new BxDolStudioAgentsQuery();
-
-        $this->_iProfileIdAi = BxDolAI::getInstance()->getProfileId();
-    }
-}
-
-/** @} */

+ 0 - 201
studio/classes/BxDolStudioAgentsAutomatorsCmts.php

@@ -1,201 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudio UNA Studio
- * @{
- */
-
-class BxDolStudioAgentsAutomatorsCmts extends BxDolStudioAgentsCmts
-{
-    protected $_sUrlPageAgents;
-
-    public function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
-    {
-        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
-
-        $this->_sUrlPageAgents = BX_DOL_URL_STUDIO . 'agents.php?page=automators';
-    }
-    
-    public function actionApproveCode()
-    {
-        if(!$this->isEnabled())
-            return echoJson([]);
-
-        $iCmt = bx_process_input(bx_get('Cmt'), BX_DATA_INT);
-        $aCmt = $this->getCommentRow($iCmt);
-        if(empty($aCmt) || !is_array($aCmt))
-            return echoJson([]);
-        
-        $iObjId = (int)$this->getId();
-        
-        $aAutomator = $this->_oAI->getAutomator($iObjId, true);
-        $aAutomator['code'] = $aCmt['cmt_text'];
-
-        $aParams = [];
-        if($aAutomator['type'] == BX_DOL_AI_AUTOMATOR_EVENT)
-            $aParams['alert'] = new BxDolAlerts('test', 'test', 0);
-
-        if(($sResult = $this->_oAI->emulCode($aAutomator, $aParams)) != '')
-            return echoJson(['msg' => _t('_sys_agents_automators_txt_eval_response') . $sResult]);
-
-        if(!$this->_oQueryAgents->updateAutomators(['code' => $aCmt['cmt_text'], 'status' => 'ready'], ['id' => (int)$this->getId()]))
-            return echoJson([]);
-
-        return echoJson(['redirect' => $this->_sUrlPageAgents]);
-    }
-
-    public function getPageJsObject()
-    {
-        return 'oBxDolStudioPageAgents';
-    }
-
-    public function getCommentsBlock($aBp = [], $aDp = [])
-    {
-        $aComments = parent::getCommentsBlock($aBp, ['in_designbox' => false]);
-        if(empty($aComments['content']))
-            return MsgBox(_t('_error occured'));
-
-        $aAutomator = $this->_oQueryAgents->getAutomatorsBy(['sample' => 'id', 'id' => (int)$this->getId()]);
-        if(empty($aAutomator) || !is_array($aAutomator))
-            return MsgBox(_t('_error occured'));
-
-        $aTmplVarsEvent = [];
-        $bTmplVarsEvent = $aAutomator['type'] == 'event';
-        if($bTmplVarsEvent)
-            $aTmplVarsEvent = [
-                'unit' => $aAutomator['alert_unit'],
-                'action' => $aAutomator['alert_action']
-            ];
-        
-        $aTmplVarsScheduler = [];
-        $bTmplVarsScheduler = $aAutomator['type'] == 'scheduler';
-        if($bTmplVarsScheduler) {
-            $aParams = [];
-            if(!empty($aAutomator['params']))
-                $aParams = json_decode($aAutomator['params'], true);
-
-            $aTmplVarsScheduler = [
-                'time' => $aParams['scheduler_time'],
-            ];
-        }
-
-        $sAutomator = $this->_oTemplate->parseHtmlByName('agents_automator_info.html', [
-            'type' => _t('_sys_agents_automators_field_type_' . $aAutomator['type']),
-            'bx_if:show_event' => [
-                'condition' => $bTmplVarsEvent,
-                'content' => $aTmplVarsEvent
-            ],
-            'bx_if:show_scheduler' => [
-                'condition' => $bTmplVarsScheduler,
-                'content' => $aTmplVarsScheduler
-            ],
-            'code' => $aAutomator['code']
-        ]);
-
-        return [
-            $sAutomator,
-            $aComments['content']
-        ];
-    }
-
-    public function getComment($mixedCmt, $aBp = [], $aDp = [])
-    {
-        return parent::getComment($mixedCmt, $aBp, array_merge($aDp, ['class_comment' => $this->_sStylePrefix . '-agents']));
-    }
-    
-    public function getFormBoxPost($aBp = [], $aDp = [])
-    {
-        $aComments = $this->_oQuery->getCommentsBy(['type' => 'latest', 'object_id' => (int)$this->getId(), 'start' => 0, 'per_page' => 1]);
-        if(!empty($aComments) && is_array($aComments)) {
-            $aLast = current($aComments);
-            if($aLast['cmt_author_id'] != $this->_iProfileIdAi)
-                return '';
-        }
-
-        return parent::getFormBoxPost($aBp, $aDp);
-    }
-
-    public function isAttachImageEnabled()
-    {
-        return false;
-    }
-
-    public function addAuto($aValues)
-    {
-        $this->_bAuto = true;
-        $mixedResult = $this->add($aValues);
-
-        $this->_bAuto = false;
-        return $mixedResult;
-    }
-
-    public function onPostAfter($iCmtId, $aDp = [])
-    {
-        $mixedResult = parent::onPostAfter($iCmtId, $aDp);
-        if($this->_bAuto || $mixedResult === false) 
-            return $mixedResult;
-
-        $iObjId = (int)$this->getId();
-        $aAutomator = $this->_oAI->getAutomator($iObjId, true);
-
-        $aComments = $this->_oQuery->getCommentsBy(['type' => 'object_id', 'object_id' => $iObjId]);
-        if($aAutomator['type'] == BX_DOL_AI_AUTOMATOR_EVENT && !empty($aAutomator['params']['trigger']))
-            $aComments[0]['cmt_text'] .= $aAutomator['params']['trigger'];
-
-        $oMessages = new BxDolAIMessages();
-        foreach($aComments as $aComment)
-            $oMessages->add((int)$aComment['cmt_author_id'] == $this->_iProfileIdAi ? 'ai' : 'hb', $aComment['cmt_text']);
-
-        $oAIModel = $this->_oAI->getModelObject($aAutomator['model_id']);
-        if(($sResponse = $oAIModel->getResponse($aAutomator['type'], $oMessages, $aAutomator['params'])) !== false) {
-            $mixedResultAuto = $this->addAuto([
-                'cmt_author_id' => $this->_iProfileIdAi,
-                'cmt_parent_id' => 0,
-                'cmt_text' => $sResponse
-            ]);
-
-            if($mixedResultAuto !== false)
-                $mixedResult['id'] .= ',' . $mixedResultAuto['id'];
-        }
-
-        return $mixedResult;
-    }
-
-    protected function _getActionsBox(&$aCmt, $aBp = [], $aDp = [])
-    {
-        if((int)$aCmt['cmt_author_id'] != $this->_iProfileIdAi)
-            return parent::_getActionsBox($aCmt, $aBp, array_merge($aDp, ['view_only' => true]));
-
-        return $this->_oTemplate->parseHtmlByName('agents_comment_actions_automator.html', [
-            'js_object' => $this->getPageJsObject(),
-            'id' => $aCmt['cmt_id']
-        ]);
-    }
-
-    protected function _getTmplVarsText($aCmt)
-    {
-        $aResult = parent::_getTmplVarsText($aCmt);
-
-        if((int)$aCmt['cmt_author_id'] == $this->_iProfileIdAi)
-            $aResult['text'] = '<pre>' . $aResult['text'] . '</pre>';
-
-        return $aResult;
-    }
-    
-    protected function _getForm($sAction, $iId, $aDp = [])
-    {
-        $aResult = parent::_getForm($sAction, $iId, $aDp);
-
-        $aResult->aInputs['cmt_text']['db']['pass'] = 'xss';
-        return $aResult;
-    }
-    
-    protected function _prepareTextForOutput($s, $iCmtId = 0)
-    {
-        return nl2br(parent::_prepareTextForOutput($s, $iCmtId));
-    }
-}
-
-/** @} */

+ 0 - 83
studio/classes/BxDolStudioAgentsCmts.php

@@ -1,83 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudio UNA Studio
- * @{
- */
-
-class BxDolStudioAgentsCmts extends BxTemplCmts
-{
-    protected $_oQueryAgents;
-
-    protected $_oAI;
-    protected $_iProfileIdAi;
-
-    protected $_bAuto;
-
-    public function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
-    {
-        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
-
-        if ($oTemplate)
-            $this->_oTemplate = $oTemplate;
-        else
-            $this->_oTemplate = BxDolStudioTemplate::getInstance();
-
-        $this->_sFormObject = 'sys_agents_comment';
-        $this->_sFormDisplayPost = 'sys_agents_comment_post';
-        $this->_sFormDisplayEdit = 'sys_agents_comment_edit';
-        
-        $this->_sTmplNameItemContent = 'agents_comment_content.html';
-        $this->_bLiveUpdates = false;
-
-        $this->_oQueryAgents = new BxDolStudioAgentsQuery();
-
-        $this->_oAI = BxDolAI::getInstance();
-        $this->_iProfileIdAi = $this->_oAI->getProfileId();
-
-        $this->_bAuto = false;
-    }
-
-    public function actionGetCmt ()
-    {
-        if(!$this->isEnabled())
-            return echoJson([]);
-
-        if($this->isViewAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
-            return echoJson([]);
-
-        $mixedCmtId = bx_process_input(bx_get('Cmt'));
-        $sCmtBrowse = ($sCmtBrowse = bx_get('CmtBrowse')) !== false ? bx_process_input($sCmtBrowse, BX_DATA_TEXT) : '';
-        $sCmtDisplay = ($sCmtDisplay = bx_get('CmtDisplay')) !== false ? bx_process_input($sCmtDisplay, BX_DATA_TEXT) : '';
-
-        $aCmtIds = strpos($mixedCmtId, ',') !== false ? explode(',', $mixedCmtId) : [$mixedCmtId];
-
-        $sContent = '';
-        foreach($aCmtIds as $iCmtId)
-            $sContent .= $this->getComment((int)$iCmtId, ['type' => $sCmtBrowse], ['type' => $sCmtDisplay, 'dynamic_mode' => true]);
-
-        $aCmt = $this->getCommentRow((int)reset($aCmtIds));
-        echoJson([
-            'parent_id' => $aCmt['cmt_parent_id'],
-            'vparent_id' => $aCmt['cmt_parent_id'],
-            'content' => $sContent
-        ]);
-    }
-
-    protected function _getCountersBox(&$aCmt, $aBp = [], $aDp = [])
-    {
-        return '';
-    }
-
-    protected function _getFormBox($sType, $aBp, $aDp)
-    {
-        return parent::_getFormBox($sType, $aBp, array_merge($aDp, [
-            'min_post_form' => false, 
-            'class_body' => $this->_sStylePrefix . '-body-agents'
-        ]));
-    }
-}
-
-/** @} */

+ 0 - 28
studio/classes/BxDolStudioAgentsHelpers.php

@@ -1,28 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudio UNA Studio
- * @{
- */
-
-class BxDolStudioAgentsHelpers extends BxTemplStudioGridAgents
-{
-    protected $_oDb;
-
-    protected $_iProfileIdAi;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sDefaultSortingOrder = 'DESC';
-
-        $this->_oDb = new BxDolStudioAgentsQuery();
-
-        $this->_iProfileIdAi = BxDolAI::getInstance()->getProfileId();
-    }
-}
-
-/** @} */

+ 7 - 5
studio/classes/BxDolStudioAgentsInstruments.php

@@ -9,16 +9,13 @@
 
 class BxDolStudioAgentsInstruments extends BxTemplStudioGridAgents
 {
-    protected $_oDb;
     protected $_sFieldName;
 
     public function __construct ($aOptions, $oTemplate = false)
     {
         parent::__construct ($aOptions, $oTemplate);
 
-        $this->_sDefaultSortingOrder = 'DESC';
         $this->_sFieldName = 'name';
-        $this->_oDb = new BxDolStudioAgentsQuery();
     }
 
     public function getPageJsObject()
@@ -66,7 +63,7 @@ class BxDolStudioAgentsInstruments extends BxTemplStudioGridAgents
 
         $sFormId = $oForm->getId();
         $sForm = $oForm->getCode(true);
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup_' . $sAction, _t($sPopupTitleKey), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
+        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup_' . $sAction, _t($sPopupTitleKey), $this->_oTemplate->parseHtmlByName('agents_agents_form.html', [
             'form_id' => $sFormId,
             'form' => $sForm,
             'object' => $this->_sObject,
@@ -83,7 +80,12 @@ class BxDolStudioAgentsInstruments extends BxTemplStudioGridAgents
 
         return $aForm;
     }
-    
+
+    protected function _getForm($sAction = '', $aData = [])
+    {
+        return [];
+    }
+
     protected function _getActionDelete ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
     {
         if ($sType == 'single' && $aRow['duplicate'] == 0)

+ 0 - 30
studio/classes/BxDolStudioAgentsProviders.php

@@ -1,30 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudio UNA Studio
- * @{
- */
-
-require_once(BX_DOL_DIR_STUDIO_INC . 'utils.inc.php');
-
-class BxDolStudioAgentsProviders extends BxTemplStudioGridAgents
-{
-    protected $_oDb;
-
-    protected $_iProfileIdAi;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sDefaultSortingOrder = 'DESC';
-
-        $this->_oDb = new BxDolStudioAgentsQuery();
-
-        $this->_iProfileIdAi = BxDolAI::getInstance()->getProfileId();
-    }
-}
-
-/** @} */

+ 1 - 1
studio/classes/BxDolStudioAgentsQuery.php

@@ -7,7 +7,7 @@
  * @{
  */
 
-class BxDolStudioAgentsQuery extends BxDolAIQuery
+class BxDolStudioAgentsQuery extends BxDolAiQuery
 {
     public function __construct()
     {

+ 2 - 2
studio/classes/BxDolStudioAgentsGrid.php → studio/classes/BxDolStudioGridAgents.php

@@ -7,7 +7,7 @@
  * @{
  */
 
-class BxDolStudioAgentsGrid extends BxTemplStudioGridAgents
+class BxDolStudioGridAgents extends BxTemplStudioGrid
 {
     protected $_oDb;
 
@@ -21,4 +21,4 @@ class BxDolStudioAgentsGrid extends BxTemplStudioGridAgents
     }
 }
 
-/** @} */
+/** @} */

+ 1 - 9
studio/classes/BxDolStudioPage.php

@@ -28,10 +28,6 @@ class BxDolStudioPage extends BxDol
     protected $iPageRssHelpLength;
     protected $sPageRssHelpId;
 
-    protected $iPageAssistantId;
-    protected $sPageAssistantChatName;
-    protected $sPageAssistantChatDescription;
-
     protected $_sTypesPreList;
 
     protected $aActions;
@@ -60,10 +56,6 @@ class BxDolStudioPage extends BxDol
         $this->sPageRssHelpUrl = 'http://feed.una.io/?section={page_name}';
         $this->iPageRssHelpLength = 5;
 
-        $this->iPageAssistantId = BxDolAI::getAssistantForStudio();
-        $this->sPageAssistantChatName = 'sys_studio_page_assistant';
-        $this->sPageAssistantChatDescription = '_sys_agents_assistants_chat_dsc_studio';
-
         $this->_sTypesPreList = 'sys_studio_widget_types';
 
         $this->aActions = [];
@@ -78,7 +70,7 @@ class BxDolStudioPage extends BxDol
         $this->_bShowHeaderBreadcrumb = getParam('sys_std_show_header_left') == 'on';
         $this->_bShowHeaderRightSearch = getParam('sys_std_show_header_right_search') == 'on';
         $this->_bShowHeaderRightSite = getParam('sys_std_show_header_right_site') == 'on';
-        $this->_bShowHeaderRightAssistant = $this->iPageAssistantId != 0;
+        $this->_bShowHeaderRightAssistant = false;
 
         if(is_string($mixedPageName)) {
             $this->aPage = $this->oDb->getPages(array('type' => 'by_page_name_full', 'value' => $mixedPageName));

+ 292 - 9
studio/js/agents.js

@@ -35,27 +35,310 @@ BxDolStudioPageAgents.prototype.initGrid = function() {
     });
 };
 
-BxDolStudioPageAgents.prototype.initAgentChat = function(sSelector, iAgentId) {
+BxDolStudioPageAgents.prototype.initAgentChat = function(sSelector, iAgentId, aThreads) {
     var $this = this;
-    var fInit = function() {
-        $this.destroyAgentChat();
-        if (typeof una === 'undefined' || !una.Chat)
+    if (typeof aThreads === 'undefined') {
+        this._fetchAgentChatThreads(iAgentId, function(aList) {
+            $this.initAgentChat(sSelector, iAgentId, aList || []);
+        });
+        return;
+    }
+
+    var oChat = $(sSelector);
+    if (!oChat.length)
+        return;
+
+    this.destroyAgentChat();
+    this._ensureAgentChatStyles();
+    this._sAgentChatSelector = sSelector;
+    this._iAgentChatId = iAgentId;
+
+    var oBox = oChat.closest('.bx-agents-popup-chat-wrap');
+    if (!oBox.length)
+        oBox = this._ensureAgentChatWrap(oChat, iAgentId);
+
+    if (!aThreads.length && !oBox.find('.bx-agents-popup-chat-thread').length)
+        aThreads = [{ thread_id: '', writable: 1, context_pid: 0, title: 'Guest', meta: '' }];
+    if (aThreads.length)
+        this._fillAgentChatThreads(oBox, aThreads);
+
+    this._oAgentChatBox = oBox;
+    oBox.off('click.agentChat').on('click.agentChat', '.bx-agents-popup-chat-thread', function(e) {
+        e.preventDefault();
+        $this._openAgentChatThread($(this));
+    });
+
+    var oFirst = oBox.find('.bx-agents-popup-chat-thread').first();
+    if (oFirst.length)
+        this._openAgentChatThread(oFirst);
+    else
+        this._initWritableAgentChat(sSelector, iAgentId, 0);
+};
+
+BxDolStudioPageAgents.prototype._fetchAgentChatThreads = function(iAgentId, fDone) {
+    var sUrl = (typeof sUrlRoot !== 'undefined' ? sUrlRoot : '/') + 'grid.php?o=sys_studio_agents_agents&a=get_chat_threads&id=' + encodeURIComponent(iAgentId) + '&_r=' + Date.now();
+    if (typeof glGrids !== 'undefined' && this.sObjNameGrid && glGrids[this.sObjNameGrid] && glGrids[this.sObjNameGrid]._sCsrfToken)
+        sUrl += '&csrf_token=' + encodeURIComponent(glGrids[this.sObjNameGrid]._sCsrfToken);
+    $.getJSON(sUrl, function(oData) {
+        fDone((oData && oData.threads) ? oData.threads : []);
+    }).fail(function() {
+        fDone([]);
+    });
+};
+
+BxDolStudioPageAgents.prototype._ensureAgentChatWrap = function(oChat, iAgentId) {
+    oChat.wrap('<div class="bx-agents-popup-chat-wrap" data-agent-id="' + parseInt(iAgentId, 10) + '"><div class="bx-agents-popup-chat-main"></div></div>');
+    var oMain = oChat.parent();
+    var oBox = oMain.parent();
+    oMain.append('<div class="bx-agents-popup-chat-note" style="display:none;">View only</div>');
+    oChat.before('<div class="bx-agents-popup-chat-artifacts" style="display:none;"></div>');
+    oBox.prepend('<aside class="bx-agents-popup-chat-list"><div class="bx-agents-popup-chat-list-title">Chats</div><div class="bx-agents-popup-chat-list-items"></div></aside>');
+    return oBox;
+};
+
+BxDolStudioPageAgents.prototype._fillAgentChatThreads = function(oBox, aThreads) {
+    var oItems = oBox.find('.bx-agents-popup-chat-list-items');
+    if (!oItems.length)
+        return;
+    oItems.empty();
+    for (var i = 0; i < aThreads.length; i++) {
+        var t = aThreads[i] || {};
+        var bMine = parseInt(t.writable, 10) === 1;
+        var oBtn = $('<button type="button" class="bx-agents-popup-chat-thread"></button>');
+        if (bMine)
+            oBtn.addClass('bx-agents-popup-chat-thread-mine');
+        oBtn.attr({
+            'data-thread-id': t.thread_id || '',
+            'data-writable': bMine ? 1 : 0,
+            'data-context': parseInt(t.context_pid, 10) || 0
+        });
+        oBtn.data('artifacts', Array.isArray(t.artifacts) ? t.artifacts : []);
+        oBtn.append($('<span class="bx-agents-popup-chat-thread-title"></span>').text(t.title || 'Chat'));
+        var sStatus = t.status === 'closed' ? 'closed' : 'opened';
+        var oMeta = $('<span class="bx-agents-popup-chat-thread-meta"></span>')
+            .addClass(sStatus === 'closed' ? 'bx-agents-popup-chat-thread-status-closed' : 'bx-agents-popup-chat-thread-status-opened')
+            .text(t.meta || ('status: ' + sStatus));
+        oBtn.append(oMeta);
+        oItems.append(oBtn);
+    }
+};
+
+BxDolStudioPageAgents.prototype._ensureAgentChatStyles = function() {
+    if (document.getElementById('bx-agents-popup-chat-inline-css'))
+        return;
+    var oStyle = document.createElement('style');
+    oStyle.id = 'bx-agents-popup-chat-inline-css';
+    oStyle.textContent =
+        '#grid-popup-sys_studio_agents_agents-message .bx-popup-width{width:68rem;max-width:calc(100vw - 2rem);}' +
+        '.bx-agents-popup-chat-wrap{display:flex;gap:0.75rem;min-height:32rem;max-height:min(36rem,calc(100vh - 10rem));}' +
+        '.bx-agents-popup-chat-list{flex:0 0 15.5rem;width:15.5rem;display:flex;flex-direction:column;min-height:0;border-right:1px solid rgba(0,0,0,.08);padding-right:0.75rem;}' +
+        '.bx-agents-popup-chat-list-title{font-weight:600;margin-bottom:0.5rem;}' +
+        '.bx-agents-popup-chat-list-items{overflow-y:auto;flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:0.25rem;}' +
+        '.bx-agents-popup-chat-thread{display:block;width:100%;text-align:left;border:0;background:transparent;padding:0.5rem 0.6rem;border-radius:0.4rem;cursor:pointer;}' +
+        '.bx-agents-popup-chat-thread:hover,.bx-agents-popup-chat-thread-active{background:rgba(37,99,235,.08);}' +
+        '.bx-agents-popup-chat-thread-title{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}' +
+        '.bx-agents-popup-chat-thread-meta{display:block;font-size:0.75rem;margin-top:0.15rem;white-space:pre-line;line-height:1.25;}' +
+        '.bx-agents-popup-chat-thread-status-opened{color:#15803d;}' +
+        '.bx-agents-popup-chat-thread-status-closed{color:#b45309;}' +
+        '.bx-agents-popup-chat-thread-dates{display:block;font-size:0.7rem;opacity:.6;margin-top:0.1rem;line-height:1.25;}' +
+        '.bx-agents-popup-chat-thread-dates span{display:block;}' +
+        '.bx-agents-popup-chat-artifacts{flex:0 0 auto;margin:0 0 0.5rem;padding:0.5rem 0.65rem;font-size:0.8rem;line-height:1.35;border:1px solid rgba(0,0,0,.12);border-radius:0.4rem;max-height:8rem;overflow-y:auto;background:#fffbe6;}' +
+        '.bx-agents-popup-chat-artifacts-row{display:flex;gap:0.5rem;min-width:0;}' +
+        '.bx-agents-popup-chat-artifacts-k{flex:0 0 7rem;font-weight:600;opacity:.7;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}' +
+        '.bx-agents-popup-chat-artifacts-v{flex:1 1 auto;min-width:0;overflow-wrap:anywhere;}' +
+        '.bx-agents-popup-chat-artifacts-empty{opacity:.6;}' +
+        '.bx-agents-popup-chat-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;}' +
+        '.bx-agents-popup-chat{height:auto;flex:1 1 auto;min-height:16rem;max-height:100%;}' +
+        '.bx-agents-popup-chat-note{margin-top:0.5rem;font-size:0.8rem;opacity:.7;}';
+    document.head.appendChild(oStyle);
+};
+
+BxDolStudioPageAgents.prototype._openAgentChatThread = function(oBtn) {
+    var sSelector = this._sAgentChatSelector;
+    var iAgentId = this._iAgentChatId;
+    var oBox = this._oAgentChatBox;
+    if (!oBtn || !oBtn.length || !sSelector)
+        return;
+
+    if (oBox && oBox.length) {
+        oBox.find('.bx-agents-popup-chat-thread').removeClass('bx-agents-popup-chat-thread-active');
+        oBtn.addClass('bx-agents-popup-chat-thread-active');
+    }
+
+    var bWritable = parseInt(oBtn.attr('data-writable'), 10) === 1;
+    var iContext = parseInt(oBtn.attr('data-context'), 10) || 0;
+    var sThreadId = oBtn.attr('data-thread-id') || '';
+
+    this.destroyAgentChat(true);
+    this._renderAgentChatArtifacts(oBox, oBtn.data('artifacts') || []);
+
+    if (bWritable) {
+        if (oBox && oBox.length)
+            oBox.find('.bx-agents-popup-chat-note').hide();
+        this._initWritableAgentChat(sSelector, iAgentId, iContext);
+        return;
+    }
+
+    if (oBox && oBox.length)
+        oBox.find('.bx-agents-popup-chat-note').show();
+    this._loadReadonlyAgentChat(sSelector, iAgentId, sThreadId);
+};
+
+BxDolStudioPageAgents.prototype._initWritableAgentChat = function(sSelector, iAgentId, iContext) {
+    if (typeof una === 'undefined' || !una.Chat)
+        return;
+
+    var sRoot = (typeof sUrlRoot !== 'undefined') ? sUrlRoot : '/';
+    var sEndpoint = sRoot + 'sys-ai-chat/' + encodeURIComponent(iAgentId);
+    iContext = parseInt(iContext, 10) || 0;
+    if (iContext > 0)
+        sEndpoint += '&context=' + encodeURIComponent(iContext);
+
+    this._oAgentChat = una.Chat.init(sSelector, {
+        agentId: iAgentId,
+        formatting: true,
+        contextProfileId: iContext,
+        endpoint: sEndpoint
+    });
+};
+
+BxDolStudioPageAgents.prototype._loadReadonlyAgentChat = function(sSelector, iAgentId, sThreadId) {
+    var $this = this;
+    var el = typeof sSelector === 'string' ? document.querySelector(sSelector) : sSelector;
+    if (!el)
+        return;
+
+    this._iAgentChatLoad = (this._iAgentChatLoad || 0) + 1;
+    var iLoad = this._iAgentChatLoad;
+    el.innerHTML = '';
+
+    var sUrl = (typeof sUrlRoot !== 'undefined' ? sUrlRoot : '/') + 'grid.php?o=sys_studio_agents_agents&a=get_chat_thread&id=' + encodeURIComponent(iAgentId) + '&thread_id=' + encodeURIComponent(sThreadId) + '&_r=' + Date.now();
+    if (typeof glGrids !== 'undefined' && this.sObjNameGrid && glGrids[this.sObjNameGrid] && glGrids[this.sObjNameGrid]._sCsrfToken)
+        sUrl += '&csrf_token=' + encodeURIComponent(glGrids[this.sObjNameGrid]._sCsrfToken);
+
+    if (this._oAgentChatXhr && this._oAgentChatXhr.abort)
+        this._oAgentChatXhr.abort();
+
+    this._oAgentChatXhr = $.getJSON(sUrl, function(oData) {
+        if (iLoad !== $this._iAgentChatLoad)
+            return;
+        if (!oData || (oData.code && oData.code != 200)) {
+            el.innerHTML = '<div class="bx-def-font-small" style="color:#c0392b;">' + $this._escAgentChat((oData && (oData.msg || oData.message)) || 'Error') + '</div>';
             return;
-        $this._oAgentChat = una.Chat.init(sSelector, { agentId: iAgentId, formatting: true });
+        }
+        if (parseInt(oData.writable, 10) === 1) {
+            $this._initWritableAgentChat(sSelector, iAgentId, 0);
+            return;
+        }
+        $this._renderAgentChatArtifacts($this._oAgentChatBox, oData.artifacts || []);
+        $this._renderReadonlyAgentChat(el, oData.messages || []);
+    }).fail(function() {
+        if (iLoad !== $this._iAgentChatLoad)
+            return;
+        el.innerHTML = '';
+    });
     };
 
-    if (typeof una !== 'undefined' && una.Chat)
-        fInit();
+BxDolStudioPageAgents.prototype._ensureAgentChatArtifactsPanel = function(oBox) {
+    if (!oBox || !oBox.length)
+        return $();
+    var oPanel = oBox.find('.bx-agents-popup-chat-main > .bx-agents-popup-chat-artifacts');
+    if (oPanel.length)
+        return oPanel;
+    oPanel = $('<div class="bx-agents-popup-chat-artifacts"></div>');
+    var oChat = oBox.find('.bx-agents-popup-chat-main > .bx-agents-popup-chat').first();
+    if (oChat.length)
+        oChat.before(oPanel);
+    else
+        oBox.find('.bx-agents-popup-chat-main').prepend(oPanel);
+    return oPanel;
 };
 
-BxDolStudioPageAgents.prototype.destroyAgentChat = function() {
-    if (!this._oAgentChat)
+BxDolStudioPageAgents.prototype._fillAgentChatArtifactsEl = function(oPanel, aArtifacts) {
+    if (!oPanel || !oPanel.length)
+        return;
+    aArtifacts = Array.isArray(aArtifacts) ? aArtifacts : [];
+    oPanel.empty();
+    if (!aArtifacts.length) {
+        oPanel.hide();
         return;
+    }
+    for (var i = 0; i < aArtifacts.length; i++) {
+        var a = aArtifacts[i] || {};
+        var oRow = $('<div class="bx-agents-popup-chat-artifacts-row"></div>');
+        oRow.append($('<span class="bx-agents-popup-chat-artifacts-k"></span>').text(a.field_name || ''));
+        oRow.append($('<span class="bx-agents-popup-chat-artifacts-v"></span>').text(a.field_value || ''));
+        oPanel.append(oRow);
+    }
+    oPanel.show();
+};
+
+BxDolStudioPageAgents.prototype._renderAgentChatArtifacts = function(oBox, aArtifacts) {
+    this._fillAgentChatArtifactsEl(this._ensureAgentChatArtifactsPanel(oBox), aArtifacts);
+};
 
+BxDolStudioPageAgents.prototype._renderReadonlyAgentChat = function(el, aMessages) {
+    var html = '<div class="bx-ai-chat flex flex-col gap-3 h-full min-h-72">' +
+        '<div class="bx-ai-chat-messages bx-def-border bx-def-round-corners bx-def-padding" style="flex:1;overflow-y:auto;min-height:16rem;">';
+    for (var i = 0; i < aMessages.length; i++) {
+        var m = aMessages[i] || {};
+        var isUser = m.role === 'user';
+        var text = '';
+        if (typeof m.content === 'string')
+            text = m.content;
+        else if (m.parts && m.parts.length) {
+            for (var j = 0; j < m.parts.length; j++) {
+                if (m.parts[j] && m.parts[j].type === 'text' && m.parts[j].content)
+                    text += (text ? '\n' : '') + m.parts[j].content;
+            }
+        }
+        html += '<div class="bx-ai-chat-message" style="display:flex;justify-content:' + (isUser ? 'flex-end' : 'flex-start') + ';margin:6px 0;">' +
+            '<div class="bx-ai-chat-message-inner bx-def-padding-sec bx-def-round-corners" style="max-width:85%;background:' + (isUser ? '#2563eb' : '#fff') + ';color:' + (isUser ? '#fff' : 'inherit') + ';">' +
+            this._escAgentChat(text).replace(/\n/g, '<br />') +
+            '</div></div>';
+    }
+    html += '</div></div>';
+    el.innerHTML = html;
+    var list = el.querySelector('.bx-ai-chat-messages');
+    if (list)
+        list.scrollTop = list.scrollHeight;
+};
+
+BxDolStudioPageAgents.prototype._escAgentChat = function(s) {
+    return String(s == null ? '' : s)
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/"/g, '&quot;');
+};
+
+BxDolStudioPageAgents.prototype.destroyAgentChat = function(bKeepBox) {
+    this._iAgentChatLoad = (this._iAgentChatLoad || 0) + 1;
+    if (this._oAgentChatXhr && this._oAgentChatXhr.abort) {
+        try { this._oAgentChatXhr.abort(); } catch (e) {}
+    }
+    this._oAgentChatXhr = null;
+
+    if (this._oAgentChat) {
     try {
         this._oAgentChat.destroy();
     } catch (e) {}
     this._oAgentChat = null;
+    }
+
+    if (this._sAgentChatSelector) {
+        var el = document.querySelector(this._sAgentChatSelector);
+        if (el)
+            el.innerHTML = '';
+    }
+
+    if (!bKeepBox) {
+        if (this._oAgentChatBox && this._oAgentChatBox.length)
+            this._oAgentChatBox.off('click.agentChat');
+        this._oAgentChatBox = null;
+        this._sAgentChatSelector = '';
+        this._iAgentChatId = 0;
+    }
 };
 
 BxDolStudioPageAgents.prototype.agentActions = function(oSource) {

+ 18 - 18
studio/template/agents_automator_form.html → studio/template/agents_agents_form.html

@@ -1,19 +1,19 @@
-<div class="bx-agents-automator-form">
-    <div class="bx-agents-automator-form-cnt">
-        __form__
-        <script language="javascript">
-            $(document).ready(function () {
-                $("#__form_id__").ajaxForm({ 
-                    dataType: "json",
-                    beforeSubmit: function (formData, jqForm, options) {
-                        bx_loading($("#__form_id__"), true);
-                    },
-                    success: function (data) {
-                        $(".bx-popup-applied:visible").dolPopupHide();
-                        glGrids.__object__.processJson(data, '__action__');
-                    }
-                });
-            });
-        </script>
-    </div>
+<div class="bx-agents-agents-form">
+    <div class="bx-agents-agents-form-cnt">
+        __form__
+        <script language="javascript">
+            $(document).ready(function () {
+                $("#__form_id__").ajaxForm({ 
+                    dataType: "json",
+                    beforeSubmit: function (formData, jqForm, options) {
+                        bx_loading($("#__form_id__"), true);
+                    },
+                    success: function (data) {
+                        $(".bx-popup-applied:visible").dolPopupHide();
+                        glGrids.__object__.processJson(data, '__action__');
+                    }
+                });
+            });
+        </script>
+    </div>
 </div>

+ 0 - 10
studio/template/agents_assistant_codes.html

@@ -1,10 +0,0 @@
-<div class="flex flex-col p-4 gap-4">
-    <div class="flex flex-col gap-2">
-        <div class="font-semibold"><bx_text:_sys_agents_assistants_txt_codes_onclick /></div>
-        <div class="bx-def-vanilla-html"><pre>__code_onclick__</pre></div>
-    </div>
-    <div class="flex flex-col gap-2">
-        <div class="font-semibold"><bx_text:_sys_agents_assistants_txt_codes_button /></div>
-        <div class="bx-def-vanilla-html"><pre>__code_button__</pre></div>
-    </div>
-</div>

+ 0 - 19
studio/template/agents_assistant_info.html

@@ -1,19 +0,0 @@
-<div class="flex flex-row">
-    <div class="flex-1">
-        <div class="flex gap-2">
-            <div class="w-20 font-semibold"><bx_text:_sys_agents_assistants_txt_assistant /></div>
-            <div>__assistant_name__</div>
-        </div>
-        <div class="flex bx-def-font-meta bx-def-font-grayed">__assistant_info__</div>
-        <bx_if:show_chat>
-            <div class="flex mt-4 gap-2">
-                <div class="w-20 font-semibold"><bx_text:_sys_agents_assistants_chats_txt_chat /></div>
-                <div>__chat_name__</div>
-            </div>
-            <div class="flex bx-def-font-meta bx-def-font-grayed">__chat_info__</div>
-        </bx_if:show_chat>
-    </div>
-    <div class="flex-0">
-        <a class="bx-btn" href="__url_back__"><i class="sys-icon arrow-left"></i></a>
-    </div>
-</div>

+ 0 - 11
studio/template/agents_automator_form_assistants.html

@@ -1,11 +0,0 @@
-<div class="bx-form-input-assistants">
-    <bx_repeat:assistants>
-        <div class="bx-form-input-assistant bx-def-padding-thd-topbottom">
-            <div class="bx-form-input-assistant-cell bx-fi-ac-txt">__input_select__</div>
-            <div class="bx-form-input-assistant-cell bx-fi-ac-btn bx-def-padding-sec-left">
-                <a class="bx-btn" href="javascript:void(0)" onclick="javascript:__js_object__.assistantDelete(this)"><bx_image_auto:times /></a>
-            </div>
-        </div>
-    </bx_repeat:assistants>
-    <div class="bx-form-input-assistant-add">__btn_add__</div>
-</div>

+ 0 - 11
studio/template/agents_automator_form_helpers.html

@@ -1,11 +0,0 @@
-<div class="bx-form-input-helpers">
-    <bx_repeat:helpers>
-        <div class="bx-form-input-helper bx-def-padding-thd-topbottom">
-            <div class="bx-form-input-helper-cell bx-fi-hc-txt">__input_select__</div>
-            <div class="bx-form-input-helper-cell bx-fi-hc-btn bx-def-padding-sec-left">
-                <a class="bx-btn" href="javascript:void(0)" onclick="javascript:__js_object__.helperDelete(this)"><bx_image_auto:times /></a>
-            </div>
-        </div>
-    </bx_repeat:helpers>
-    <div class="bx-form-input-helper-add">__btn_add__</div>
-</div>

+ 0 - 11
studio/template/agents_automator_form_providers.html

@@ -1,11 +0,0 @@
-<div class="bx-form-input-providers">
-    <bx_repeat:providers>
-        <div class="bx-form-input-provider bx-def-padding-thd-topbottom">
-            <div class="bx-form-input-provider-cell bx-fi-pc-txt">__input_select__</div>
-            <div class="bx-form-input-provider-cell bx-fi-pc-btn bx-def-padding-sec-left">
-                <a class="bx-btn" href="javascript:void(0)" onclick="javascript:__js_object__.providerDelete(this)"><bx_image_auto:times /></a>
-            </div>
-        </div>
-    </bx_repeat:providers>
-    <div class="bx-form-input-provider-add">__btn_add__</div>
-</div>

+ 0 - 26
studio/template/agents_automator_info.html

@@ -1,26 +0,0 @@
-<div>
-    <div class="flex gap-2">
-        <div class="w-20 font-semibold"><bx_text:_sys_agents_automators_txt_type /></div>
-        <div>__type__</div>
-    </div>
-    <bx_if:show_event>
-        <div class="flex gap-2">
-            <div class="w-20 font-semibold"><bx_text:_sys_agents_automators_txt_alert_unit /></div>
-            <div>__unit__</div>
-        </div>
-        <div class="flex gap-2">
-            <div class="w-20 font-semibold"><bx_text:_sys_agents_automators_txt_alert_action /></div>
-            <div>__action__</div>
-        </div>
-    </bx_if:show_event>
-    <bx_if:show_scheduler>
-        <div class="flex gap-2">
-            <div class="w-20 font-semibold"><bx_text:_sys_agents_automators_txt_scheduler_time /></div>
-            <div>__time__</div>
-        </div>
-    </bx_if:show_scheduler>
-    <div class="flex gap-2">
-        <div class="w-20 font-semibold"><bx_text:_sys_agents_automators_txt_code /></div>
-        <div class="bx-def-vanilla-html bx-def-vh-sm flex-1 max-w-none"><pre>__code__</pre></div>
-    </div>
-</div>

+ 0 - 10
studio/template/agents_comment_content.html

@@ -1,10 +0,0 @@
-<div class="__style_prefix__-body bx-def-vanilla-html bx-def-vh-sm relative mt-2 max-w-none text-clip">
-    __text__
-    <div class="__style_prefix__-body-overflow bx-def-color-bg-box-overflow absolute bottom-0 w-full h-1/5"></div>
-</div>
-<bx_if:show_attached>
-    <div class="__style_prefix__-attached bx-clearfix -mx-1 -mb-1">__attached__</div>
-</bx_if:show_attached>
-<bx_if:show_counters>
-    <div class="__style_prefix__-counters -mx-1 mt-1">__counters__</div>
-</bx_if:show_counters>

+ 18 - 1
studio/template/agents_popup_chat.html

@@ -1 +1,18 @@
-<div id="__chat_id__" class="bx-agents-popup-chat h-[32rem]"></div>
+<div class="bx-agents-popup-chat-wrap" data-agent-id="__agent_id__">
+    <aside class="bx-agents-popup-chat-list">
+        <div class="bx-agents-popup-chat-list-title">__txt_chats__</div>
+        <div class="bx-agents-popup-chat-list-items">
+            <bx_repeat:threads>
+                <button type="button" class="bx-agents-popup-chat-thread__class_mine__" data-thread-id="__thread_id__" data-writable="__writable__" data-context="__context_pid__">
+                    <span class="bx-agents-popup-chat-thread-title">__title__</span>
+                    <span class="bx-agents-popup-chat-thread-meta">__meta__</span>
+                </button>
+            </bx_repeat:threads>
+        </div>
+    </aside>
+    <div class="bx-agents-popup-chat-main">
+        <div class="bx-agents-popup-chat-artifacts" style="display:none;"></div>
+        <div id="__chat_id__" class="bx-agents-popup-chat"></div>
+        <div class="bx-agents-popup-chat-note" style="display:none;">__txt_readonly__</div>
+    </div>
+</div>

+ 125 - 2
studio/template/css/agents.css

@@ -14,11 +14,127 @@
 }
 
 #grid-popup-sys_studio_agents_agents-message .bx-popup-width {
-    width: 42rem;
+    width: 68rem;
     max-width: calc(100vw - 2rem);
 }
+.bx-agents-popup-chat-wrap {
+    display: flex;
+    gap: 0.75rem;
+    min-height: 32rem;
+    max-height: min(36rem, calc(100vh - 10rem));
+}
+.bx-agents-popup-chat-list {
+    flex: 0 0 15.5rem;
+    width: 15.5rem;
+    display: flex;
+    flex-direction: column;
+    min-height: 0;
+    border-right: 1px solid rgba(0,0,0,.08);
+    padding-right: 0.75rem;
+}
+.bx-agents-popup-chat-list-title {
+    font-weight: 600;
+    margin-bottom: 0.5rem;
+}
+.bx-agents-popup-chat-list-items {
+    overflow-y: auto;
+    flex: 1 1 auto;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
+    gap: 0.25rem;
+}
+.bx-agents-popup-chat-thread {
+    display: block;
+    width: 100%;
+    text-align: left;
+    border: 0;
+    background: transparent;
+    padding: 0.5rem 0.6rem;
+    border-radius: 0.4rem;
+    cursor: pointer;
+}
+.bx-agents-popup-chat-thread:hover,
+.bx-agents-popup-chat-thread-active {
+    background: rgba(37, 99, 235, .08);
+}
+.bx-agents-popup-chat-thread-title {
+    display: block;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+.bx-agents-popup-chat-thread-meta {
+    display: block;
+    font-size: 0.75rem;
+    margin-top: 0.15rem;
+    white-space: pre-line;
+    line-height: 1.25;
+}
+.bx-agents-popup-chat-thread-status-opened {
+    color: #15803d;
+}
+.bx-agents-popup-chat-thread-status-closed {
+    color: #b45309;
+}
+.bx-agents-popup-chat-thread-dates {
+    display: block;
+    font-size: 0.7rem;
+    opacity: .6;
+    margin-top: 0.1rem;
+    line-height: 1.25;
+}
+.bx-agents-popup-chat-thread-dates span {
+    display: block;
+}
+.bx-agents-popup-chat-artifacts {
+    flex: 0 0 auto;
+    margin: 0 0 0.5rem;
+    padding: 0.5rem 0.65rem;
+    font-size: 0.8rem;
+    line-height: 1.35;
+    border: 1px solid rgba(0,0,0,.12);
+    border-radius: 0.4rem;
+    max-height: 8rem;
+    overflow-y: auto;
+    background: #fffbe6;
+}
+.bx-agents-popup-chat-artifacts-row {
+    display: flex;
+    gap: 0.5rem;
+    min-width: 0;
+}
+.bx-agents-popup-chat-artifacts-k {
+    flex: 0 0 7rem;
+    font-weight: 600;
+    opacity: .7;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+.bx-agents-popup-chat-artifacts-v {
+    flex: 1 1 auto;
+    min-width: 0;
+    overflow-wrap: anywhere;
+}
+.bx-agents-popup-chat-artifacts-empty {
+    opacity: .6;
+}
+.bx-agents-popup-chat-main {
+    flex: 1 1 auto;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+}
 .bx-agents-popup-chat {
-    height: 32rem;
+    height: auto;
+    flex: 1 1 auto;
+    min-height: 16rem;
+}
+.bx-agents-popup-chat-note {
+    margin-top: 0.5rem;
+    font-size: 0.8rem;
+    opacity: .7;
 }
 
 /**--- Automators ---**/
@@ -151,6 +267,13 @@
 .select2-container .select2-selection--single {
     height: 30px;
 }
+.bx-agents-automator-form .select2-container {
+    width: 100% !important;
+}
+#bx_std_agents_add_popup .bx-popup,
+#bx_std_agents_edit_popup .bx-popup {
+    overflow: visible;
+}
 
 .select2-results__option .bx-alert-wrap {
     display:flex;

+ 4 - 252
studio/template/scripts/BxBaseStudioAgents.php

@@ -38,35 +38,14 @@ class BxBaseStudioAgents extends BxDolStudioAgents
             BX_DOL_STUDIO_AGENTS_TYPE_AI_PROVIDERS => ['icon' => 'mi-agt-providers.svg'],
             BX_DOL_STUDIO_AGENTS_TYPE_TOOLS => ['icon' => 'mi-agt-tools.svg'],
             BX_DOL_STUDIO_AGENTS_TYPE_VECTOR_STORE => ['icon' => 'mi-agt-vector-store.svg'],
-            BX_DOL_STUDIO_AGENTS_TYPE_SETTINGS => ['icon' => 'mi-agt-settings.svg'],
-
-            /*
-             * Hidden for now. Most probably they will be removed.
-             * 
-            BX_DOL_STUDIO_AGENTS_TYPE_PROVIDERS => ['icon' => 'mi-agt-providers.svg'],
-            BX_DOL_STUDIO_AGENTS_TYPE_HELPERS => ['icon' => 'mi-agt-helpers.svg'],
-            BX_DOL_STUDIO_AGENTS_TYPE_AUTOMATORS => ['icon' => 'mi-agt-automators.svg'],
-             */
+            BX_DOL_STUDIO_AGENTS_TYPE_SETTINGS => ['icon' => 'mi-agt-settings.svg']
         ];
 
         $this->aGridObjects = [
+            BX_DOL_STUDIO_AGENTS_TYPE_AGENTS => 'sys_studio_agents_agents',
             BX_DOL_STUDIO_AGENTS_TYPE_AI_PROVIDERS => 'sys_studio_agents_models',
-            BX_DOL_STUDIO_AGENTS_TYPE_VECTOR_STORE => 'sys_studio_agents_vector_store',
             BX_DOL_STUDIO_AGENTS_TYPE_TOOLS => 'sys_studio_agents_tools',
-
-            BX_DOL_STUDIO_AGENTS_TYPE_ASSISTANTS => 'sys_studio_agents_assistants',
-            BX_DOL_STUDIO_AGENTS_TYPE_ASSISTANTS . '_chats' => 'sys_studio_agents_assistants_chats',
-            BX_DOL_STUDIO_AGENTS_TYPE_ASSISTANTS . '_files' => 'sys_studio_agents_assistants_files',
-            BX_DOL_STUDIO_AGENTS_TYPE_AGENTS => 'sys_studio_agents_agents',
-
-            /*
-             * Hidden for now. Most probably they will be removed.
-             * 
-            BX_DOL_STUDIO_AGENTS_TYPE_AUTOMATORS => 'sys_studio_agents_automators',
-            BX_DOL_STUDIO_AGENTS_TYPE_PROVIDERS => 'sys_studio_agents_providers',
-            BX_DOL_STUDIO_AGENTS_TYPE_HELPERS => 'sys_studio_agents_helpers',
-             * 
-             */
+            BX_DOL_STUDIO_AGENTS_TYPE_VECTOR_STORE => 'sys_studio_agents_vector_store',
         ];
 
         $this->sSessionKeyAgentsView = 'bx_std_agents_view';
@@ -84,8 +63,6 @@ class BxBaseStudioAgents extends BxDolStudioAgents
 
     public function getPageMenu($aMenu = [], $aMarkers = [])
     {
-        $sJsObject = $this->getPageJsObject();
-
         $aMenu = [];
         foreach($this->aMenuItems as $sMenuItem => $aItem)
             $aMenu[] = [
@@ -139,97 +116,6 @@ class BxBaseStudioAgents extends BxDolStudioAgents
         return $this->getGrid($this->aGridObjects[BX_DOL_STUDIO_AGENTS_TYPE_VECTOR_STORE]);
     }
 
-    protected function getAssistants()
-    {
-        $oAi = BxDolAI::getInstance();
-        $oTemplate = BxDolStudioTemplate::getInstance();
-        
-        $this->aPageJsOptions['sPageUrl'] .= 'assistants';
-
-        $sSubPage = '';
-        if(($sSubPage = bx_get('spage')) !== false)
-            $sSubPage = bx_process_input($sSubPage, BX_DATA_TEXT);
-
-        $iAssistantId = 0;
-        if(($iAssistantId = bx_get('aid')) !== false)
-            $iAssistantId = bx_process_input($iAssistantId, BX_DATA_INT);
-
-        $iChatId = 0;
-        if(($iChatId = bx_get('cid')) !== false)
-            $iChatId = bx_process_input($iChatId, BX_DATA_INT);
-
-        if($iAssistantId && $iChatId) {
-            $aResult = [];
-
-            $sAssistantUrl = $this->sSubpageUrl . 'assistants&spage=chats&aid=' . $iAssistantId;
-            $aAssistant = $oAi->getAssistantById($iAssistantId);
-            if(!empty($aAssistant) && is_array($aAssistant)) {
-                $aChat = $oAi->getAssistantChatById($iChatId);
-                if(!empty($aChat) && is_array($aChat))
-                    $aResult[] = $oTemplate->parseHtmlByName('agents_assistant_info.html', [
-                        'assistant_name' => $aAssistant['name'],
-                        'assistant_info' => $aAssistant['description'],
-                        'bx_if:show_chat' => [
-                            'condition' => true,
-                            'content' => [
-                                'chat_name' => $aChat['name'],
-                                'chat_info' => $aChat['description'],
-                            ]
-                        ],
-                        'url_back' => $sAssistantUrl
-                    ]);
-            }
-
-            if(($oCmts = $oAi->getAssistantChatCmtsObject($iChatId, $oTemplate)) !== false) {
-                $this->aPageJsOptions = array_merge($this->aPageJsOptions, [
-                    'sPageUrl' => $sAssistantUrl . '&cid=' . $iChatId,
-                    'sActionUrlCmts' => bx_append_url_params(BX_DOL_URL_ROOT . 'cmts.php', [
-                        'sys' => $oCmts->getSystemName(),
-                        'id' => $iChatId
-                    ])
-                ]);
-
-                $aResult[] = $oCmts->getCommentsBlock();
-            }
-            else
-                $aResult[] = MsgBox(_t('_error occured'));
-
-            return $aResult;
-        }
-        else if($iAssistantId) {
-            $aResult = [];
-
-            $aAssistant = $oAi->getAssistantById($iAssistantId);
-            if(!empty($aAssistant) && is_array($aAssistant))
-                $aResult[] = $oTemplate->parseHtmlByName('agents_assistant_info.html', [
-                    'assistant_name' => $aAssistant['name'],
-                    'assistant_info' => $aAssistant['description'],
-                    'bx_if:show_chat' => [
-                        'condition' => false,
-                        'content' => [
-                            'chat_name' => '',
-                            'chat_info' => '',
-                        ]
-                    ],
-                    'url_back' => $this->aPageJsOptions['sPageUrl']
-                ]);
-            
-            switch($sSubPage) {
-                case 'chats':
-                    $aResult[] = $this->getGrid($this->aGridObjects[BX_DOL_STUDIO_AGENTS_TYPE_ASSISTANTS . '_chats']);
-                    break;
-
-                case 'files':
-                    $aResult[] = $this->getGrid($this->aGridObjects[BX_DOL_STUDIO_AGENTS_TYPE_ASSISTANTS . '_files']);
-                    break;
-            }
-
-            return $aResult;
-        }
-
-        return $this->getGrid($this->aGridObjects[BX_DOL_STUDIO_AGENTS_TYPE_ASSISTANTS]);
-    }
-
     protected function _setView($sView)
     {
         return BxDolSession::getInstance()->setValue($this->{'sSessionKey' . ucfirst($this->sPage) . 'View'}, $sView);
@@ -273,7 +159,7 @@ class BxBaseStudioAgents extends BxDolStudioAgents
         $sJsObject = $this->getPageJsObject();
         $oTemplate = BxDolStudioTemplate::getInstance();
 
-        $oAi = BxDolAI::getInstance();
+        $oAi = BxDolAi::getInstance();
         $aAgents = $oAi->getAgentsBy(['sample' => 'all']);
 
         $oGrid = $this->getGrid($this->aGridObjects[BX_DOL_STUDIO_AGENTS_TYPE_AGENTS], true);
@@ -419,78 +305,6 @@ class BxBaseStudioAgents extends BxDolStudioAgents
         return $this->getGrid($this->aGridObjects[BX_DOL_STUDIO_AGENTS_TYPE_AGENTS]);
     }
 
-    /*
-     * Isn't used for now. Most probably they will be removed.
-     * 
-    protected function getAutomators()
-    {
-        $oTemplate = BxDolStudioTemplate::getInstance();
-
-        $this->aPageJsOptions['sPageUrl'] .= 'automators';
-
-        if(($iId = bx_get('id')) !== false) {
-            if(($oCmts = BxDolAI::getInstance()->getAutomatorCmtsObject($iId, $oTemplate)) !== false) {
-                $this->aPageJsOptions = array_merge($this->aPageJsOptions, [
-                    'sPageUrl' => $this->sSubpageUrl . 'automators&id=' . $iId,
-                    'sActionUrlCmts' => bx_append_url_params(BX_DOL_URL_ROOT . 'cmts.php', [
-                        'sys' => $oCmts->getSystemName(),
-                        'id' => $iId
-                    ])
-                ]);
-
-                return $oCmts->getCommentsBlock();
-            }
-            else
-                return MsgBox(_t('_error occured'));
-        }
-
-        return $this->getGrid($this->aGridObjects[BX_DOL_STUDIO_AGENTS_TYPE_AUTOMATORS]);
-    }
-    
-    protected function getHelpers()
-    {
-        $oTemplate = BxDolStudioTemplate::getInstance();
-        
-        $this->aPageJsOptions['sPageUrl'] .= 'helpers';
-
-        if(($iId = bx_get('id')) !== false) {
-            $this->aPageJsOptions = array_merge($this->aPageJsOptions, [
-                'sPageUrl' => $this->sSubpageUrl . 'helpers&id=' . $iId,
-            ]);
-            
-            $aHelper = BxDolAI::getInstance()->getHelperById($iId);
-
-            $aForm = $this->_getHelpersForm('tune', $aHelper);
-            $oForm = new BxTemplFormView($aForm);
-            $oForm->initChecker();
-
-            if($oForm->isSubmittedAndValid()) {
-                if($oForm->update($iId) !== false) {
-                    $sMessage = $oForm->getCleanValue('message');
-                    $oForm->aInputs['result']['value'] = BxDolAI::callHelper($iId, $sMessage);
-                }
-            }
-
-            return $oForm->getCode();
-        }
-
-        return $this->getGrid($this->aGridObjects[BX_DOL_STUDIO_AGENTS_TYPE_HELPERS]);
-    }
-
-    protected function getProviders()
-    {
-        $this->aPageJsOptions = array_merge($this->aPageJsOptions, [
-            'sPageUrl' => $this->sSubpageUrl . 'providers',
-            'sActionUrlGrid' => bx_append_url_params(BX_DOL_URL_ROOT . 'grid.php', [
-                'o' => 'sys_studio_agents_providers'
-            ])
-        ]);
-
-        return $this->getGrid($this->aGridObjects[BX_DOL_STUDIO_AGENTS_TYPE_PROVIDERS]);
-    }
-     * 
-     */
-
     protected function getGrid($sObjectName, $bObject = false)
     {
         $oGrid = BxDolGrid::getObjectInstance($sObjectName);
@@ -499,69 +313,7 @@ class BxBaseStudioAgents extends BxDolStudioAgents
 
         return $bObject ? $oGrid : $oGrid->getCode();
     }
-    
-    protected function _getHelpersForm($sAction, $aHelper = [])
-    {
-        $aForm = array(
-            'form_attrs' => array(
-                'id' => 'bx_std_agents_helpers_' . $sAction,
-                'action' => $this->aPageJsOptions['sPageUrl'],
-                'method' => 'post',
-            ),
-            'params' => array (
-                'db' => array(
-                    'table' => 'sys_agents_helpers',
-                    'key' => 'id',
-                    'submit_name' => 'do_submit',
-                ),
-            ),
-            'inputs' => [
-                'prompt' => [
-                    'type' => 'textarea',
-                    'name' => 'prompt',
-                    'caption' => _t('_sys_agents_helpers_field_prompt'),
-                    'value' => isset($aHelper['prompt']) ? $aHelper['prompt'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_helpers_field_prompt_err'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ]
-                ],
-                'message' => [
-                    'type' => 'textarea',
-                    'name' => 'message',
-                    'caption' => _t('_sys_agents_helpers_field_message'),
-                    'value' => '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_helpers_field_message_err'),
-                    ],
-                ],
-                'result' => [
-                    'type' => 'textarea',
-                    'name' => 'result',
-                    'caption' => _t('_sys_agents_helpers_field_result'),
-                    'value' => '',
-                    'attrs' => [
-                        'disabled' => 'disabled'
-                    ]
-                ],
-                'submit' => [
-                    'type' => 'submit',
-                    'name' => 'do_submit',
-                    'value' => _t('_sys_submit'),
-                ],
-            ],
-        );
 
-        return $aForm;
-    }
 }
 
 /** @} */

+ 264 - 33
studio/template/scripts/BxBaseStudioAgentsAgents.php

@@ -63,8 +63,13 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
             return;
         }
 
-        $oAi = BxDolAI::getInstance();
-        $sResponse = $oAi->callAgent('manual', $aAgent);
+        $oAi = BxDolAi::getInstance();
+        try {
+            $sResponse = $oAi->callAgent('manual', $aAgent);
+        } catch (Throwable $o) {
+            echoJson(['msg' => $o->getMessage()]);
+            return;
+        }
 
         $oParsedown = new Parsedown();
         $oParsedown->setSafeMode(true);
@@ -86,7 +91,7 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
             return;
         }
 
-        $oDb = new BxDolAIQuery();
+        $oDb = new BxDolAiQuery();
         $oDb->wipeAgentChatHistory($aAgent);
 
         $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
@@ -98,7 +103,7 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
         $sAction = 'add';
 
         $oTemplate = BxDolStudioTemplate::getInstance();
-        $oAI = BxDolAI::getInstance();
+        $oAI = BxDolAi::getInstance();
 
         $aForm = $this->_getForm($sAction);
         $oForm = new BxTemplFormView($aForm);
@@ -151,7 +156,7 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
         }
 
         $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_agents_popup_add'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
+        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_agents_popup_add'), $this->_oTemplate->parseHtmlByName('agents_agents_form.html', [
             'form_id' => $sFormId,
             'form' => $oForm->getCode(true),
             'object' => $this->_sObject,
@@ -166,7 +171,7 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
         $sAction = 'edit';
 
         $oTemplate = BxDolStudioTemplate::getInstance();
-        $oAI = BxDolAI::getInstance();
+        $oAI = BxDolAi::getInstance();
 
         $iId = $this->_getId();
         $aAgent = BxDolAiQuery::getAgentObject($iId);
@@ -220,7 +225,7 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
         } 
 
         $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_agents_popup_edit', $aAgent['name']), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
+        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_agents_popup_edit', $aAgent['name']), $this->_oTemplate->parseHtmlByName('agents_agents_form.html', [
             'form_id' => $sFormId,
             'form' => $oForm->getCode(true),
             'object' => $this->_sObject,
@@ -266,21 +271,121 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
 
         $sChatId = 'bx-ai-chat-' . $iId;
         $sName = !empty($aAgent['title']) ? $aAgent['title'] : $aAgent['name'];
+        $aThreadsTpl = [];
+        $oAi = BxDolAi::getInstance();
+        $aThreads = $oAi && method_exists($oAi, 'listAgentChatThreads') ? $oAi->listAgentChatThreads($aAgent) : [];
+        foreach ($aThreads as $aThread) {
+            $aThreadsTpl[] = [
+                'thread_id' => bx_html_attribute($aThread['thread_id']),
+                'writable' => (int)$aThread['writable'],
+                'context_pid' => (int)$aThread['context_pid'],
+                'title' => bx_process_output($aThread['title']),
+                'meta' => bx_process_output($aThread['meta']),
+                'class_mine' => $aThread['class_mine'],
+            ];
+        }
+
         $sContent = BxTemplStudioFunctions::getInstance()->popupBox('popup_agent_chat_' . $iId, _t('_sys_agents_agents_popup_message', $sName), $this->_oTemplate->parseHtmlByName('agents_popup_chat.html', [
             'chat_id' => $sChatId,
+            'agent_id' => (int)$iId,
+            'txt_readonly' => _t('_sys_agents_agents_txt_readonly'),
+            'txt_chats' => _t('_sys_agents_agents_txt_chats'),
+            'bx_repeat:threads' => $aThreadsTpl,
         ]));
 
+        $aThreadsJs = [];
+        foreach ($aThreads as $aThread)
+            $aThreadsJs[] = $this->_agentChatThreadJs($aThread);
+        $sThreadsJson = json_encode($aThreadsJs, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
+        if ($sThreadsJson === false)
+            $sThreadsJson = '[]';
+
         $sJsObject = $this->getPageJsObject();
         return echoJson(['popup' => [
             'html' => $sContent,
             'options' => [
                 'closeOnOuterClick' => false,
-                'onShow' => $sJsObject . ".initAgentChat('#" . $sChatId . "', " . $iId . ");",
+                'onShow' => $sJsObject . ".initAgentChat('#" . $sChatId . "', " . (int)$iId . ", " . $sThreadsJson . ");",
                 'onHide' => $sJsObject . ".destroyAgentChat();",
             ]
         ]]);
     }
 
+    public function performActionGetChatThreads()
+    {
+        $iId = $this->_getId();
+        $aAgent = BxDolAiQuery::getAgentObject($iId);
+        if (!$aAgent) {
+            echoJson(['code' => 404, 'threads' => []]);
+            return;
+        }
+
+        $aThreadsJs = [];
+        $oAi = BxDolAi::getInstance();
+        $aThreads = $oAi && method_exists($oAi, 'listAgentChatThreads') ? $oAi->listAgentChatThreads($aAgent) : [];
+        foreach ($aThreads as $aThread)
+            $aThreadsJs[] = $this->_agentChatThreadJs($aThread);
+
+        echoJson(['code' => 200, 'threads' => $aThreadsJs]);
+    }
+
+    public function performActionGetChatThread()
+    {
+        $iId = $this->_getId();
+        $aAgent = BxDolAiQuery::getAgentObject($iId);
+        if (!$aAgent) {
+            echoJson(['code' => 404, 'msg' => _t('_sys_txt_error_occured'), 'messages' => []]);
+            return;
+        }
+
+        $sThreadId = trim((string)bx_get('thread_id'));
+        $oAi = BxDolAi::getInstance();
+        if (!$oAi || !method_exists($oAi, 'getChatHistoryUiMessagesByThread')) {
+            echoJson(['code' => 503, 'msg' => _t('_sys_txt_error_occured'), 'messages' => []]);
+            return;
+        }
+
+        $aMessages = $oAi->getChatHistoryUiMessagesByThread($aAgent, $sThreadId);
+        if ($aMessages === false) {
+            echoJson(['code' => 404, 'msg' => _t('_sys_txt_error_occured'), 'messages' => []]);
+            return;
+        }
+
+        echoJson([
+            'code' => 200,
+            'messages' => $aMessages,
+            'writable' => $oAi->isOwnAgentChatThread($aAgent, $sThreadId) ? 1 : 0,
+            'artifacts' => method_exists($oAi, 'getChatHistoryArtifactsByThread') ? $oAi->getChatHistoryArtifactsByThread($aAgent, $sThreadId) : [],
+        ]);
+    }
+
+    protected function _agentChatThreadJs($aThread)
+    {
+        $aArtifacts = [];
+        if (!empty($aThread['artifacts']) && is_array($aThread['artifacts'])) {
+            foreach ($aThread['artifacts'] as $aItem) {
+                if (!is_array($aItem))
+                    continue;
+                $aArtifacts[] = [
+                    'field_name' => (string)($aItem['field_name'] ?? ''),
+                    'field_value' => (string)($aItem['field_value'] ?? ''),
+                ];
+            }
+        }
+
+        return [
+            'thread_id' => (string)$aThread['thread_id'],
+            'writable' => (int)$aThread['writable'],
+            'context_pid' => (int)$aThread['context_pid'],
+            'title' => (string)$aThread['title'],
+            'meta' => (string)$aThread['meta'],
+            'status' => (string)($aThread['status'] ?? 'opened'),
+            'created_at' => (string)($aThread['created_at'] ?? ''),
+            'updated_at' => (string)($aThread['updated_at'] ?? ''),
+            'artifacts' => $aArtifacts,
+        ];
+    }
+
     protected function _getCellSwitcher ($mixedValue, $sKey, $aField, $aRow)
     {
         // if(empty($aRow['code']) || $aRow['status'] != BX_DOL_AI_AUTOMATOR_STATUS_READY)
@@ -299,7 +404,7 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
     {
         $sIcon = '';
         if($mixedValue) {
-            list($sIcon, $sIconUrl, $sIconA, $sIconHtml) = $this->_oTemplate->getTemplateFunctions()->getIcon($mixedValue, ['class' => 'sys-colored']);
+            list($sIcon, $sIconUrl, $sIconA, $sIconHtml, $sIconFontWithHtml) = $this->_oTemplate->getTemplateFunctions()->getIcon($mixedValue, ['class' => 'sys-colored']);
 
             if($sIcon)
                 $sIcon = $this->_oTemplate->parseIcon(BxDolIconset::getObjectInstance()->getIcon($sIcon));
@@ -307,6 +412,8 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
                 $sIcon = $sIconHtml;
             else if($sIconUrl)
                 $sIcon = '<img src="' . bx_html_attribute($sIconUrl) . '" />';
+            else if($sIconFontWithHtml)
+                $sIcon = $sIconFontWithHtml;
         }
 
         if (!$sIcon)
@@ -352,7 +459,7 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
             // $this->_oDb->deleteAutomatorHelpers(['automator_id' => (int)$mixedId]);
             // $this->_oDb->deleteAutomatorAssistants(['automator_id' => (int)$mixedId]);
 
-            // if(($oCmts = BxDolAI::getInstance()->getAutomatorCmtsObject($mixedId)) !== false)
+            // if(($oCmts = BxDolAi::getInstance()->getAutomatorCmtsObject($mixedId)) !== false)
             //    $oCmts->onObjectDelete();
         }
 
@@ -443,8 +550,8 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
                     'name' => 'model_id',
                     'caption' => _t('_sys_agents_field_model_id'),
                     'info' => _t('_sys_agents_field_model_id_info'),
-                    'value' => isset($aAgent['model_id']) ? $aAgent['model_id'] : BxDolAI::getDefaultModel(),
-                    'values' => $this->_oDb->getModelsBy(['sample' => 'all_pairs', 'active' => 1, 'capabilities' => ['chatvlm', 'chatllm']]),
+                    'value' => isset($aAgent['model_id']) ? $aAgent['model_id'] : BxDolAi::getDefaultModel(),
+                    'values' => $this->_getModelSelectValues(),
                     'required' => '1',
                     'checker' => [
                         'func' => 'Avail',
@@ -460,7 +567,7 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
                     'caption' => _t('_sys_agents_field_profile_id'),
                     'info' => _t('_sys_agents_field_profile_id_inf'),
                     'value' => isset($aAgent['profile_id']) ? $aAgent['profile_id'] : 0,
-                    'values' => bx_srv('system', 'get_options_agents_profile', [], 'TemplServices'),
+                    'values' => $this->_getProfileSelectValues(),
                     'required' => '1',
                     'required' => '1',
                     'checker' => [
@@ -517,13 +624,12 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
                     ]
                 ],
 
-                'alert_section' => array(
+                'alert_section' => [
                     'type' => 'block_header',
-                    'caption' => 'Trigger - alert',
+                    'caption' => _t('_sys_agents_block_header_alert'),
                     'collapsable' => true,
                     'collapsed' => true,
-                ),
-
+                ],
                 'alert_sample' => [
                     'type' => 'custom',
                     'name' => 'alert_sample',
@@ -537,7 +643,6 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
                         'payload' => !empty($aAgent['alert']) ? json_encode($this->_getAlertPayload($aAgent['alert']), JSON_PRETTY_PRINT) : '',
                     ]),
                 ],
-
                 'alert' => [
                     'type' => 'custom',
                     'name' => 'alert',
@@ -548,14 +653,16 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
                         'pass' => 'Xss',
                     ],
                 ],
+                'alert_section_end' => [
+                    'type' => 'block_end',
+                ],
 
                 'scheduler_section' => [
                     'type' => 'block_header',
-                    'caption' => 'Trigger - scheduler',
+                    'caption' => _t('_sys_agents_block_header_scheduler'),
                     'collapsable' => true,
                     'collapsed' => true,
                 ],
-
                 'scheduler_cron' => [
                     'type' => 'text',
                     'name' => 'scheduler_cron',
@@ -566,14 +673,16 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
                         'pass' => 'Xss',
                     ],
                 ],
+                'scheduler_section_end' => [
+                    'type' => 'block_end',
+                ],
 
-                'webhook_section' => array(
+                'webhook_section' => [
                     'type' => 'block_header',
-                    'caption' => 'Trigger - webhook',
+                    'caption' => _t('_sys_agents_block_header_webhook'),
                     'collapsable' => true,
                     'collapsed' => true,
-                ),
-
+                ],
                 'webhook_sample' => [
                     'type' => 'custom',
                     'name' => 'webhook_sample',
@@ -590,32 +699,37 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
                         'pass' => 'Xss',
                     ],
                 ],
+                'webhook_section_end' => [
+                    'type' => 'block_end',
+                ],
 
-                'message_section' => array(
+                'message_section' => [
                     'type' => 'block_header',
-                    'caption' => 'Trigger - message',
+                    'caption' => _t('_sys_agents_block_header_message'),
                     'collapsable' => true,
                     'collapsed' => true,
-                ),
-
+                ],
                 'message_profile_id' => [
                     'type' => 'select',
                     'name' => 'message_profile_id',
                     'caption' => _t('_sys_agents_field_message_profile'),
                     'info' => _t('_sys_agents_field_message_profile_info'),
                     'value' => isset($aAgent['message_profile_id']) ? $aAgent['message_profile_id'] : '',
-                    'values' => bx_srv('system', 'get_options_agents_profile', [true, '_adm_nav_txt_items_visible_for_all'], 'TemplServices'),
+                    'values' => $this->_getProfileSelectValues(true, '_adm_nav_txt_items_visible_for_all'),
                     'db' => [
                         'pass' => 'Xss',
                     ],
                 ],
+                'message_section_end' => [
+                    'type' => 'block_end',
+                ],
 
-                'form_field_section' => array(
+                'form_field_section' => [
                     'type' => 'block_header',
-                    'caption' => 'Trigger - form field',
+                    'caption' => _t('_sys_agents_block_header_form_input'),
                     'collapsable' => true,
                     'collapsed' => true,
-                ),
+                ],
 
                 'form_object' => [
                     'type' => 'custom',
@@ -638,11 +752,107 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
                         'pass' => 'Xss',
                     ],
                 ],
-
                 'form_field_section_end' => array(
                     'type' => 'block_end',
                 ),
 
+                'manual_section' => [
+                    'type' => 'block_header',
+                    'caption' => _t('_sys_agents_block_header_manual'),
+                    'collapsable' => true,
+                    'collapsed' => true,
+                ],
+                'max_turns' => [
+                    'type' => 'text',
+                    'name' => 'max_turns',
+                    'caption' => _t('_sys_agents_field_manual_max_turns'),
+                    'info' => _t('_sys_agents_field_manual_max_turns_info'),
+                    'value' => $aAgent['max_turns'] ?? 0,
+                    'required' => '1',
+                    'db' => [
+                        'pass' => 'Int',
+                    ],
+                ],
+                'chat_ttl_min' => [
+                    'type' => 'text',
+                    'name' => 'chat_ttl_min',
+                    'caption' => _t('_sys_agents_field_manual_chat_ttl_min'),
+                    'info' => _t('_sys_agents_field_manual_chat_ttl_min_info'),
+                    'value' => $aAgent['chat_ttl_min'] ?? 0,
+                    'required' => '1',
+                    'db' => [
+                        'pass' => 'Int',
+                    ],
+                ],
+                'limit_message' => [
+                    'type' => 'textarea',
+                    'name' => 'limit_message',
+                    'caption' => _t('_sys_agents_field_manual_limit_message'),
+                    'info' => _t('_sys_agents_field_manual_limit_message_info'),
+                    'value' => $aAgent['limit_message'] ?? '',
+                    'db' => [
+                        'pass' => 'Xss',
+                    ],
+                ],
+                'max_input_chars' => [
+                    'type' => 'text',
+                    'name' => 'max_input_chars',
+                    'caption' => _t('_sys_agents_field_manual_max_input_chars'),
+                    'info' => _t('_sys_agents_field_manual_max_input_chars_info'),
+                    'value' => $aAgent['max_input_chars'] ?? 0,
+                    'required' => '1',
+                    'db' => [
+                        'pass' => 'Int',
+                    ],
+                ],
+                'max_tokens' => [
+                    'type' => 'text',
+                    'name' => 'max_tokens',
+                    'caption' => _t('_sys_agents_field_manual_max_tokens'),
+                    'info' => _t('_sys_agents_field_manual_max_tokens_info'),
+                    'value' => $aAgent['max_tokens'] ?? 0,
+                    'required' => '1',
+                    'db' => [
+                        'pass' => 'Int',
+                    ],
+                ],
+                'max_sessions_per_hour' => [
+                    'type' => 'text',
+                    'name' => 'max_sessions_per_hour',
+                    'caption' => _t('_sys_agents_field_manual_max_sessions_per_hour'),
+                    'info' => _t('_sys_agents_field_manual_max_sessions_per_hour_info'),
+                    'value' => $aAgent['max_sessions_per_hour'] ?? 3,
+                    'required' => '1',
+                    'db' => [
+                        'pass' => 'Int',
+                    ],
+                ],
+                'max_sessions_per_day' => [
+                    'type' => 'text',
+                    'name' => 'max_sessions_per_day',
+                    'caption' => _t('_sys_agents_field_manual_max_sessions_per_day'),
+                    'info' => _t('_sys_agents_field_manual_max_sessions_per_day_info'),
+                    'value' => $aAgent['max_sessions_per_day'] ?? 10,
+                    'required' => '1',
+                    'db' => [
+                        'pass' => 'Int',
+                    ],
+                ],
+                'hidden_first_message' => [
+                    'type' => 'text',
+                    'name' => 'hidden_first_message',
+                    'caption' => _t('_sys_agents_field_manual_hidden_first_message'),
+                    'info' => _t('_sys_agents_field_manual_hidden_first_message_info'),
+                    'value' => $aAgent['hidden_first_message'] ?? '',
+                    'attrs' => ['maxlength' => 64],
+                    'db' => [
+                        'pass' => 'Xss',
+                    ],
+                ],
+                'manual_section_end' => [
+                    'type' => 'block_end',
+                ],
+
                 'vector_store_id' => [
                     'type' => 'select',
                     'name' => 'vector_store_id',
@@ -1039,6 +1249,27 @@ class BxBaseStudioAgentsAgents extends BxDolStudioAgentsAgents
         return null;
     }
 
+    protected function _getModelSelectValues()
+    {
+        $aParams = ['sample' => 'all_pairs', 'active' => 1];
+
+        $aModels = $this->_oDb->getModelsBy(array_merge($aParams, ['capabilities' => ['chatvlm', 'chatllm']]));
+        if(!$aModels || !is_array($aModels))
+            $aModels = $this->_oDb->getModelsBy($aParams);
+
+        return ($aModels && is_array($aModels)) ? $aModels : [];
+    }
+
+    protected function _getProfileSelectValues($bSelectOne = true, $sSelectOneLangKey = '_Select_one')
+    {
+        $aProfiles = bx_srv('system', 'get_options_agents_profile', [$bSelectOne, $sSelectOneLangKey], 'TemplServices');
+        if($aProfiles && is_array($aProfiles))
+            return $aProfiles;
+
+        $aProfiles = bx_srv('system', 'get_options_profile_bot', [], 'TemplServices');
+        return ($aProfiles && is_array($aProfiles)) ? $aProfiles : [];
+    }
+
     protected function _addJsCss()
     {
         parent::_addJsCss();

+ 0 - 318
studio/template/scripts/BxBaseStudioAgentsAssistants.php

@@ -1,318 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxBaseStudioAgentsAssistants extends BxDolStudioAgentsAssistants
-{
-    protected $_sUrlPage;
-    protected $_sFieldName;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sUrlPage = BX_DOL_URL_STUDIO . 'agents.php?page=assistants';
-
-        $this->_sFieldName = 'name';
-    }
-
-    public function getPageJsObject()
-    {
-        return 'oBxDolStudioPageAgents';
-    }
-
-    public function performActionAdd()
-    {
-        $sAction = 'add';
-
-        $oAI = BxDolAI::getInstance();
-
-        $aForm = $this->_getForm($sAction);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $aValsToAdd = ['added' => time()];
-
-            $sName = $oForm->getCleanValue($this->_sFieldName);
-            $sName = BxDolAIAssistant::getName($sName);
-            BxDolForm::setSubmittedValue($this->_sFieldName, $sName, $oForm->aFormAttrs['method']);
-
-            $iProfileId = $oForm->getCleanValue('profile_id');
-            if(empty($iProfileId)) {
-                $iProfileId = (int)getParam('sys_agents_profile');
-                if(empty($iProfileId))
-                    $iProfileId = current(bx_srv('system', 'get_options_agents_profile', [false], 'TemplServices'))['key'];
-
-                $aValsToAdd['profile_id'] = $iProfileId;
-            }
-
-            $oAIModel = $oAI->getModelObject($oForm->getCleanValue('model_id'));
-            if(($aAssistant = $oAIModel->getAssistant(['name' => $sName, 'prompt' => $oForm->getCleanValue('prompt')])) !== false)
-                $aValsToAdd = array_merge($aValsToAdd, [
-                    'ai_vs_id' => $aAssistant['vector_store_id'],
-                    'ai_asst_id' => $aAssistant['assistant_id']
-                ]);
-
-            if(($iId = $oForm->insert($aValsToAdd)) !== false)
-                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-            else
-                $aRes = ['msg' => _t('_sys_txt_error_occured')];
-
-            return echoJson($aRes);
-        }
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_assistants_popup_add'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    public function performActionEdit()
-    {
-        $sAction = 'edit';
-
-        $iId = $this->_getId();
-        $aAssistant = $this->_oDb->getAssistantsBy(['sample' => 'id', 'id' => $iId]);
-
-        $aForm = $this->_getFormEdit($sAction, $aAssistant);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $aValsToAdd = [];
-
-            $sName = $oForm->getCleanValue($this->_sFieldName);
-            if($aAssistant[$this->_sFieldName] != $sName) {
-                $sName = BxDolAIAssistant::getName($sName);
-                BxDolForm::setSubmittedValue($this->_sFieldName, $sName, $oForm->aFormAttrs['method']);
-            }
-
-            $iProfileId = $oForm->getCleanValue('profile_id');
-            if(empty($iProfileId)) {
-                $iProfileId = (int)getParam('sys_agents_profile');
-                if(empty($iProfileId))
-                    $iProfileId = current(bx_srv('system', 'get_options_agents_profile', [false], 'TemplServices'))['key'];
-
-                $aValsToAdd['profile_id'] = $iProfileId;
-            }
-
-            $aRes = [];
-            $aResError = ['msg' => _t('_sys_txt_error_occured')];
-
-            $oAIModel = BxDolAI::getInstance()->getModelObject($aAssistant['model_id']);
-            if(($sKey = 'ai_asst_id') && !empty($aAssistant[$sKey]) && !$oAIModel->editAssistant($aAssistant[$sKey], ['name' => $sName, 'prompt' => $oForm->getCleanValue('prompt')]))
-                return echoJson($aResError);
-
-            if($oForm->update($iId, $aValsToAdd) !== false)
-                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-            else
-                $aRes = $aResError;
-
-            return echoJson($aRes);
-        } 
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_assistants_popup_edit'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    public function performActionCodes()
-    {
-        $sAction = 'codes';
-
-        $iId = $this->_getId();
-
-        $sContent = BxTemplStudioFunctions::getInstance()->transBox('bx_std_agents_assistants_codes', $this->_oTemplate->parseHtmlByName('agents_assistant_codes.html', [
-            'code_onclick' => '{onclick}',
-            'code_button' => htmlspecialchars($this->_oTemplate->parseLink('javascript:void(0)', _t('_sys_agents_assistants_txt_add_knowledge'), [
-                'class' => 'bx-btn', 
-                'onclick' => '{onclick}'
-            ]), ENT_NOQUOTES)
-        ]));        
-        $sContent = bx_replace_markers($sContent, ['onclick' => "javascript:bx_agents_action(this, 'asst', 'add_knowledge', {id: " . $iId . "})"]);
-                
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => []]]);
-    }
-
-    protected function _getActionChats($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = [])
-    {
-    	$a['attr'] = array_merge($a['attr'], [
-            "onclick" => "window.open('" . $this->_sUrlPage . '&spage=chats&aid=' . $aRow['id'] . "', '_self');"
-    	]);
-
-    	return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
-    }
-
-    protected function _getActionFiles($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = [])
-    {
-    	$a['attr'] = array_merge($a['attr'], [
-            "onclick" => "window.open('" . $this->_sUrlPage . '&spage=files&aid=' . $aRow['id'] . "', '_self');"
-    	]);
-
-    	return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
-    }
-
-    protected function _delete ($mixedId)
-    {
-        $iAssistantId = (int)$mixedId;
-        $aAssistant = $this->_oDb->getAssistantsBy(['sample' => 'id', 'id' => $iAssistantId]);
-
-        $mixedResult = parent::_delete($mixedId);
-        if($mixedResult) {
-            $oAi = BxDolAI::getInstance();
-            $oAIModel = $oAi->getModelObject($aAssistant['model_id']);
-
-            $aChats = $this->_oDb->getChatsBy(['sample' => 'assistant_id', 'assistant_id' => $iAssistantId]);
-            foreach($aChats as $aChat) {
-                if(($oCmts = $oAi->getAssistantChatCmtsObject($aChat['id'])) !== false)
-                    $oCmts->onObjectDelete();
-
-                if(!empty($aChat['ai_file_id'])) {
-                    $oAIModel->callVectorStoresFilesDelete($aAssistant['ai_vs_id'], $aChat['ai_file_id']);
-                    $oAIModel->callFilesDelete($aChat['ai_file_id']);
-                }
-            }
-
-            $this->_oDb->deleteChats(['assistant_id' => $iAssistantId]);
-
-            $this->_oDb->deleteAutomatorAssistants(['assistant_id' => $iAssistantId]);
-
-            $oAIModel->callVectorStoresDelete($aAssistant['ai_vs_id']);
-            $oAIModel->callAssistantsDelete($aAssistant['ai_asst_id']);
-        }
-
-        return $mixedResult;
-    }
-
-    protected function _getFormEdit($sAction, $aAssistant = [])
-    {
-        $aForm = $this->_getForm($sAction, $aAssistant);
-        $aForm['form_attrs']['action'] .= '&id=' . $aAssistant['id'];
-
-        if(!isset($aForm['inputs']['model_id']['attrs']))
-            $aForm['inputs']['model_id']['attrs'] = [];
-
-        $aForm['inputs']['model_id']['attrs'] = array_merge($aForm['inputs']['model_id']['attrs'], [
-            'disabled' => 'disabled'
-        ]);
-        unset($aForm['inputs']['model_id']['db']);
-
-        return $aForm;
-    }
-
-    protected function _getForm($sAction, $aAssistant = [])
-    {
-        $sJsObject = $this->getPageJsObject();
-
-        $aForm = [
-            'form_attrs' => [
-                'id' => 'bx_std_agents_assistants_' . $sAction,
-                'action' => BX_DOL_URL_ROOT . bx_append_url_params('grid.php', ['o' => 'sys_studio_agents_assistants', 'a' => $sAction]),
-                'method' => 'post',
-            ],
-            'params' => [
-                'db' => [
-                    'table' => 'sys_agents_assistants',
-                    'key' => 'id',
-                    'submit_name' => 'do_submit',
-                ],
-            ],
-            'inputs' => [
-                'name' => [
-                    'type' => 'text',
-                    'name' => 'name',
-                    'required' => '1',
-                    'caption' => _t('_sys_agents_assistants_field_name'),
-                    'value' => isset($aAssistant['name']) ? $aAssistant['name'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_form_field_err_enter'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ]
-                ],
-                'model_id' => [
-                    'type' => 'select',
-                    'name' => 'model_id',
-                    'caption' => _t('_sys_agents_assistants_field_model_id'),
-                    'info' => '',
-                    'value' => isset($aAssistant['model_id']) ? $aAssistant['model_id'] : BxDolAI::getDefaultModel(),
-                    'values' => $this->_oDb->getModelsBy(['sample' => 'all_pairs', 'for_asst' => 1, 'active' => 1, 'hidden' => 0]),
-                    'required' => '1',
-                    'db' => [
-                        'pass' => 'Int',
-                    ]
-                ],
-                'profile_id' => [
-                    'type' => 'select',
-                    'name' => 'profile_id',
-                    'caption' => _t('_sys_agents_assistants_field_profile_id'),
-                    'info' => _t('_sys_agents_assistants_field_profile_id_inf'),
-                    'value' => isset($aAssistant['profile_id']) ? $aAssistant['profile_id'] : 0,
-                    'values' => bx_srv('system', 'get_options_agents_profile', [], 'TemplServices'),
-                    'required' => '0',
-                    'db' => [
-                        'pass' => 'Int',
-                    ]
-                ],
-                'description' => [
-                    'type' => 'textarea',
-                    'name' => 'description',
-                    'caption' => _t('_sys_agents_assistants_field_description'),
-                    'value' => isset($aAssistant['description']) ? $aAssistant['description'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_form_field_err_enter'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ]
-                ],
-                'prompt' => [
-                    'type' => 'textarea',
-                    'name' => 'prompt',
-                    'caption' => _t('_sys_agents_assistants_field_prompt'),
-                    'value' => isset($aAssistant['prompt']) ? $aAssistant['prompt'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_assistants_field_prompt_err'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ]
-                ],
-                'submit' => $this->_getFormControls(),
-            ],
-        ];
-
-        return $aForm;
-    }
-}
-
-/** @} */

+ 0 - 291
studio/template/scripts/BxBaseStudioAgentsAsstChats.php

@@ -1,291 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-bx_import('BxDolAIAssistant');
-
-class BxBaseStudioAgentsAsstChats extends BxDolStudioAgentsAsstChats
-{
-    protected $_sUrlPage;
-    protected $_sFieldName;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sUrlPage = BX_DOL_URL_STUDIO . 'agents.php?page=assistants&spage=chats&aid=' . $this->_iAssistantId;
-
-        $this->_sFieldName = 'name';
-    }
-
-    public function getPageJsObject()
-    {
-        return 'oBxDolStudioPageAgents';
-    }
-
-    public function performActionAdd()
-    {
-        $sAction = 'add';
-
-        $aForm = $this->_getForm($sAction);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $aValsToAdd = ['assistant_id' => $this->_iAssistantId, 'added' => time()];
-
-            $sName = $oForm->getCleanValue($this->_sFieldName);
-            $sName = BxDolAIAssistant::getChatName($sName);
-            BxDolForm::setSubmittedValue($this->_sFieldName, $sName, $oForm->aFormAttrs['method']);
-
-            if(($iId = $oForm->insert($aValsToAdd)) !== false)
-                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-            else
-                $aRes = ['msg' => _t('_sys_txt_error_occured')];
-
-            return echoJson($aRes);
-        }
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_assistants_chats_popup_add'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    public function performActionEdit()
-    {
-        $sAction = 'edit';
-
-        $iId = $this->_getId();
-        $aChat = $this->_oDb->getChatsBy(['sample' => 'id', 'id' => $iId]);
-
-        $aForm = $this->_getFormEdit($sAction, $aChat);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $aValsToAdd = [];
-
-            $sName = $oForm->getCleanValue($this->_sFieldName);
-            if($aChat[$this->_sFieldName] != $sName) {
-                $sName = BxDolAIAssistant::getChatName($sName);
-                BxDolForm::setSubmittedValue($this->_sFieldName, $sName, $oForm->aFormAttrs['method']);
-            }
-
-            if($oForm->update($iId, $aValsToAdd) !== false)
-                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-            else
-                $aRes = ['msg' => _t('_sys_txt_error_occured')];
-
-            return echoJson($aRes);
-        } 
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_assistants_chats_popup_edit'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-    
-    public function performActionStore()
-    {
-        $sAction = 'store';
-
-        $oAI = BxDolAI::getInstance();
-                
-        $iId = $this->_getId();
-        $aChat = $this->_oDb->getChatsBy(['sample' => 'id', 'id' => $iId]);
-        $aAssistant = $this->_oDb->getAssistantsBy(['sample' => 'id', 'id' => $aChat['assistant_id']]);
-
-        $aComments = $oAI->getAssistantChatCmtsObject($iId)->getCommentsBy(['type' => 'object_id', 'object_id' => $iId]);
-
-        $aMessages = [];
-        foreach($aComments as $aComment)
-            $aMessages[] = [
-                'role' => (int)$aComment['cmt_author_id'] == $this->_iProfileIdAi ? "assistant" : "user",
-                'content' => $aComment['cmt_text']
-            ];
-        
-        $oAIModel = $oAI->getModelObject($aAssistant['model_id']);
-
-        if(!empty($aChat['ai_file_id'])) {
-            $aResponse = $oAIModel->callVectorStoresFilesDelete($aAssistant['ai_vs_id'], $aChat['ai_file_id']);
-            if(!$aResponse)
-                return echoJson([]);
-
-            $aResponse = $oAIModel->callFilesDelete($aChat['ai_file_id']);
-            if(!$aResponse)
-                return echoJson([]);
-
-            $this->_oDb->updateChats(['ai_file_id' => '', 'stored' => 0], ['id' => $iId]);
-        }
-
-        $aRes = [];
-        if(($sFile = $aChat['name'] . '.json') && ($aFile = $oAIModel->callFiles(['content' => json_encode($aMessages), 'name' => $sFile, 'mime' => 'application/json'])) !== false) {
-            if(($aResponse = $oAIModel->callVectorStoresFiles($aAssistant['ai_vs_id'], ['file_id' => $aFile['id']])) !== false) {
-                $iNow = time();
-                $this->_oDb->insertFile(['name' => $sFile, 'assistant_id' => $aAssistant['id'], 'added' => $iNow, 'ai_file_id' => $aFile['id'], 'locked' => 1]);
-                $this->_oDb->updateChats(['type' => BX_DOL_AI_ASST_TYPE_PERMANENT, 'ai_file_id' => $aFile['id'], 'stored' => $iNow], ['id' => $iId]);
-
-                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-            }
-        }
-
-        return echoJson($aRes);
-    }
-
-    public function performActionUnstore()
-    {
-        $sAction = 'unstore';
-
-        $oAI = BxDolAI::getInstance();
-                
-        $iId = $this->_getId();
-        $aChat = $this->_oDb->getChatsBy(['sample' => 'id', 'id' => $iId]);
-        if(empty($aChat['ai_file_id'])) 
-            return echoJson([]);
-
-        $aAssistant = $this->_oDb->getAssistantsBy(['sample' => 'id', 'id' => $aChat['assistant_id']]);
-
-        $oAIModel = $oAI->getModelObject($aAssistant['model_id']);
-        if(!$oAIModel->callVectorStoresFilesDelete($aAssistant['ai_vs_id'], $aChat['ai_file_id']))
-            return echoJson([]);
-
-        if(!$oAIModel->callFilesDelete($aChat['ai_file_id']))
-            return echoJson([]);
-
-        $this->_oDb->deleteFiles(['ai_file_id' => $aChat['ai_file_id']]);
-        $this->_oDb->updateChats(['ai_file_id' => '', 'stored' => 0], ['id' => $iId]);
-
-        echoJson(['grid' => $this->getCode(false), 'blink' => $iId]);
-    }
-
-    protected function _getCellType($mixedValue, $sKey, $aField, $aRow)
-    {
-        return parent::_getCellDefault(_t('_sys_agents_assistants_chats_txt_type_' . $mixedValue), $sKey, $aField, $aRow);
-    }
-
-    protected function _getCellStored($mixedValue, $sKey, $aField, $aRow)
-    {
-        return parent::_getCellDefault($mixedValue ? bx_time_js($mixedValue) : '', $sKey, $aField, $aRow);
-    }
-
-    protected function _getActionChat($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = [])
-    {
-    	$a['attr'] = array_merge($a['attr'], [
-            "onclick" => "window.open('" . $this->_sUrlPage . "&cid=" . $aRow['id'] . "', '_self');"
-    	]);
-
-    	return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
-    }
-    
-    protected function _getActionStore($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = [])
-    {
-        $aChat = $this->_oDb->getChatsBy(['sample' => 'id', 'id' => $aRow['id']]);
-        if(!empty($aChat['ai_file_id'])) 
-            return '';
-
-        return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
-    }
-    
-    protected function _getActionUnstore($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = [])
-    {
-        $aChat = $this->_oDb->getChatsBy(['sample' => 'id', 'id' => $aRow['id']]);
-        if(empty($aChat['ai_file_id'])) 
-            return '';
-
-        return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
-    }
-
-    protected function _delete ($mixedId)
-    {
-        $aChat = $this->_oDb->getChatsBy(['sample' => 'id', 'id' => (int)$mixedId]);
-
-        $mixedResult = parent::_delete($mixedId);
-        if($mixedResult)
-            BxDolAIAssistant::getObjectInstance($aChat['assistant_id'])->deleteChat($aChat);
-
-        return $mixedResult;
-    }
-
-    protected function _getFormEdit($sAction, $aChat = [])
-    {
-        $aForm = $this->_getForm($sAction, $aChat);
-        $aForm['form_attrs']['action'] .= '&id=' . $aChat['id'];
-
-        return $aForm;
-    }
-
-    protected function _getForm($sAction, $aChat = [])
-    {
-        $sJsObject = $this->getPageJsObject();
-
-        $aForm = array(
-            'form_attrs' => array(
-                'id' => 'bx_std_agents_assistants_' . $sAction,
-                'action' => BX_DOL_URL_ROOT . bx_append_url_params('grid.php', ['o' => 'sys_studio_agents_assistants_chats', 'a' => $sAction, 'aid' => $this->_iAssistantId]),
-                'method' => 'post',
-            ),
-            'params' => array (
-                'db' => array(
-                    'table' => 'sys_agents_assistants_chats',
-                    'key' => 'id',
-                    'submit_name' => 'do_submit',
-                ),
-            ),
-            'inputs' => array(
-                'name' => [
-                    'type' => 'text',
-                    'name' => 'name',
-                    'required' => '1',
-                    'caption' => _t('_sys_agents_assistants_chats_field_name'),
-                    'value' => isset($aChat['name']) ? $aChat['name'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_form_field_err_enter'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ]
-                ],
-                'description' => [
-                    'type' => 'textarea',
-                    'name' => 'description',
-                    'caption' => _t('_sys_agents_assistants_chats_field_description'),
-                    'value' => isset($aChat['description']) ? $aChat['description'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_form_field_err_enter'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ]
-                ],
-                'submit' => $this->_getFormControls(),
-            ),
-        );
-
-        return $aForm;
-    }
-}
-
-/** @} */

+ 0 - 184
studio/template/scripts/BxBaseStudioAgentsAsstFiles.php

@@ -1,184 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxBaseStudioAgentsAsstFiles extends BxDolStudioAgentsAsstFiles
-{
-    protected $_sUrlPage;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sUrlPage = BX_DOL_URL_STUDIO . 'agents.php?page=assistants&spage=files&aid=' . $this->_iAssistantId;
-    }
-
-    public function getPageJsObject()
-    {
-        return 'oBxDolStudioPageAgents';
-    }
-
-    public function performActionAdd()
-    {
-        $sAction = 'add';
-
-        $aForm = $this->_getForm($sAction);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $aValsToAdd = ['assistant_id' => $this->_iAssistantId, 'added' => time()];
-
-            if(!empty($_FILES['file']['tmp_name'])) {
-                $sName = $_FILES['file']['name'];
-                $sMime = $_FILES['file']['type'];
-                $iSize = $_FILES['file']['size'];
-
-                $oHandle = fopen($_FILES['file']['tmp_name'], "r");
-                $sContents = fread($oHandle, $iSize);
-                fclose($oHandle);
-
-                $oAIModel = BxDolAI::getInstance()->getModelObject($this->_aAssistantInfo['model_id']);
-                if(($aFile = $oAIModel->callFiles(['content' => $sContents, 'name' => $sName, 'mime' => $sMime])) !== false) {
-                    if(($aResponse = $oAIModel->callVectorStoresFiles($this->_aAssistantInfo['ai_vs_id'], ['file_id' => $aFile['id']])) !== false) {
-                        $aValsToAdd = array_merge($aValsToAdd, [
-                            'name' => $sName,
-                            'ai_file_id' => $aFile['id'],
-                            'ai_file_size' => $iSize
-                        ]);
-                    }
-                }
-            }
-
-            if(($iId = $oForm->insert($aValsToAdd)) !== false)
-                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-            else
-                $aRes = ['msg' => _t('_sys_txt_error_occured')];
-
-            return echoJson($aRes);
-        }
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_assistants_files_popup_add'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    public function performActionSync()
-    {
-        $sAction = 'sync';
-
-        $oAIModel = BxDolAI::getInstance()->getModelObject($this->_aAssistantInfo['model_id']);
-        if(($aFiles = $oAIModel->callVectorStoresFilesList($this->_aAssistantInfo['ai_vs_id'])) === false)
-            return echoJson([]);
-
-        foreach($aFiles as $aFile) {
-            $aFileInfo = $oAIModel->callFilesRetrieve($aFile['id']);
-
-            $this->_oDb->updateFiles([
-                'ai_file_size' => is_array($aFileInfo) && !empty($aFileInfo['bytes']) ? $aFileInfo['bytes'] : 0, 
-                'ai_file_status' => $aFile['status']
-            ], ['ai_file_id' => $aFile['id']]);
-        }
-
-        return echoJson(['grid' => $this->getCode(false)]);
-    }
-    
-    public function performActionDelete()
-    {
-        $sAction = 'delete';
-
-        $iId = $this->_getId();
-
-        $aFile = $this->_oDb->getFilesBy(['sample' => 'id', 'id' => $iId]);
-        if(empty($aFile) || !is_array($aFile))
-            return echoJson([]);
-
-        return parent::performActionDelete();
-    }
-
-    protected function _getCellSize($mixedValue, $sKey, $aField, $aRow)
-    {
-        return parent::_getCellDefault(!empty($aRow['ai_file_size']) ? _t_format_size($aRow['ai_file_size']) : _t('_undefined'), $sKey, $aField, $aRow);
-    }
-
-    protected function _getCellStatus($mixedValue, $sKey, $aField, $aRow)
-    {
-        return parent::_getCellDefault(_t('_sys_agents_assistants_files_txt_status_' . $aRow['ai_file_status']), $sKey, $aField, $aRow);
-    }
-
-    protected function _getActionDelete($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = [])
-    {
-        if((int)$aRow['locked'] != 0) 
-            return '';
-
-        return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
-    }
-
-    protected function _delete ($mixedId)
-    {
-        $iFileId = (int)$mixedId;
-        $aFile = $this->_oDb->getFilesBy(['sample' => 'id', 'id' => $iFileId]);
-
-        $mixedResult = parent::_delete($mixedId);
-        if($mixedResult && !empty($aFile['ai_file_id'])) {
-            $oAi = BxDolAI::getInstance();
-
-            $oAIModel = $oAi->getModelObject($this->_aAssistantInfo['model_id']);
-            $oAIModel->callVectorStoresFilesDelete($this->_aAssistantInfo['ai_vs_id'], $aFile['ai_file_id']);
-            $oAIModel->callFilesDelete($aFile['ai_file_id']);
-        }
-
-        return $mixedResult;
-    }
-
-    protected function _getForm($sAction, $aFiles = [])
-    {
-        $sJsObject = $this->getPageJsObject();
-
-        $aForm = array(
-            'form_attrs' => array(
-                'id' => 'bx_std_agents_assistants_files_' . $sAction,
-                'action' => BX_DOL_URL_ROOT . bx_append_url_params('grid.php', ['o' => 'sys_studio_agents_assistants_files', 'a' => $sAction, 'aid' => $this->_iAssistantId]),
-                'method' => 'post',
-            ),
-            'params' => array (
-                'db' => array(
-                    'table' => 'sys_agents_assistants_files',
-                    'key' => 'id',
-                    'submit_name' => 'do_submit',
-                ),
-            ),
-            'inputs' => array(
-                'file' => [
-                    'type' => 'file',
-                    'name' => 'file',
-                    'required' => '1',
-                    'caption' => _t('_sys_agents_assistants_files_field_file'),
-                    'value' => '',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => '',
-                        'error' => _t('_sys_agents_form_field_err_enter'),
-                    ]
-                ],
-                'submit' => $this->_getFormControls(),
-            ),
-        );
-
-        return $aForm;
-    }
-}
-
-/** @} */

+ 0 - 790
studio/template/scripts/BxBaseStudioAgentsAutomators.php

@@ -1,790 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxBaseStudioAgentsAutomators extends BxDolStudioAgentsAutomators
-{
-    protected $_sUrlPage;
-    protected $_sFieldName;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sUrlPage = BX_DOL_URL_STUDIO . 'agents.php?page=automators';
-
-        $this->_sFieldName = 'name';
-    }
-
-    public function getPageJsObject()
-    {
-        return 'oBxDolStudioPageAgents';
-    }
-
-    public function performActionAdd()
-    {
-        $sAction = 'add';
-
-        $oTemplate = BxDolStudioTemplate::getInstance();
-        $oAI = BxDolAI::getInstance();
-
-        $aForm = $this->_getForm($sAction);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $aValsToAdd = [
-                'added' => time()
-            ];
-
-            $sName = $oForm->getCleanValue($this->_sFieldName);
-            $sName = $this->_getAutomatorName($sName);
-            BxDolForm::setSubmittedValue($this->_sFieldName, $sName, $oForm->aFormAttrs['method']);
-
-            $iProfileId = $oForm->getCleanValue('profile_id');
-            if(empty($iProfileId)) {
-                $iProfileId = (int)getParam('sys_agents_profile');
-                if(empty($iProfileId))
-                    $iProfileId = current(bx_srv('system', 'get_options_agents_profile', [false], 'TemplServices'))['key'];
-
-                $aValsToAdd['profile_id'] = $iProfileId;
-            }
-
-            $sSchedulerTime = $oForm->getCleanValue('scheduler_time');
-            if(!empty($sSchedulerTime))
-                $aValsToAdd['params'] = json_encode(['scheduler_time' => $sSchedulerTime]);
-  
-            $iModel = $oForm->getCleanValue('model_id');
-            $sType = $oForm->getCleanValue('type');
-            $aProviders = $oForm->getCleanValue('providers');
-            $bProviders = !empty($aProviders) && is_array($aProviders);
-            $aHelpers = $oForm->getCleanValue('helpers');
-            $bHelpers = !empty($aHelpers) && is_array($aHelpers);
-            $aAssistants = $oForm->getCleanValue('assistants');
-            $bAssistants = !empty($aAssistants) && is_array($aAssistants);
-            $sMessage = $oForm->getCleanValue('message');
-            $bMessage = !empty($sMessage);
-            $sMessageAdd = '';
-            $sMessageResponse = '';
-
-            $oAIModel = $oAI->getModelObject($iModel);
-
-            $sInstructions = $oAI->getAutomatorInstruction('profile', $iProfileId);
-            if($bProviders)
-                $sInstructions .= $oAI->getAutomatorInstruction('providers', $aProviders);
-            if($bHelpers)
-                $sInstructions .= $oAI->getAutomatorInstruction('helpers', $aHelpers);
-            if($bAssistants)
-                $sInstructions .= $oAI->getAutomatorInstruction('assistants', $aAssistants);
-
-            $bIsValid = true;
-            $aResponseInit = [];
-            switch($sType) {
-                case BX_DOL_AI_AUTOMATOR_EVENT:
-                    if(($oMessage = new BxDolAIMessageString('hb', $sMessage)) && ($aResponseInit = $oAIModel->getResponseInit($sType, $oMessage)) !== false) {
-                        $oAIModel->setParams($aResponseInit['params']);
-
-                        $sMessageAdd = $aResponseInit['params']['trigger'];
-
-                        $aValsToAdd = array_merge($aValsToAdd, $aResponseInit);
-                        if(!empty($aValsToAdd['params']) && is_array($aValsToAdd['params']))
-                            $aValsToAdd['params'] = json_encode($aValsToAdd['params']);
-                    }
-                    else {
-                        $oForm->aInputs['message']['error'] = _t('_sys_agents_automators_err_event_not_found');
-                        $bIsValid = false;
-                    }
-                    break;
-
-                case BX_DOL_AI_AUTOMATOR_SCHEDULER:
-                case BX_DOL_AI_AUTOMATOR_WEBHOOK:
-                    if(($oMessage = new BxDolAIMessageString('hb', $sMessage)) && ($aResponseInit = $oAIModel->getResponseInit($sType, $oMessage)) !== false) {
-                        $oAIModel->setParams($aResponseInit['params']);
-
-                        $aValsToAdd = array_merge($aValsToAdd, $aResponseInit);
-                        if(!empty($aValsToAdd['params']) && is_array($aValsToAdd['params']))
-                            $aValsToAdd['params'] = json_encode($aValsToAdd['params']);
-                    }
-                    else {
-                        $oForm->aInputs['message']['error'] = _t('_sys_agents_automators_err_event_not_found');
-                        $bIsValid = false;
-                    }
-                    break;
-            }
-
-            if($bIsValid) {
-                if(($oMessage = new BxDolAIMessageString('hb', $sMessage . $sInstructions . $sMessageAdd)) && ($sResponse = $oAIModel->getResponse($sType, $oMessage, $oAIModel->getParams())) !== false) {
-                    $sMessageResponse = $sResponse;
-                }
-                else {
-                    $oForm->aInputs['message']['error'] = _t('_sys_agents_automators_err_cannot_get_code');
-                    $bIsValid = false;
-                }
-            }
-
-            if($bIsValid) {
-                if(($iId = $oForm->insert($aValsToAdd)) !== false) {
-                    if($bProviders)
-                        foreach($aProviders as $iProviderId)
-                            $this->_oDb->insertAutomatorProvider([
-                                'automator_id' => $iId, 
-                                'provider_id' => $iProviderId
-                            ]);
-
-                    if($bHelpers)
-                        foreach($aHelpers as $iHelperId)
-                            $this->_oDb->insertAutomatorHelper([
-                                'automator_id' => $iId, 
-                                'helper_id' => $iHelperId
-                            ]);
-
-                    if($bAssistants)
-                        foreach($aAssistants as $iAssistantId)
-                            $this->_oDb->insertAutomatorAssistant([
-                                'automator_id' => $iId, 
-                                'assistant_id' => $iAssistantId
-                            ]);
-
-                    if(($oCmts = BxDolAI::getInstance()->getAutomatorCmtsObject($iId, $oTemplate)) !== null) {
-                        if($bMessage) {
-                            $iProfileId = bx_get_logged_profile_id();
-
-                            $aResult = $oCmts->addAuto([
-                                'cmt_author_id' => $iProfileId,
-                                'cmt_parent_id' => 0,
-                                'cmt_text' => $sMessage
-                            ]);
-
-                            if(!empty($aResult['id']))
-                                $this->_oDb->updateAutomators(['message_id' => $aResult['id']], ['id' => $iId]);
-
-                            sleep(1);
-                            $oCmts->addAuto([
-                                'cmt_author_id' => $iProfileId,
-                                'cmt_parent_id' => 0,
-                                'cmt_text' => $sInstructions
-                            ]);
-                        }
-
-                        if($sMessageResponse) {
-                            sleep(1);
-                            $oCmts->addAuto([
-                                'cmt_author_id' => $this->_iProfileIdAi,
-                                'cmt_parent_id' => 0,
-                                'cmt_text' => $sMessageResponse
-                            ]);
-                        }
-                    }
-
-                    $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-                }
-                else
-                    $aRes = ['msg' => _t('_sys_txt_error_occured')];
-
-                return echoJson($aRes);
-            }
-        }
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_automators_popup_add'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    public function performActionEdit()
-    {
-        $sAction = 'edit';
-
-        $oTemplate = BxDolStudioTemplate::getInstance();
-        $oAI = BxDolAI::getInstance();
-
-        $iId = $this->_getId();
-        $aAutomator = $oAI->getAutomator($iId);
-        $aAutomator['providers'] = $this->_oDb->getAutomatorsBy(['sample' => 'providers_by_id_pairs', 'id' => $iId]);
-        $aAutomator['helpers'] = $this->_oDb->getAutomatorsBy(['sample' => 'helpers_by_id_pairs', 'id' => $iId]);
-
-        $aForm = $this->_getFormEdit($sAction, $aAutomator);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $aValsToAdd = [];
-
-            $sName = $oForm->getCleanValue($this->_sFieldName);
-            if($aAutomator[$this->_sFieldName] != $sName) {
-                $sName = $this->_getAutomatorName($sName);
-                BxDolForm::setSubmittedValue($this->_sFieldName, $sName, $oForm->aFormAttrs['method']);
-            }
-
-            /**
-             * Process Providers
-             */
-            $aProvidersIds = $oForm->getCleanValue('providers_ids');
-            $bProvidersIds = !empty($aProvidersIds) && is_array($aProvidersIds);
-            $aProvidersValues = $oForm->getCleanValue('providers');
-            $bProvidersValues = !empty($aProvidersValues) && is_array($aProvidersValues);
-
-            //--- Providers: Remove deleted
-            if(!empty($aAutomator['providers']) && is_array($aAutomator['providers']))
-                $this->_oDb->deleteAutomatorProvidersById(array_diff(array_keys($aAutomator['providers']), $bProvidersIds ? $aProvidersIds : []));
-
-            //--- Providers: Update existed
-            if($bProvidersIds)
-                foreach($aProvidersIds as $iIndex => $iApId)
-                    $this->_oDb->updateAutomatorProvider(['provider_id' => (int)$aProvidersValues[$iIndex]], ['id' => (int)$iApId]);
-
-            //--- Providers: Add new
-            $iProvidersIds = $bProvidersIds ? count($aProvidersIds) : 0;
-            $iProvidersValues = $bProvidersValues ? count($aProvidersValues) : 0;
-            if($iProvidersValues > $iProvidersIds) {
-                $aProvidersValues = array_slice($aProvidersValues, $iProvidersIds);
-                foreach($aProvidersValues as $iProvidersValue)
-                    $this->_oDb->insertAutomatorProvider([
-                        'automator_id' => $iId,
-                        'provider_id' => (int)$iProvidersValue,
-                    ]);
-            }
-
-            /**
-             * Process Helpers
-             */
-            $aHelpersIds = $oForm->getCleanValue('helpers_ids');
-            $bHelpersIds = !empty($aHelpersIds) && is_array($aHelpersIds);
-            $aHelpersValues = $oForm->getCleanValue('helpers');
-            $bHelpersValues = !empty($aHelpersValues) && is_array($aHelpersValues);
-
-            //--- Helpers: Remove deleted
-            if(!empty($aAutomator['helpers']) && is_array($aAutomator['helpers']))
-                $this->_oDb->deleteAutomatorHelpersById(array_diff(array_keys($aAutomator['helpers']), $bHelpersIds ? $aHelpersIds : []));
-
-            //--- Helpers: Update existed
-            if($bHelpersIds)
-                foreach($aHelpersIds as $iIndex => $iAhId)
-                    $this->_oDb->updateAutomatorHelper(['helper_id' => (int)$aHelpersValues[$iIndex]], ['id' => (int)$iAhId]);
-
-            //--- Helpers: Add new
-            $iHelpersIds = $bHelpersIds ? count($aHelpersIds) : 0;
-            $iHelpersValues = $bHelpersValues ? count($aHelpersValues) : 0;
-            if($iHelpersValues > $iHelpersIds) {
-                $aHelpersValues = array_slice($aHelpersValues, $iHelpersIds);
-                foreach($aHelpersValues as $iHelpersValue)
-                    $this->_oDb->insertAutomatorHelper([
-                        'automator_id' => $iId,
-                        'helper_id' => (int)$iHelpersValue,
-                    ]);
-            }
-
-            /**
-             * Process Assistants
-             */
-            $aAssistantsIds = $oForm->getCleanValue('assistants_ids');
-            $bAssistantsIds = !empty($aAssistantsIds) && is_array($aAssistantsIds);
-            $aAssistantsValues = $oForm->getCleanValue('assistants');
-            $bAssistantsValues = !empty($aAssistantsValues) && is_array($aAssistantsValues);
-
-            //--- Assistants: Remove deleted
-            if(!empty($aAutomator['assistants']) && is_array($aAutomator['assistants']))
-                $this->_oDb->deleteAutomatorAssistantsById(array_diff(array_keys($aAutomator['assistants']), $bAssistantsIds ? $aAssistantsIds : []));
-
-            //--- Assistants: Update existed
-            if($bAssistantsIds)
-                foreach($aAssistantsIds as $iIndex => $iAhId)
-                    $this->_oDb->updateAutomatorAssistant(['assistant_id' => (int)$aAssistantsValues[$iIndex]], ['id' => (int)$iAhId]);
-
-            //--- Assistants: Add new
-            $iAssistantsIds = $bAssistantsIds ? count($aAssistantsIds) : 0;
-            $iAssistantsValues = $bAssistantsValues ? count($aAssistantsValues) : 0;
-            if($iAssistantsValues > $iAssistantsIds) {
-                $aAssistantsValues = array_slice($aAssistantsValues, $iAssistantsIds);
-                foreach($aAssistantsValues as $iAssistantsValue)
-                    $this->_oDb->insertAutomatorAssistant([
-                        'automator_id' => $iId,
-                        'assistant_id' => (int)$iAssistantsValue,
-                    ]);
-            }
-
-            $iProfileId = $oForm->getCleanValue('profile_id');
-            if(empty($iProfileId)) {
-                $iProfileId = (int)getParam('sys_agents_profile');
-                if(empty($iProfileId))
-                    $iProfileId = current(bx_srv('system', 'get_options_agents_profile', [false], 'TemplServices'))['key'];
-
-                $aValsToAdd['profile_id'] = $iProfileId;
-            }
-
-            $sSchedulerTime = $oForm->getCleanValue('scheduler_time');
-            if(!empty($sSchedulerTime))
-                $aValsToAdd['params'] = json_encode(['scheduler_time' => $sSchedulerTime]);
-
-            if($oForm->update($iId, $aValsToAdd) !== false) {
-                if(($oCmts = BxDolAI::getInstance()->getAutomatorCmtsObject($iId, $oTemplate)) !== null) {
-                    $sInstructions = $oAI->getAutomatorInstruction('profile', $iProfileId);
-
-                    $aProviders = $this->_oDb->getAutomatorsBy(['sample' => 'providers_by_id_pairs', 'id' => $iId]);
-                    if(!empty($aProviders) && is_array($aProviders))
-                        $sInstructions .= $oAI->getAutomatorInstruction('providers', array_values($aProviders));
-
-                    $aHelpers = $this->_oDb->getAutomatorsBy(['sample' => 'helpers_by_id_pairs', 'id' => $iId]);
-                    if(!empty($aHelpers) && is_array($aHelpers))
-                        $sInstructions .= $oAI->getAutomatorInstruction('helpers', array_values($aHelpers));
-
-                    $aAssistants = $this->_oDb->getAutomatorsBy(['sample' => 'assistants_by_id_pairs', 'id' => $iId]);
-                    if(!empty($aAssistants) && is_array($aAssistants))
-                        $sInstructions .= $oAI->getAutomatorInstruction('assistants', array_values($aAssistants));
-
-                    $oCmts->addAuto([
-                        'cmt_author_id' => $iProfileId,
-                        'cmt_parent_id' => 0,
-                        'cmt_text' => $sInstructions
-                    ]);
-
-                    if(($oMessage = new BxDolAIMessageString('hb', $sInstructions)) && ($sResponse = $oAI->getModelObject($aAutomator['model_id'])->getResponse($aAutomator['type'], $oMessage, $aAutomator['params'])) !== false) {
-                        sleep(1);
-                        $oCmts->addAuto([
-                            'cmt_author_id' => $this->_iProfileIdAi,
-                            'cmt_parent_id' => 0,
-                            'cmt_text' => $sResponse
-                        ]);
-                    }
-                }
-
-                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-            }
-            else
-                $aRes = ['msg' => _t('_sys_txt_error_occured')];
-
-            return echoJson($aRes);
-        } 
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_automators_popup_edit'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    protected function _getCellSwitcher ($mixedValue, $sKey, $aField, $aRow)
-    {
-        if(empty($aRow['code']) || $aRow['status'] != BX_DOL_AI_AUTOMATOR_STATUS_READY)
-            return parent::_getCellDefault('', $sKey, $aField, $aRow);
-
-        return parent::_getCellSwitcher ($mixedValue, $sKey, $aField, $aRow);
-    }
-
-    protected function _getCellType($mixedValue, $sKey, $aField, $aRow)
-    {
-        return parent::_getCellDefault(_t('_sys_agents_automators_field_type_' . $mixedValue), $sKey, $aField, $aRow);
-    }
-
-    protected function _getCellData($sKey, $aField, $aRow)
-    {
-        if($sKey == 'message_id' && ($iCmtId = (int)$aRow[$sKey]) != 0 && ($oCmts = BxDolAI::getInstance()->getAutomatorCmtsObject($aRow['id'])) !== false) {
-            $aCmt = $oCmts->getCommentSimple($iCmtId);
-            if(!empty($aCmt) && is_array($aCmt))
-                $aRow[$sKey] = $aCmt['cmt_text'];
-        }
-
-        return parent::_getCellData($sKey, $aField, $aRow);
-    }
-
-    protected function _getCellStatus($mixedValue, $sKey, $aField, $aRow)
-    {
-        return parent::_getCellDefault(_t('_sys_agents_automators_field_status_' . $mixedValue), $sKey, $aField, $aRow);
-    }
-    
-    protected function _delete ($mixedId)
-    {
-        $mixedResult = parent::_delete($mixedId);
-        if($mixedResult) {
-            $this->_oDb->deleteAutomatorProviders(['automator_id' => (int)$mixedId]);
-            $this->_oDb->deleteAutomatorHelpers(['automator_id' => (int)$mixedId]);
-            $this->_oDb->deleteAutomatorAssistants(['automator_id' => (int)$mixedId]);
-
-            if(($oCmts = BxDolAI::getInstance()->getAutomatorCmtsObject($mixedId)) !== false)
-                $oCmts->onObjectDelete();
-        }
-
-        return $mixedResult;
-    }
-
-    protected function _getFormEdit($sAction, $aAutomator = [])
-    {
-        $aForm = $this->_getForm($sAction, $aAutomator);
-        $aForm['form_attrs']['action'] .= '&id=' . $aAutomator['id'];
-
-        unset($aForm['inputs']['type']);
-        unset($aForm['inputs']['message']);
-
-        return $aForm;
-    }
-
-    protected function _getForm($sAction, $aAutomator = [])
-    {
-        $sJsObject = $this->getPageJsObject();
-
-        $sType = isset($aAutomator['type']) ? $aAutomator['type'] : 'event';
-
-        if(!empty($aAutomator['params']) && is_string($aAutomator['params']))
-            $aAutomator['params'] = json_decode($aAutomator['params'], true);
-
-        $aForm = array(
-            'form_attrs' => array(
-                'id' => 'bx_std_agents_automators_' . $sAction,
-                'action' => BX_DOL_URL_ROOT . 'grid.php?o=sys_studio_agents_automators&a=' . $sAction,
-                'method' => 'post',
-            ),
-            'params' => array (
-                'db' => array(
-                    'table' => 'sys_agents_automators',
-                    'key' => 'id',
-                    'submit_name' => 'do_submit',
-                ),
-            ),
-            'inputs' => array(
-                'name' => [
-                    'type' => 'text',
-                    'name' => 'name',
-                    'caption' => _t('_sys_agents_automators_field_name'),
-                    'value' => isset($aAutomator['name']) ? $aAutomator['name'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_form_field_err_enter'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ],
-                ],
-                'model_id' => [
-                    'type' => 'select',
-                    'name' => 'model_id',
-                    'caption' => _t('_sys_agents_automators_field_model_id'),
-                    'info' => '',
-                    'value' => isset($aAutomator['model_id']) ? $aAutomator['model_id'] : BxDolAI::getDefaultModel(),
-                    'values' => $this->_oDb->getModelsBy(['sample' => 'all_pairs', 'active' => 1, 'hidden' => 0]),
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_form_field_err_select'),
-                    ],
-                    'db' => [
-                        'pass' => 'Int',
-                    ],
-                ],
-                'profile_id' => [
-                    'type' => 'select',
-                    'name' => 'profile_id',
-                    'caption' => _t('_sys_agents_automators_field_profile_id'),
-                    'info' => _t('_sys_agents_automators_field_profile_id_inf'),
-                    'value' => isset($aAutomator['profile_id']) ? $aAutomator['profile_id'] : 0,
-                    'values' => bx_srv('system', 'get_options_agents_profile', [], 'TemplServices'),
-                    'required' => '0',
-                    'db' => [
-                        'pass' => 'Int',
-                    ]
-                ],
-                'type' => [
-                    'type' => 'select',
-                    'name' => 'type',
-                    'caption' => _t('_sys_agents_automators_field_type'),
-                    'info' => '',
-                    'value' => $sType,
-                    'values' => [
-                        'event' => _t('_sys_agents_automators_field_type_event'),
-                        'scheduler' => _t('_sys_agents_automators_field_type_scheduler'),
-                        'webhook' => _t('_sys_agents_automators_field_type_webhook'),
-                    ],
-                    'attrs' => [
-                        'onchange' => $this->getPageJsObject() . '.onChangeAutomatorType(this)',
-                    ],
-                    'required' => '1',
-                    'db' => [
-                        'pass' => 'Xss',
-                    ]
-                ],
-                'alert_unit' => [
-                    'type' => 'hidden',
-                    'name' => 'alert_unit',
-                    'caption' => _t('_sys_agents_automators_field_alert_unit'),
-                    'value' => isset($aAutomator['alert_unit']) ? $aAutomator['alert_unit'] : '',
-                    'tr_attrs' => [
-                        'style' => $sType != 'event' ? 'display:none' : ''
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ],
-                ],
-                'alert_action' => [
-                    'type' => 'hidden',
-                    'name' => 'alert_action',
-                    'caption' => _t('_sys_agents_automators_field_alert_action'),
-                    'value' => isset($aAutomator['alert_action']) ? $aAutomator['alert_action'] : '',
-                    'tr_attrs' => [
-                        'style' => $sType != 'event' ? 'display:none' : ''
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ],
-                ],
-                'scheduler_time' => [
-                    'type' => 'hidden',
-                    'name' => 'scheduler_time',
-                    'caption' => _t('_sys_agents_automators_field_scheduler_time'),
-                    'value' => isset($aAutomator['params']['scheduler_time']) ? $aAutomator['params']['scheduler_time'] : '',
-                    'tr_attrs' => [
-                        'style' => $sType != 'scheduler' ? 'display:none' : ''
-                    ],
-                ],
-                'providers' => [
-                    'type' => 'custom',
-                    'name' => 'providers',
-                    'caption' => _t('_sys_agents_automators_field_providers'),
-                    'value' => '',
-                ],
-                'helpers' => [
-                    'type' => 'custom',
-                    'name' => 'helpers',
-                    'caption' => _t('_sys_agents_automators_field_helpers'),
-                    'value' => '',
-                ],
-                'assistants' => [
-                    'type' => 'custom',
-                    'name' => 'assistants',
-                    'caption' => _t('_sys_agents_automators_field_assistants'),
-                    'value' => '',
-                ],
-                'message' => [
-                    'type' => 'textarea',
-                    'name' => 'message',
-                    'caption' => _t('_sys_agents_automators_field_message'),
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_automators_field_message_err'),
-                    ],
-                ],
-                'submit' => $this->_getFormControls(),
-
-            ),
-        );
-        
-        if(isset($aForm['inputs']['providers'])) {
-            $oForm = new BxTemplFormView([]);
-
-            $sProviders = 'providers';
-            $aProviders = array_map(function($sTitle) {
-                return _t($sTitle);
-            }, ['' => '_sys_please_select'] + $this->_oDb->getProvidersBy(['sample' => 'all_pairs', 'active' => 1]));
-
-            $aTmplVarsProviders = [];
-            if(!empty($aAutomator[$sProviders]) && is_array($aAutomator[$sProviders])) {
-                foreach($aAutomator[$sProviders] as $iApId => $iProviderId) {
-                    $aInputSelect = [
-                        'type' => 'select',
-                        'name' => $sProviders . '[]',
-                        'values' => $aProviders,
-                        'value' => (int)$iProviderId,
-                        'attrs' => [
-                            'class' => 'bx-def-margin-sec-top-auto'
-                        ]
-                    ];
-                    $sInput = $oForm->genInput($aInputSelect);
-
-                    $aInputHidden = [
-                        'type' => 'hidden',
-                        'name' => $sProviders . '_ids[]',
-                        'value' => (int)$iApId,
-                    ];
-                    $sInput .= $oForm->genInput($aInputHidden);
-
-                    $aTmplVarsProviders[] = ['js_object' => $sJsObject, 'input_select' => $sInput];
-                }
-            }
-            else  {
-                $aInputSelect = [
-                    'type' => 'select',
-                    'name' => $sProviders . '[]',
-                    'values' => $aProviders,
-                    'value' => '',
-                    'attrs' => [
-                        'class' => 'bx-def-margin-sec-top-auto'
-                    ]
-                ];
-
-                $aTmplVarsProviders = [
-                    ['js_object' => $sJsObject, 'input_select' => $oForm->genInput($aInputSelect)],
-                ];
-            }
-        
-            $aInputButton = [
-                'type' => 'button',
-                'name' => $sProviders . '_add',
-                'value' => _t('_sys_agents_automators_field_providers_add'),
-                'attrs' => [
-                    'class' => 'bx-def-margin-sec-top',
-                    'onclick' => $sJsObject . ".providerAdd(this, '" . $sProviders . "');"
-                ]
-            ];
-
-            $aForm['inputs'][$sProviders]['content'] = $this->_oTemplate->parseHtmlByName('agents_automator_form_providers.html', [
-                'bx_repeat:providers' => $aTmplVarsProviders,
-                'btn_add' => $oForm->genInputButton($aInputButton)
-            ]);
-        }
-
-        if(isset($aForm['inputs']['helpers'])) {
-            $oForm = new BxTemplFormView([]);
-
-            $sHelpers = 'helpers';
-            $aHelpers = array_map(function($sTitle) {
-                return _t($sTitle);
-            }, ['' => '_sys_please_select'] + $this->_oDb->getHelpersBy(['sample' => 'all_pairs', 'active' => 1]));
-
-            $aTmplVarsHelpers = [];
-            if(!empty($aAutomator[$sHelpers]) && is_array($aAutomator[$sHelpers])) {
-                foreach($aAutomator[$sHelpers] as $iAhId => $iHelperId) {
-                    $aInputSelect = [
-                        'type' => 'select',
-                        'name' => $sHelpers . '[]',
-                        'values' => $aHelpers,
-                        'value' => (int)$iHelperId,
-                        'attrs' => [
-                            'class' => 'bx-def-margin-sec-top-auto'
-                        ]
-                    ];
-                    $sInput = $oForm->genInput($aInputSelect);
-
-                    $aInputHidden = [
-                        'type' => 'hidden',
-                        'name' => $sHelpers . '_ids[]',
-                        'value' => (int)$iAhId,
-                    ];
-                    $sInput .= $oForm->genInput($aInputHidden);
-
-                    $aTmplVarsHelpers[] = ['js_object' => $sJsObject, 'input_select' => $sInput];
-                }
-            }
-            else  {
-                $aInputSelect = [
-                    'type' => 'select',
-                    'name' => $sHelpers . '[]',
-                    'values' => $aHelpers,
-                    'value' => '',
-                    'attrs' => [
-                        'class' => 'bx-def-margin-sec-top-auto'
-                    ]
-                ];
-
-                $aTmplVarsHelpers = [
-                    ['js_object' => $sJsObject, 'input_select' => $oForm->genInput($aInputSelect)],
-                ];
-            }
-        
-            $aInputButton = [
-                'type' => 'button',
-                'name' => $sHelpers . '_add',
-                'value' => _t('_sys_agents_automators_field_helpers_add'),
-                'attrs' => [
-                    'class' => 'bx-def-margin-sec-top',
-                    'onclick' => $sJsObject . ".helperAdd(this, '" . $sHelpers . "');"
-                ]
-            ];
-
-            $aForm['inputs'][$sHelpers]['content'] = $this->_oTemplate->parseHtmlByName('agents_automator_form_helpers.html', [
-                'bx_repeat:helpers' => $aTmplVarsHelpers,
-                'btn_add' => $oForm->genInputButton($aInputButton)
-            ]);
-        }
-
-        if(isset($aForm['inputs']['assistants'])) {
-            $oForm = new BxTemplFormView([]);
-
-            $sAssistants = 'assistants';
-            $aAssistants = array_map(function($sTitle) {
-                return _t($sTitle);
-            }, ['' => '_sys_please_select'] + $this->_oDb->getAssistantsBy(['sample' => 'all_pairs', 'active' => 1]));
-
-            $aTmplVarsAssistants = [];
-            if(!empty($aAutomator[$sAssistants]) && is_array($aAutomator[$sAssistants])) {
-                foreach($aAutomator[$sAssistants] as $iAhId => $iAssistantId) {
-                    $aInputSelect = [
-                        'type' => 'select',
-                        'name' => $sAssistants . '[]',
-                        'values' => $aAssistants,
-                        'value' => (int)$iAssistantId,
-                        'attrs' => [
-                            'class' => 'bx-def-margin-sec-top-auto'
-                        ]
-                    ];
-                    $sInput = $oForm->genInput($aInputSelect);
-
-                    $aInputHidden = [
-                        'type' => 'hidden',
-                        'name' => $sAssistants . '_ids[]',
-                        'value' => (int)$iAhId,
-                    ];
-                    $sInput .= $oForm->genInput($aInputHidden);
-
-                    $aTmplVarsAssistants[] = ['js_object' => $sJsObject, 'input_select' => $sInput];
-                }
-            }
-            else  {
-                $aInputSelect = [
-                    'type' => 'select',
-                    'name' => $sAssistants . '[]',
-                    'values' => $aAssistants,
-                    'value' => '',
-                    'attrs' => [
-                        'class' => 'bx-def-margin-sec-top-auto'
-                    ]
-                ];
-
-                $aTmplVarsAssistants = [
-                    ['js_object' => $sJsObject, 'input_select' => $oForm->genInput($aInputSelect)],
-                ];
-            }
-        
-            $aInputButton = [
-                'type' => 'button',
-                'name' => $sAssistants . '_add',
-                'value' => _t('_sys_agents_automators_field_assistants_add'),
-                'attrs' => [
-                    'class' => 'bx-def-margin-sec-top',
-                    'onclick' => $sJsObject . ".assistantAdd(this, '" . $sAssistants . "');"
-                ]
-            ];
-
-            $aForm['inputs'][$sAssistants]['content'] = $this->_oTemplate->parseHtmlByName('agents_automator_form_assistants.html', [
-                'bx_repeat:assistants' => $aTmplVarsAssistants,
-                'btn_add' => $oForm->genInputButton($aInputButton)
-            ]);
-        }
-
-        return $aForm;
-    }
-
-    protected function _getAutomatorName($sName)
-    {
-        return uriGenerate($sName, 'sys_agents_automators', 'name', ['lowercase' => false]);
-    }
-}
-
-/** @} */

+ 0 - 243
studio/template/scripts/BxBaseStudioAgentsHelpers.php

@@ -1,243 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxBaseStudioAgentsHelpers extends BxDolStudioAgentsHelpers
-{
-    protected $_sUrlPage;
-    protected $_sFieldName;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sUrlPage = BX_DOL_URL_STUDIO . 'agents.php?page=helpers';
-
-        $this->_sFieldName = 'name';
-    }
-
-    public function getPageJsObject()
-    {
-        return 'oBxDolStudioPageAgents';
-    }
-
-    public function performActionAdd()
-    {
-        $sAction = 'add';
-
-        $aForm = $this->_getForm($sAction);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $aValsToAdd = ['added' => time()];
-
-            $sName = $oForm->getCleanValue($this->_sFieldName);
-            $sName = $this->_getHelperName($sName);
-            BxDolForm::setSubmittedValue($this->_sFieldName, $sName, $oForm->aFormAttrs['method']);
-
-            $iProfileId = $oForm->getCleanValue('profile_id');
-            if(empty($iProfileId)) {
-                $iProfileId = (int)getParam('sys_agents_profile');
-                if(empty($iProfileId))
-                    $iProfileId = current(bx_srv('system', 'get_options_agents_profile', [false], 'TemplServices'))['key'];
-
-                $aValsToAdd['profile_id'] = $iProfileId;
-            }
-
-            $bIsValid = true;
-            if($bIsValid) {
-                if(($iId = $oForm->insert($aValsToAdd)) !== false) {
-                    $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-                }
-                else
-                    $aRes = ['msg' => _t('_sys_txt_error_occured')];
-
-                return echoJson($aRes);
-            }
-        }
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_helpers_popup_add'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    public function performActionEdit()
-    {
-        $sAction = 'edit';
-
-        $iId = $this->_getId();
-        $aHelper = $this->_oDb->getHelpersBy(['sample' => 'id', 'id' => $iId]);
-
-        $aForm = $this->_getFormEdit($sAction, $aHelper);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $aValsToAdd = [];
-
-            $sName = $oForm->getCleanValue($this->_sFieldName);
-            if($aHelper[$this->_sFieldName] != $sName) {
-                $sName = $this->_getHelperName($sName);
-                BxDolForm::setSubmittedValue($this->_sFieldName, $sName, $oForm->aFormAttrs['method']);
-            }
-
-            $iProfileId = $oForm->getCleanValue('profile_id');
-            if(empty($iProfileId)) {
-                $iProfileId = (int)getParam('sys_agents_profile');
-                if(empty($iProfileId))
-                    $iProfileId = current(bx_srv('system', 'get_options_agents_profile', [false], 'TemplServices'))['key'];
-
-                $aValsToAdd['profile_id'] = $iProfileId;
-            }
-            
-            if($oForm->update($iId, $aValsToAdd) !== false)
-                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-            else
-                $aRes = ['msg' => _t('_sys_txt_error_occured')];
-
-            return echoJson($aRes);
-        } 
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_helpers_popup_edit'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    protected function _delete ($mixedId)
-    {
-        $mixedResult = parent::_delete($mixedId);
-        if($mixedResult)
-            $this->_oDb->deleteAutomatorHelpers(['helper_id' => (int)$mixedId]);
-
-        return $mixedResult;
-    }
-
-    protected function _getFormEdit($sAction, $aHelper = [])
-    {
-        $aForm = $this->_getForm($sAction, $aHelper);
-        $aForm['form_attrs']['action'] .= '&id=' . $aHelper['id'];
-
-        return $aForm;
-    }
-
-    protected function _getForm($sAction, $aHelper = [])
-    {
-        $sJsObject = $this->getPageJsObject();
-
-        $aForm = array(
-            'form_attrs' => array(
-                'id' => 'bx_std_agents_helpers_' . $sAction,
-                'action' => BX_DOL_URL_ROOT . 'grid.php?o=sys_studio_agents_helpers&a=' . $sAction,
-                'method' => 'post',
-            ),
-            'params' => array (
-                'db' => array(
-                    'table' => 'sys_agents_helpers',
-                    'key' => 'id',
-                    'submit_name' => 'do_submit',
-                ),
-            ),
-            'inputs' => array(
-                'name' => [
-                    'type' => 'text',
-                    'name' => 'name',
-                    'required' => '1',
-                    'caption' => _t('_sys_agents_helpers_field_name'),
-                    'value' => isset($aHelper['name']) ? $aHelper['name'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_form_field_err_enter'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ]
-                ],
-                'model_id' => [
-                    'type' => 'select',
-                    'name' => 'model_id',
-                    'caption' => _t('_sys_agents_helpers_field_model_id'),
-                    'info' => '',
-                    'value' => isset($aHelper['model_id']) ? $aHelper['model_id'] : BxDolAI::getDefaultModel(),
-                    'values' => $this->_oDb->getModelsBy(['sample' => 'all_pairs', 'active' => 1, 'hidden' => 0]),
-                    'required' => '1',
-                    'db' => [
-                        'pass' => 'Int',
-                    ]
-                ],
-                'profile_id' => [
-                    'type' => 'select',
-                    'name' => 'profile_id',
-                    'caption' => _t('_sys_agents_helpers_field_profile_id'),
-                    'info' => _t('_sys_agents_helpers_field_profile_id_inf'),
-                    'value' => isset($aHelper['profile_id']) ? $aHelper['profile_id'] : 0,
-                    'values' => bx_srv('system', 'get_options_agents_profile', [], 'TemplServices'),
-                    'required' => '0',
-                    'db' => [
-                        'pass' => 'Int',
-                    ]
-                ],
-                'description' => [
-                    'type' => 'textarea',
-                    'name' => 'description',
-                    'caption' => _t('_sys_agents_helpers_field_description'),
-                    'value' => isset($aHelper['description']) ? $aHelper['description'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_form_field_err_enter'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ]
-                ],
-                'prompt' => [
-                    'type' => 'textarea',
-                    'name' => 'prompt',
-                    'caption' => _t('_sys_agents_helpers_field_prompt'),
-                    'value' => isset($aHelper['prompt']) ? $aHelper['prompt'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_helpers_field_prompt_err'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ]
-                ],
-                'submit' => $this->_getFormControls(),
-            ),
-        );
-
-        return $aForm;
-    }
-
-    protected function _getHelperName($sName)
-    {
-        return uriGenerate($sName, 'sys_agents_helpers', 'name', ['lowercase' => false]);
-    }
-}
-
-/** @} */

+ 1 - 1
studio/template/scripts/BxBaseStudioAgentsLogs.php

@@ -8,7 +8,7 @@
  * @{
  */
 
-class BxBaseStudioAgentsLogs extends BxDolStudioAgentsGrid
+class BxBaseStudioAgentsLogs extends BxTemplStudioGridAgents
 {
     public function __construct ($aOptions, $oTemplate = false)
     {

+ 2 - 2
studio/template/scripts/BxBaseStudioAgentsModels.php

@@ -8,7 +8,7 @@
  * @{
  */
 
-class BxBaseStudioAgentsModels extends BxDolStudioAgentsGrid
+class BxBaseStudioAgentsModels extends BxTemplStudioGridAgents
 {
     protected $_sUrlPage;
 
@@ -67,7 +67,7 @@ class BxBaseStudioAgentsModels extends BxDolStudioAgentsGrid
         } 
 
         $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_models_popup_edit'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
+        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup', _t('_sys_agents_models_popup_edit'), $this->_oTemplate->parseHtmlByName('agents_agents_form.html', [
             'form_id' => $sFormId,
             'form' => $oForm->getCode(true),
             'object' => $this->_sObject,

+ 0 - 291
studio/template/scripts/BxBaseStudioAgentsProviders.php

@@ -1,291 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxBaseStudioAgentsProviders extends BxDolStudioAgentsProviders
-{
-    protected $_sUrlPage;
-    protected $_sFieldName;
-
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-
-        $this->_sUrlPage = BX_DOL_URL_STUDIO . 'agents.php?page=providers';
-
-        $this->_sFieldName = 'name';
-    }
-
-    public function getPageJsObject()
-    {
-        return 'oBxDolStudioPageAgents';
-    }
-
-    public function performActionAdd()
-    {
-        $sAction = 'add';
-
-        $aProvider = [];
-        if(($iTypeId = bx_get('type_id')) !== false && $iTypeId > 0) {
-            $aProvider = [
-                'type_id' => (int)$iTypeId,
-                'options' => $this->_oDb->getProviderOptionsBy(['sample' => 'provider_type_id', 'provider_type_id' => $iTypeId])
-            ];
-        }
-
-        $aForm = $this->_getForm($sAction, $aProvider);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $aValsToAdd = [
-                'profile_id' => bx_get_logged_profile_id(), 
-                'added' => time(), 
-                'active' => 1
-            ];
-
-            $sName = $oForm->getCleanValue($this->_sFieldName);
-            $sName = $this->_getProviderName($sName);
-            BxDolForm::setSubmittedValue($this->_sFieldName, $sName, $oForm->aFormAttrs['method']);
-
-            if(($iId = $oForm->insert($aValsToAdd)) !== false) {
-                if(!empty($aProvider['options']) && is_array($aProvider['options']))
-                    foreach($aProvider['options'] as $aOption)
-                        $this->_oDb->insertProviderValue([
-                            'provider_id' => $iId, 
-                            'option_id' => $aOption['id'],
-                            'value' => $oForm->getCleanValue($aOption['name'])
-                        ]);
-
-                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
-            }
-            else
-                $aRes = ['msg' => _t('_sys_txt_error_occured')];
-
-            return echoJson($aRes);
-        }
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup_' . $sAction, _t('_sys_agents_providers_popup_add'), $this->_oTemplate->parseHtmlByName('agents_provider_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    public function performActionEdit()
-    {
-        $sAction = 'edit';
-
-        $iId = $this->_getId();
-
-        $oAIProvider = BxDolAI::getInstance()->getProviderObject($iId);
-        if(!$oAIProvider)
-            return echoJson([]);
-
-        $aProviderInfo = $oAIProvider->getInfo();
-
-        $aForm = $this->_getFormEdit($sAction, $aProviderInfo);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker();
-
-        if($oForm->isSubmittedAndValid()) {
-            $sName = $oForm->getCleanValue($this->_sFieldName);
-            if($aProviderInfo[$this->_sFieldName] != $sName) {
-                $sName = $this->_getProviderName($sName);
-                BxDolForm::setSubmittedValue($this->_sFieldName, $sName, $oForm->aFormAttrs['method']);
-            }
-
-            if($oForm->update($iId) === false)
-                return echoJson(['msg' => _t('_sys_txt_error_occured')]);
-
-            if(!empty($aProviderInfo['options']) && is_array($aProviderInfo['options']))
-                foreach($aProviderInfo['options'] as $aOption)
-                    $this->_oDb->insertProviderValue([
-                        'provider_id' => $iId, 
-                        'option_id' => $aOption['id'],
-                        'value' => $oForm->getCleanValue($aOption['name'])
-                    ]);
-
-            return echoJson(['grid' => $this->getCode(false), 'blink' => $iId]);
-        } 
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup_' . $sAction, _t('_sys_agents_providers_popup_edit'), $this->_oTemplate->parseHtmlByName('agents_provider_form.html', [
-            'form_id' => $sFormId,
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
-    }
-
-    public function performActionInfo()
-    {
-        $sAction = 'view';
-
-        $iId = $this->_getId();
-
-        $oAIProvider = BxDolAI::getInstance()->getProviderObject($iId);
-        if(!$oAIProvider)
-            return echoJson([]);
-
-        $aProviderInfo = $oAIProvider->getInfo();
-
-        $aForm = $this->_getFormView($sAction, $aProviderInfo);
-        $oForm = new BxTemplFormView($aForm);
-        $oForm->initChecker($aProviderInfo);
-
-        $sFormId = $oForm->getId();
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup_' . $sAction, _t('_sys_agents_providers_popup_view'), $this->_oTemplate->parseHtmlByName('agents_provider_form.html', [
-            'form_id' => $oForm->getId(),
-            'form' => $oForm->getCode(true),
-            'object' => $this->_sObject,
-            'action' => $sAction
-        ]));
-
-        echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => true, 'removeOnClose' => true]]]);
-    }
-
-    protected function _delete ($mixedId)
-    {
-        $mixedResult = parent::_delete($mixedId);
-        if($mixedResult) {
-            $this->_oDb->deleteProviderValues(['provider_id' => (int)$mixedId]);
-
-            $this->_oDb->deleteAutomatorProviders(['provider_id' => (int)$mixedId]);
-        }
-
-        return $mixedResult;
-    }
-
-    protected function _getFormView($sAction, $aProvider = [])
-    {
-        $aForm = $this->_getForm($sAction, $aProvider);
-        $aForm['params']['view_mode'] = 1;
-
-        return $aForm;
-    }
-    
-    protected function _getFormEdit($sAction, $aProvider = [])
-    {
-        $aForm = $this->_getForm($sAction, $aProvider);
-        $aForm['form_attrs']['action'] .= '&id=' . $aProvider['id'];
-
-        return $aForm;
-    }
-
-    protected function _getForm($sAction, $aProvider = [])
-    {
-        $bProvider = !empty($aProvider) && is_array($aProvider);
-
-        $aForm = [
-            'form_attrs' => [
-                'id' => 'bx_std_agents_providers_' . $sAction,
-                'action' => BX_DOL_URL_ROOT . 'grid.php?o=sys_studio_agents_providers&a=' . $sAction,
-                'method' => 'post',
-            ],
-            'params' => [
-                'db' => [
-                    'table' => 'sys_agents_providers',
-                    'key' => 'id',
-                    'submit_name' => 'do_submit',
-                ],
-            ],
-            'inputs' => [
-                'controls' => $this->_getFormControls(),
-            ],
-        ];
-                    
-        if($sAction == 'add' && !$bProvider) {
-            $aForm['inputs'] = bx_array_insert_before([
-                'type_id' => [
-                    'type' => 'select',
-                    'name' => 'type_id',
-                    'caption' => _t('_sys_agents_providers_field_type_id'),
-                    'info' => '',
-                    'value' => isset($aProvider['type_id']) ? $aProvider['type_id'] : 0,
-                    'values' => array_map(function($sTitle) {
-                        return _t($sTitle);
-                    }, ['' => '_sys_please_select'] + $this->_oDb->getProviderTypesBy(['sample' => 'all_pairs', 'active' => 1])),
-                    'required' => '1',
-                    'db' => [
-                        'pass' => 'Int',
-                    ]
-                ],
-            ], $aForm['inputs'], 'controls');
-
-            $aForm['inputs']['controls'][0] = array_merge($aForm['inputs']['controls'][0], [
-                'name' => 'do_select',
-                'value' => _t('_sys_select')
-            ]);
-        }
-        else {
-            $aInputsAdd = [
-                'type_id' => [
-                    'type' => 'hidden',
-                    'name' => 'type_id',
-                    'value' => isset($aProvider['type_id']) ? $aProvider['type_id'] : 0,
-                    'db' => [
-                        'pass' => 'Int',
-                    ]
-                ],
-                'name' => [
-                    'type' => 'text',
-                    'name' => 'name',
-                    'caption' => _t('_sys_agents_providers_field_name'),
-                    'value' => isset($aProvider['name']) ? $aProvider['name'] : '',
-                    'required' => '1',
-                    'checker' => [
-                        'func' => 'Avail',
-                        'params' => [],
-                        'error' => _t('_sys_agents_form_field_err_enter'),
-                    ],
-                    'db' => [
-                        'pass' => 'Xss',
-                    ],
-                ],
-            ];
-
-            if(!empty($aProvider['options']) && is_array($aProvider['options']))
-                foreach($aProvider['options'] as $aOption) {
-                    if($aOption['type'] == 'value' && empty($aOption['value']))
-                        continue;
-
-                    $aInputsAdd[$aOption['name']] = [
-                        'type' => $aOption['type'],
-                        'name' => $aOption['name'],
-                        'caption' => _t($aOption['title']),
-                        'info' => _t($aOption['description']),
-                        'value' => isset($aOption['value']) ? $aOption['value'] : '',
-                        'checker' => !empty($aOption['check_type']) ? [
-                            'func' => $aOption['check_type'],
-                            'params' => !empty($aOption['check_params']) ? unserialize($aOption['check_params']) : '',
-                            'error' => _t($aOption['check_error']),
-                        ] : null,
-                    ];
-                }
-
-            $aForm['inputs'] = bx_array_insert_before($aInputsAdd, $aForm['inputs'], 'controls');
-        }
-
-        return $aForm;
-    }
-
-    protected function _getProviderName($sName)
-    {
-        return uriGenerate($sName, 'sys_agents_providers', 'name', ['lowercase' => false]);
-    }
-}
-
-/** @} */

+ 2 - 2
studio/template/scripts/BxBaseStudioAgentsVectorStore.php

@@ -77,7 +77,7 @@ class BxBaseStudioAgentsVectorStore extends BxDolStudioAgentsInstruments
 
         $sFormId = $oForm->getId();
         $sForm = $oForm->getCode(true);
-        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup_' . $sAction, _t('_sys_agents_vector_store_popup_add_data'), $this->_oTemplate->parseHtmlByName('agents_automator_form.html', [
+        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($sFormId . '_popup_' . $sAction, _t('_sys_agents_vector_store_popup_add_data'), $this->_oTemplate->parseHtmlByName('agents_agents_form.html', [
             'form_id' => $sFormId,
             'form' => $sForm,
             'object' => $this->_sObject,
@@ -125,7 +125,7 @@ class BxBaseStudioAgentsVectorStore extends BxDolStudioAgentsInstruments
         $sDocs = $oParsedown->text($aVectorStore['docs']);
 
         $aEmbeddingProviders = ['' => _t('_sys_please_select')];
-        $a = BxDolAI::getInstance()->getModels(['capabilities' => 'embeddings', 'active' => true]);
+        $a = BxDolAi::getInstance()->getModels(['capabilities' => 'embeddings', 'active' => true]);
         foreach ($a as $k => $v) {
             $aEmbeddingProviders[$k] = $v;
         }

+ 1 - 1
studio/template/scripts/BxBaseStudioAgentsVectorStoreData.php

@@ -8,7 +8,7 @@
  * @{
  */
 
-class BxBaseStudioAgentsVectorStoreData extends BxDolStudioAgentsGrid
+class BxBaseStudioAgentsVectorStoreData extends BxTemplStudioGridAgents
 {
     public function __construct ($aOptions, $oTemplate = false)
     {

+ 1 - 1
studio/template/scripts/BxBaseStudioGridAgents.php

@@ -8,7 +8,7 @@
  * @{
  */
 
-class BxBaseStudioGridAgents extends BxTemplStudioGrid
+class BxBaseStudioGridAgents extends BxDolStudioGridAgents
 {
     public function __construct($aOptions, $oTemplate = false)
     {

+ 3 - 10
studio/template/scripts/BxBaseStudioPage.php

@@ -274,17 +274,10 @@ class BxBaseStudioPage extends BxDolStudioPage
         if(!$this->_bShowHeaderRightAssistant)
             return '';
 
-        $oAssistant = BxDolAIAssistant::getObjectInstance($this->iPageAssistantId);
-        if(!$oAssistant)
-            return '';
-
-        $oTemplate = BxDolStudioTemplate::getInstance();
-
-        $sContent = $oAssistant->getAskChat($this->sPageAssistantChatName, $this->sPageAssistantChatDescription, '', $oTemplate);
-        if(!$sContent)
-            return '';
+        //TODO: Use new Manual agent here.
+        $sContent = '';
 
-        return $oTemplate->parseHtmlByName('page_caption_assistant.html', [
+        return BxDolStudioTemplate::getInstance()->parseHtmlByName('page_caption_assistant.html', [
             'content' => $sContent
         ]);
     }

+ 0 - 19
studio/template/scripts_templ/BxTemplStudioAgentsAssistants.php

@@ -1,19 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudioView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxTemplStudioAgentsAssistants extends BxBaseStudioAgentsAssistants
-{
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-    }
-}
-
-/** @} */

+ 0 - 19
studio/template/scripts_templ/BxTemplStudioAgentsAsstChats.php

@@ -1,19 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudioView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxTemplStudioAgentsAsstChats extends BxBaseStudioAgentsAsstChats
-{
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-    }
-}
-
-/** @} */

+ 0 - 19
studio/template/scripts_templ/BxTemplStudioAgentsAsstFiles.php

@@ -1,19 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudioView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxTemplStudioAgentsAsstFiles extends BxBaseStudioAgentsAsstFiles
-{
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-    }
-}
-
-/** @} */

+ 0 - 19
studio/template/scripts_templ/BxTemplStudioAgentsAutomators.php

@@ -1,19 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudioView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxTemplStudioAgentsAutomators extends BxBaseStudioAgentsAutomators
-{
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-    }
-}
-
-/** @} */

+ 0 - 19
studio/template/scripts_templ/BxTemplStudioAgentsHelpers.php

@@ -1,19 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudioView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxTemplStudioAgentsHelpers extends BxBaseStudioAgentsHelpers
-{
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-    }
-}
-
-/** @} */

+ 0 - 19
studio/template/scripts_templ/BxTemplStudioAgentsProviders.php

@@ -1,19 +0,0 @@
-<?php defined('BX_DOL') or die('hack attempt');
-/**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup    UnaStudioView UNA Studio Representation classes
- * @ingroup     UnaStudio
- * @{
- */
-
-class BxTemplStudioAgentsProviders extends BxBaseStudioAgentsProviders
-{
-    public function __construct ($aOptions, $oTemplate = false)
-    {
-        parent::__construct ($aOptions, $oTemplate);
-    }
-}
-
-/** @} */

+ 0 - 5
template/agents_comment_actions_assistant.html

@@ -1,5 +0,0 @@
-<div class="__style_prefix__-actions mt-2">
-    <a class="bx-btn bx-btn-small" href="javascript:void(0)" onclick="javascript:__js_object__.cmtRemove(this, __id__)">
-        <bx_text:_sys_menu_item_title_cmts_item_delete />
-    </a>
-</div>

+ 0 - 5
template/agents_comment_actions_automator.html

@@ -1,5 +0,0 @@
-<div class="__style_prefix__-actions mt-2">
-    <a class="bx-btn bx-btn-small bx-btn-primary" href="javascript:void(0)" onclick="javascript:__js_object__.approveCode(this, __id__)">
-        <bx_text:_sys_agents_automators_btn_approve />
-    </a>
-</div>

+ 0 - 10
template/agents_comment_content.html

@@ -1,10 +0,0 @@
-<div class="__style_prefix__-body bx-def-vanilla-html bx-def-vh-sm relative mt-2 max-w-none text-clip">
-    __text__
-    <div class="__style_prefix__-body-overflow bx-def-color-bg-box-overflow absolute bottom-0 w-full h-1/5"></div>
-</div>
-<bx_if:show_attached>
-    <div class="__style_prefix__-attached bx-clearfix -mx-1 -mb-1">__attached__</div>
-</bx_if:show_attached>
-<bx_if:show_counters>
-    <div class="__style_prefix__-counters -mx-1 mt-1">__counters__</div>
-</bx_if:show_counters>

+ 0 - 14
template/agents_comment_loading.html

@@ -1,14 +0,0 @@
-<div id="__html_id__" class="__style_prefix__-agents-loading">
-    <bx_text:_sys_agents_assistants_chats_msg_loading />
-</div>
-<script language="javascript">
-    bx_loading('__html_id__', true); 
-    
-    var __js_object___interval = setInterval(function() {
-        if(window['__js_object__'] == undefined) 
-            return; 
-        
-        clearInterval(__js_object___interval); 
-        __js_object__._getCmt('#cmt__id__', __id__);
-    }, 200);
-</script>

+ 1 - 1
template/scripts/BxBaseSearch.php

@@ -140,7 +140,7 @@ class BxBaseSearch extends BxDolSearch
             unset($aForm['inputs']['section']);
             unset($aForm['inputs']['search']);
             unset($aForm['inputs']['keyword']['caption']);
-            $aForm['inputs']['keyword']['attrs']['placeholder'] = _t('_sys_search' . (BxDolAI::getAssistantForLiveSearch() ? '_or_ask' : '')  . '_placeholder');
+            $aForm['inputs']['keyword']['attrs']['placeholder'] = _t('_sys_search_placeholder');
             $aForm['inputs']['keyword']['attrs']['onkeydown'] = "return bx_search_on_type(event, $sJsParams);";
             $aForm['inputs']['keyword']['attrs']['onpaste'] = $aForm['inputs']['keyword']['attrs']['onkeydown'];
         }

+ 11 - 6
template/scripts/BxBaseSearchExtended.php

@@ -80,7 +80,7 @@ class BxBaseSearchExtended extends BxDolSearchExtended
      * @param $aParams['template'] custom unit templates to use
      * @return HTML string with search results
      */ 
-    public function getResults($aParams = array())
+    public function getResults($aParams = [])
     {
         if(!$this->isEnabled())
             return '';
@@ -141,7 +141,7 @@ class BxBaseSearchExtended extends BxDolSearchExtended
 
             if(!$bCondition) {
                 if(!isset($aParams['cond']))
-                    $aParams['cond'] = array();
+                    $aParams['cond'] = [];
 
                 switch($oForm->aInputs[$aField['name']]['type']) {
                     case 'location':
@@ -216,14 +216,19 @@ class BxBaseSearchExtended extends BxDolSearchExtended
         if($this->_bIsApi)
             return [$this->getResultsAPI($mixedResults, $iStart, $iPerPage, $aParamsSearch)];
 
+        if(!empty($aParams['cond']) && is_array($aParams['cond']))
+            $aParams['cond'] = self::encodeConditions($aParams['cond']);
+
         //-- Paginate
         $bTmplVarsPaginate = false;
         $aTmplVarsPaginate = [];
         if(($bTmplVarsPaginate = $iStart || $iResults > $iPerPage) !== false) {
-            if(!empty($aParams['cond']) && is_array($aParams['cond']))
-                $aParams['cond'] = self::encodeConditions($aParams['cond']);
+            $aPaginate = [
+                'num' => $iResults, 
+                'start' => $iStart, 
+                'per_page' => $iPerPage
+            ];
 
-            $aPaginate = ['num' => $iResults, 'start' => $iStart, 'per_page' => $iPerPage];
             if(($sKt = 'total') && ($mixedTotal = $aParams[$sKt] ?? false)) {
                 if(is_numeric($mixedTotal))
                     $aPaginate[$sKt] = (int)$mixedTotal;
@@ -306,7 +311,7 @@ class BxBaseSearchExtended extends BxDolSearchExtended
 
         $sOnChange = '';
         if(!$bJsMode) {
-            unset($aParams['start'], $aParams['per_page']);
+            unset($aParams['_q'], $aParams['start'], $aParams['per_page']);
             list($sPageLink, $aPageParams) = bx_get_base_url_inline($aParams);
             $sOnChange = "bx_search_extnded_sort(this,'" . BxDolPermalinks::getInstance()->permalink(bx_append_url_params($sPageLink, $aPageParams)) . "')";
         }

+ 83 - 63
template/scripts/BxBaseServices.php

@@ -879,25 +879,27 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
         else
             $iContextPid = $oAi ? $oAi->filterContextPid((int)$mixedContextPid, $bRequirePost) : 0;
 
-        $aBlock = [
+        if ($bIsApi)
+            return [bx_api_get_block('ai_agent', [
             'agent_id' => $iId,
             'agent_profile' => BxDolProfile::getData($aAgent['profile_id']),
             'context_profile_id' => $iContextPid,
-        ];
+            'hidden_first_message' => (string)($aAgent['hidden_first_message'] ?? ''),
+            ])];
 
-        return $bIsApi ? [bx_api_get_block('ai_agent', $aBlock)] : BxDolTemplate::getInstance()->parseHtmlByName('agent_ai_chat.html', [
-            'id' => $iId,
-            'context_profile_id' => $iContextPid,
-        ]);
-    }
+        $sEndpoint = BX_DOL_URL_ROOT . 'sys-ai-chat/' . (int)$iId;
+        if ($iContextPid > 0)
+            $sEndpoint .= '&context=' . $iContextPid;
 
-    public function serviceGetBlockAskAssistant($aParams = [])
-    {
-        $iAssistant = BxDolAI::getAssistantForAskBlock();
-        if(!$iAssistant)
-            return '';
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->addJs([BX_DOL_URL_ROOT . 'plugins_public/bundle.js']);
 
-        return BxDolAIAssistant::getObjectInstance($iAssistant)->getAskBlock($aParams);
+        return $oTemplate->parseHtmlByName('agent_ai_chat.html', [
+            'id' => (int)$iId,
+            'context_profile_id' => (int)$iContextPid,
+            'endpoint_js' => json_encode($sEndpoint),
+            'bundle_url' => BX_DOL_URL_ROOT . 'plugins_public/bundle.js',
+        ]);
     }
 
     public function serviceGetMenu($aParams)
@@ -1613,21 +1615,6 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
         return $aResult;
     }
 
-    public function serviceGetOptionsStudioAssistant()
-    {
-        return $this->_getOptionsAiAssistants();
-    }
-
-    public function serviceGetOptionsLiveSearchAssistant()
-    {
-        return $this->_getOptionsAiAssistants();
-    }
-
-    public function serviceGetOptionsAskBlockAssistant()
-    {
-        return $this->_getOptionsAiAssistants();
-    }
-
     public function serviceRedirect($sUrl = false)
     {
         if (!getParam('sys_confirmation_before_redirect')) {
@@ -1831,45 +1818,90 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
 
     public function serviceAiChat($iAgentId)
     {
-        $sMethod = $_SERVER['REQUEST_METHOD'] ?? '';
-        if ('GET' !== $sMethod && 'POST' !== $sMethod)
-            return echoJson(['code' => 405, 'msg' => _t('_error occured')]);
+        $sMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '';
+        if('GET' === $sMethod)
+            return $this->_aiChatHydrate($iAgentId);
 
-        if ('GET' === $sMethod && false !== bx_get('offset'))
+        if('POST' !== $sMethod)
             return echoJson(['code' => 405, 'msg' => _t('_error occured')]);
 
         $aAgent = $this->_aiChatLoadAgent($iAgentId);
-        if (!$aAgent)
+        if(!$aAgent)
             return echoJson(['code' => (int)$iAgentId ? 404 : 400, 'msg' => _t('_sys_agents_agent_not_found')]);
 
         $oAi = BxDolAI::getInstance();
-        if (!$oAi)
+        if(!$oAi)
             return echoJson(['code' => 503, 'msg' => _t('_sys_agents_exception')]);
 
-        if (!$oAi->canChatDirectly($aAgent))
-            return echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized')]);
-
-        $aParams = $this->_aiChatHistoryParams($aAgent);
-        if (!$aParams)
+        if(!$oAi->canChatDirectly($aAgent))
             return echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized')]);
 
-        if ('GET' === $sMethod)
-            return echoJson([
-                'messages' => $oAi->getChatHistoryUiMessages($aAgent['id'], $aParams),
-                'activeRun' => null,
-                'interrupts' => null,
-            ]);
-
         $sJson = file_get_contents('php://input');
         $aData = json_decode($sJson, true);
-        if (json_last_error() !== JSON_ERROR_NONE)
+        if(json_last_error() !== JSON_ERROR_NONE)
             return echoJson(['code' => 400, 'msg' => _t('_sys_agents_json_field_err')]);
 
         $sPrompt = $oAi->extractChatPromptFromRequest($aData);
-        if (!$sPrompt)
+        if(!$sPrompt)
             return echoJson(['code' => 400, 'msg' => _t('_sys_agents_json_field_err')]);
 
-        $oAi->streamAgentChat($aAgent['id'], $sPrompt, $aParams, $aData['threadId'] ?? null);
+        $aParams = $this->_aiChatHistoryParams($aAgent);
+        if (!$aParams)
+            return echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized')]);
+
+        $aParams['request_user_turns'] = $oAi->countChatUserTurns($aData['messages'] ?? []);
+
+        $sThreadId = !empty($aData['threadId']) ? $aData['threadId'] : null;
+
+        if(method_exists($oAi, 'streamAgentChat') && class_exists('NeuronAI\Chat\Messages\Stream\Adapters\AGUIAdapter')) {
+            $oAi->streamAgentChat($aAgent['id'], $sPrompt, $aParams, $sThreadId);
+            return;
+        }
+
+        try {
+            $sReply = $oAi->callAgent('manual', $aAgent, $sPrompt);
+        } catch (Throwable $o) {
+            return echoJson(['code' => 500, 'msg' => $o->getMessage(), 'messages' => []]);
+        }
+        return echoJson(['code' => 200, 'msg' => $sReply, 'text' => $sReply]);
+    }
+
+    protected function _aiChatHydrate($iAgentId)
+    {
+        if(false !== bx_get('offset'))
+            return echoJson(['code' => 405, 'msg' => _t('_error occured'), 'messages' => [], 'activeRun' => null, 'interrupts' => null]);
+
+        $aAgent = $this->_aiChatLoadAgent($iAgentId);
+        if(!$aAgent)
+            return echoJson(['code' => (int)$iAgentId ? 404 : 400, 'msg' => _t('_sys_agents_agent_not_found'), 'messages' => [], 'activeRun' => null, 'interrupts' => null]);
+
+        $oAi = BxDolAI::getInstance();
+        if(!$oAi)
+            return echoJson(['code' => 503, 'msg' => _t('_sys_agents_exception'), 'messages' => [], 'activeRun' => null, 'interrupts' => null]);
+
+        if(!$oAi->canChatDirectly($aAgent))
+            return echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized'), 'messages' => [], 'activeRun' => null, 'interrupts' => null]);
+
+        $aParams = $this->_aiChatHistoryParams($aAgent);
+        if (!$aParams)
+            return echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized'), 'messages' => [], 'activeRun' => null, 'interrupts' => null]);
+
+        try {
+            $aMessages = method_exists($oAi, 'getChatHistoryUiMessages')
+                ? $oAi->getChatHistoryUiMessages($aAgent['id'], $aParams)
+                : [];
+        } catch (Throwable $o) {
+            $aMessages = [];
+        }
+
+        if(!is_array($aMessages))
+            $aMessages = [];
+
+        return echoJson([
+            'messages' => $aMessages,
+            'activeRun' => null,
+            'interrupts' => null,
+        ]);
     }
 
     /**
@@ -1894,24 +1926,12 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
         if (!$iAgentId)
             return false;
 
-        $aAgent = BxDolAiQuery::getAgentObject($iAgentId);
-        if (!$aAgent || !$aAgent['active'])
+        $aAgent = BxDolAIQuery::getAgentObject($iAgentId);
+        if(!$aAgent || empty($aAgent['active']))
             return false;
 
         return $aAgent;
     }
-
-    protected function _getOptionsAiAssistants()
-    {
-        $aResult = ['' => _t('_Select_one')];
-
-        $aAgents = BxDolAI::getInstance()->getAgentsByTriggerType('manual');
-        if($aAgents && is_array($aAgents)) 
-            foreach($aAgents as $aAgent)
-                $aResult[$aAgent['id']] = $aAgent['title'];
-
-        return $aResult;
-    }
 }
 
 /** @} */

+ 15 - 14
yarn.lock

@@ -687,12 +687,12 @@
     "@tanstack/ai-event-client" "^0.10.0"
     "@tanstack/ai-utils" "^0.4.0"
 
-"@tanstack/ai-client@^0.29.2":
-  version "0.29.2"
-  resolved "https://registry.yarnpkg.com/@tanstack/ai-client/-/ai-client-0.29.2.tgz#ef5b336d5d8e12a6bb261ae0dda16af8e19ed464"
-  integrity sha512-SciysncXh5R5MHQ1GoF3kHfaygRFljUO1yVRvH97elR5l7Xjx9VYVB0KLnW7g11SIwxUrpODlLynlxEJSL2USg==
+"@tanstack/ai-client@^0.31.0":
+  version "0.31.0"
+  resolved "https://registry.yarnpkg.com/@tanstack/ai-client/-/ai-client-0.31.0.tgz#d2aa50b7fe7b769e60efe5b4c1cbb62c7bb52480"
+  integrity sha512-K7VlIfyiPXpTkdVkfoGbful++/jr9wJet4ri0uhBvfxgIUi4FNM+50TadfcZ14H+L4mDdnVB4IaRYy+XGxV9aw==
   dependencies:
-    "@tanstack/ai" "^0.52.0"
+    "@tanstack/ai" "^0.53.0"
     "@tanstack/ai-event-client" "^0.11.2"
     "@tanstack/ai-utils" "^0.4.0"
 
@@ -733,10 +733,10 @@
     "@tanstack/ai-utils" "^0.4.0"
     partial-json "^0.1.7"
 
-"@tanstack/ai@^0.52.0":
-  version "0.52.2"
-  resolved "https://registry.yarnpkg.com/@tanstack/ai/-/ai-0.52.2.tgz#0f92a4ea8aa3210eb9b914e02af3f5976517f3eb"
-  integrity sha512-Oewjnt7IAmSFRuvbBwbLFK+zZSKSVaf6HU36SbwTNvVqLOAeRwc1Jdg4mi2iqxxgDQu4yK4REooo8L/uF9VTOA==
+"@tanstack/ai@^0.53.0":
+  version "0.53.0"
+  resolved "https://registry.yarnpkg.com/@tanstack/ai/-/ai-0.53.0.tgz#8790209bc9f0cba4a28ed5414df0bc813ce47062"
+  integrity sha512-a4zDqb7SU0PU+pua69y5IDKLHO9dsD99vMIrzKNfcJyzJ81aASIkbrXXcD4n15YOj+FaWPZmD4Qewmd+5IV4Iw==
   dependencies:
     "@ag-ui/core" "0.1.1-canary.beta.0"
     "@standard-schema/spec" "^1.1.0"
@@ -2633,7 +2633,8 @@ jstree@^3.3.17:
 
 "jush@https://github.com/vrana/jush":
   version "0.0.0"
-  resolved "https://github.com/vrana/jush#e89a152b995b33b2f308078c3dee7bee506711e3"
+  uid d60192b0d676ffc02ad48dc0630ff2769d2d4cd5
+  resolved "https://github.com/vrana/jush#d60192b0d676ffc02ad48dc0630ff2769d2d4cd5"
 
 just-diff-apply@^5.2.0:
   version "5.5.0"
@@ -2645,10 +2646,10 @@ just-diff@^6.0.0:
   resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-6.0.2.tgz#03b65908543ac0521caf6d8eb85035f7d27ea285"
   integrity sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==
 
-leaflet-providers@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/leaflet-providers/-/leaflet-providers-3.0.0.tgz#93269327366b1e130e1da6d763674bc2a8aff58c"
-  integrity sha512-PWwsWRpf7xMrCofRUfWR6FBdw2v08j48tXCLQCoS1PinxPpVU70AQTr9N3NcbTIONiF9nS6m45LxBWVYx2i+wg==
+leaflet-providers@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/leaflet-providers/-/leaflet-providers-4.0.0.tgz#a98c5631a671c366739d8658e40be1d02e2c57cb"
+  integrity sha512-4m3Q4t0e+cHVNIPzlkW9HFWEgplImFRv8cnUFJYOfFoS6p2KosAfmh2Do6qk4HMdqutqbHMpv3F7sn/CUSw6oQ==
 
 leaflet@^1.9.4:
   version "1.9.4"

Некоторые файлы не были показаны из-за большого количества измененных файлов