| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111 |
- <?php defined('BX_DOL') or die('hack attempt');
- /**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * Operator tool: create / update / catalog Studio agents (sys_agents_agents).
- * mysql_write_safe cannot touch that table — use this instead.
- *
- * @defgroup UnaCore UNA Core
- * @{
- */
- use NeuronAI\Tools\PropertyType;
- use NeuronAI\Tools\ToolProperty;
- class BxDolAIToolAgentCreate extends BxDolAITool
- {
- protected const TRIGGERS = ['alert', 'scheduler', 'webhook', 'manual', 'agent', 'message', 'form-input'];
- protected const PRESETS = ['comment_reply', 'manual', 'message', 'custom'];
- protected const COMMENT_ACTIONS = ['commentPost', 'comment_added', 'commentAdded', 'cmt_post'];
- protected const FORBIDDEN_TOOL_TYPES = ['mysql_write'];
- protected const CLIP = 20000;
- protected const PROFILE_NAME_MAX = 40;
- protected const VERSION = 4;
- /** @var int[]|null see _allowedAccounts() */
- protected $_aAllowedAccounts = null;
- /** @var array<string,string>|null lowercase label => sys_modules.name */
- protected $_aModuleNames = null;
- protected const COMMENT_TOOLS = ['comments_get', 'comments_add', 'comment_get', 'content_get'];
- protected const COMMENT_PROMPT_TOOLS = "You run on a UNA commentPost alert. The user message is JSON: object_id, sender_profile_id, extra (comment id/text/system).\nIf sender_profile_id or extra.author / extra.author_id equals this agent's profile_id, do nothing — never reply to yourself (prevents loops).\nRead context with comments_get / content_get. Post at most one reply with comments_add on the same object. Then stop.\nMatch the comment language. Do not mention that you are an agent unless asked.";
- public function __construct()
- {
- parent::__construct(
- 'agent_create',
- 'Create or update a Studio agent (sys_agents_agents). Do NOT use mysql_write on that table. '
- . 'Never ask the user for a numeric profile_id. action=catalog returns profiles and profile_buttons — call chat_buttons with exactly those labels. '
- . 'Existing: profile=display name or id. New bot: profile_new=Name (creates a Persons profile under the preset Robot account, sys_profile_bot). create without profile/profile_new is refused. '
- . 'preset=comment_reply for “reply to every comment”: trigger=alert, async=1, comments_* tools. '
- . 'Pass module as one name from installed content modules: ' . $this->getContentModules() . '. Or alert as unit:action (for example bx_forum:commentPost). '
- . 'Show the plan in the site language. Confirm with chat_buttons whose reply labels are exactly "' . _t('_Yes') . '" and "' . _t('_No') . '". Wait for ' . _t('_Yes') . ', then create. Prompt goes in prompt_system.'
- );
- }
- protected function properties(): array
- {
- return [
- new ToolProperty('action', PropertyType::STRING, 'catalog | create | update. catalog = list alerts/tools/models/agents. create = insert. update = change prompt/tools/alert/active of agent_id.', true),
- new ToolProperty('preset', PropertyType::STRING, 'comment_reply | manual | message | custom. comment_reply = alert agent that replies to comments.', false),
- new ToolProperty('title', PropertyType::STRING, 'Human title. Required for create.', false),
- new ToolProperty('name', PropertyType::STRING, 'Unique machine name. Auto from title if empty.', false),
- new ToolProperty('description', PropertyType::STRING, 'Operator notes.', false),
- new ToolProperty('prompt_system', PropertyType::STRING, 'System / role prompt. Required for create. This is what the new agent will follow.', false),
- new ToolProperty('prompt_steps', PropertyType::STRING, 'Optional steps prompt.', false),
- new ToolProperty('prompt_output', PropertyType::STRING, 'Optional output prompt.', false),
- new ToolProperty('prompt_tools', PropertyType::STRING, 'Optional extra tool rules. comment_reply appends loop-prevention rules.', false),
- new ToolProperty('trigger', PropertyType::STRING, 'alert | scheduler | webhook | manual | agent | message | form-input. Default from preset (comment_reply → alert).', false),
- new ToolProperty('alert', PropertyType::STRING, 'unit:action from sys_alerts_log, e.g. bx_forum:commentPost. Required for trigger=alert unless module is set.', false),
- new ToolProperty('module', PropertyType::STRING, 'Installed module: module from installed modules list.', false),
- new ToolProperty('tools', PropertyType::STRING, 'CSV of tool types or ids (comments_add,comments_get). comment_reply fills comments_* if empty.', false),
- new ToolProperty('model_id', PropertyType::INTEGER, 'Chat model id. Default = operator agent / sys_agents_model.', false),
- new ToolProperty('profile_id', PropertyType::INTEGER, 'Numeric profile id. Prefer profile (name) or profile_new. Do not ask the user for this number.', false),
- new ToolProperty('profile', PropertyType::STRING, 'Existing profile the agent acts as: display name, @uri, or id. From catalog.profiles. Do not ask the user for a number.', false),
- new ToolProperty('profile_new', PropertyType::STRING, 'Short display name for a NEW Persons profile, like a person name: 1–3 words, max 40 chars. Examples: Posts Bot. NEVER a prompt, sentence, or agent instructions.', false),
- new ToolProperty('vector_store_id', PropertyType::INTEGER, 'Knowledge store id. Default = operator / first active store / 0.', false),
- new ToolProperty('async', PropertyType::INTEGER, '1 = delayed (use for alert comment replies so the post is not blocked). Default 1 for alert.', false),
- new ToolProperty('active', PropertyType::INTEGER, '1 = run immediately. Default 1.', false),
- new ToolProperty('scheduler_cron', PropertyType::STRING, 'CRON string for trigger=scheduler.', false),
- new ToolProperty('webhook_key', PropertyType::STRING, 'Bearer key for trigger=webhook.', false),
- new ToolProperty('message_profile_id', PropertyType::INTEGER, 'Accepted sender for trigger=message. 0 = any.', false),
- new ToolProperty('form_object', PropertyType::STRING, 'Form object for trigger=form-input.', false),
- new ToolProperty('form_input', PropertyType::STRING, 'Form input name for trigger=form-input.', false),
- new ToolProperty('agent_id', PropertyType::INTEGER, 'Existing sys_agents_agents.id for update.', false),
- new ToolProperty('query', PropertyType::STRING, 'catalog filter: alert unit/action, tool type, agent name.', false),
- new ToolProperty('dry_run', PropertyType::BOOLEAN, 'If true, validate and return the row without writing.', false),
- ];
- }
- public function __invoke(
- $action = 'catalog',
- $preset = '',
- $title = '',
- $name = '',
- $description = '',
- $prompt_system = '',
- $prompt_steps = '',
- $prompt_output = '',
- $prompt_tools = '',
- $trigger = '',
- $alert = '',
- $module = '',
- $tools = '',
- $model_id = 0,
- $profile_id = 0,
- $profile = '',
- $profile_new = '',
- $vector_store_id = 0,
- $async = null,
- $active = 1,
- $scheduler_cron = '',
- $webhook_key = '',
- $message_profile_id = 0,
- $form_object = '',
- $form_input = '',
- $agent_id = 0,
- $query = '',
- $dry_run = false
- ): array {
- $sAction = strtolower(trim((string)$action));
- if ($sAction === 'list')
- $sAction = 'catalog';
- if (!in_array($sAction, ['catalog', 'create', 'update'], true))
- return ['ok' => 0, 'error' => 'action must be catalog, create, or update'];
- try {
- if ($sAction === 'catalog')
- return $this->_catalog((string)$query, (string)$preset, (string)$module);
- if ($sAction === 'update')
- return $this->_update((int)$agent_id, [
- 'title' => $title,
- 'description' => $description,
- 'prompt_system' => $prompt_system,
- 'prompt_steps' => $prompt_steps,
- 'prompt_output' => $prompt_output,
- 'prompt_tools' => $prompt_tools,
- 'tools' => $tools,
- 'alert' => $alert,
- 'module' => $module,
- 'async' => $async,
- 'active' => $active,
- 'preset' => $preset,
- 'profile' => $profile,
- 'profile_id' => $profile_id,
- 'profile_new' => $profile_new,
- ], $this->_isDry($dry_run));
- return $this->_create([
- 'preset' => $preset,
- 'title' => $title,
- 'name' => $name,
- 'description' => $description,
- 'prompt_system' => $prompt_system,
- 'prompt_steps' => $prompt_steps,
- 'prompt_output' => $prompt_output,
- 'prompt_tools' => $prompt_tools,
- 'trigger' => $trigger,
- 'alert' => $alert,
- 'module' => $module,
- 'tools' => $tools,
- 'model_id' => $model_id,
- 'profile_id' => $profile_id,
- 'vector_store_id' => $vector_store_id,
- 'async' => $async,
- 'active' => $active,
- 'scheduler_cron' => $scheduler_cron,
- 'webhook_key' => $webhook_key,
- 'message_profile_id' => $message_profile_id,
- 'form_object' => $form_object,
- 'form_input' => $form_input,
- 'profile' => $profile,
- 'profile_new' => $profile_new,
- ], $this->_isDry($dry_run));
- } catch (Throwable $o) {
- return ['ok' => 0, 'error' => $o->getMessage()];
- }
- }
- protected function _isDry($dry_run): bool
- {
- return $dry_run === true || $dry_run === 1 || $dry_run === '1' || $dry_run === 'true';
- }
- protected function _catalog(string $sQuery, string $sPreset, string $sModule): array
- {
- $oDb = BxDolDb::getInstance();
- $sQ = mb_strtolower(trim($sQuery));
- $sMod = $this->_resolveModule($sModule !== '' ? $sModule : $sQuery);
- $aAlerts = [];
- if ($oDb->isTableExists('sys_alerts_log')) {
- $aRows = $oDb->getAll("SELECT `unit`, `action`, `counter_24h` FROM `sys_alerts_log` ORDER BY `counter_24h` DESC, `unit`, `action` LIMIT 400");
- foreach ($aRows as $aRow) {
- $sKey = $aRow['unit'] . ':' . $aRow['action'];
- $sHay = mb_strtolower($sKey . ' ' . ($aRow['unit'] ?? '') . ' ' . ($aRow['action'] ?? ''));
- $bComment = in_array($aRow['action'], self::COMMENT_ACTIONS, true);
- if ($sQ !== '' && strpos($sHay, $sQ) === false && !($sMod && $aRow['unit'] === $sMod) && !($sPreset === 'comment_reply' && $bComment))
- continue;
- if ($sPreset === 'comment_reply' && $sQ === '' && $sModule === '' && !$bComment)
- continue;
- if ($sMod && $aRow['unit'] !== $sMod && strpos($sHay, $sQ) === false)
- continue;
- $aAlerts[] = [
- 'alert' => $sKey,
- 'fired_24h' => (int)$aRow['counter_24h'],
- 'desc' => method_exists('BxDolAiQuery', 'getAlertDesc') ? (string)BxDolAiQuery::getAlertDesc($sKey) : '',
- ];
- if (count($aAlerts) >= 40)
- break;
- }
- }
- $aTools = [];
- $aToolRows = $oDb->getAll("SELECT `id`, `type`, `title`, `active` FROM `sys_agents_tools` ORDER BY `type` ASC");
- foreach ($aToolRows as $aRow) {
- if (in_array($aRow['type'], BxDolAiAgentTools::hiddenTypes(), true))
- continue;
- $sHay = mb_strtolower($aRow['id'] . ' ' . $aRow['type'] . ' ' . $aRow['title']);
- if ($sQ !== '' && strpos($sHay, $sQ) === false && $sPreset !== 'comment_reply')
- continue;
- if ($sPreset === 'comment_reply' && $sQ === '' && !in_array($aRow['type'], self::COMMENT_TOOLS, true))
- continue;
- $aTools[] = [
- 'id' => (int)$aRow['id'],
- 'type' => $aRow['type'],
- 'title' => $aRow['title'],
- 'active' => (int)$aRow['active'],
- ];
- }
- $aModels = $oDb->getAll("SELECT `id`, `type`, `model`, `title`, `capabilities`, `active` FROM `sys_agents_models` WHERE `active` = 1 AND (`capabilities` LIKE '%chatllm%' OR `capabilities` LIKE '%chatvlm%') ORDER BY `title` ASC LIMIT 30");
- $aStores = $oDb->isTableExists('sys_agents_vector_store')
- ? $oDb->getAll("SELECT `id`, `title`, `type`, `active` FROM `sys_agents_vector_store` WHERE `active` = 1 ORDER BY `title` ASC LIMIT 20")
- : [];
- $aAgents = $oDb->getAll("SELECT `id`, `name`, `title`, `trigger`, `alert`, `active`, `profile_id`, `model_id` FROM `sys_agents_agents` ORDER BY `id` DESC LIMIT 30");
- if ($sQ !== '') {
- $aAgents = array_values(array_filter($aAgents, function ($aRow) use ($sQ) {
- $sHay = mb_strtolower(($aRow['name'] ?? '') . ' ' . ($aRow['title'] ?? '') . ' ' . ($aRow['alert'] ?? '') . ' ' . ($aRow['trigger'] ?? ''));
- return strpos($sHay, $sQ) !== false;
- }));
- }
- $aDefaults = $this->_defaults();
- $aProfiles = $this->_listProfiles('');
- $sSuggest = $this->_suggestProfileName($sPreset);
- return [
- 'ok' => 1,
- 'version' => self::VERSION,
- 'action' => 'catalog',
- 'profiles' => $aProfiles,
- 'profiles_count' => count($aProfiles),
- 'suggested_new_name' => $sSuggest,
- 'profile_buttons' => $this->_profileButtons($aProfiles, $sSuggest),
- 'profile_hint' => 'Call chat_buttons now with exactly profile_buttons as reply labels (one per line, no other text). Tap on a name → profile=<name>; tap on «' . _t('_sys_agents_profile_btn_new', $sSuggest) . '» → profile_new=' . $sSuggest . '. create without profile/profile_new is refused.',
- 'defaults' => $aDefaults,
- 'presets' => self::PRESETS,
- 'comment_reply_hint' => 'Pick an alert, example - bx_forum:commentPost (module=bx_forum), module bx_forum must be installed. Tools: comments_get,comments_add,comment_get,content_get. async=1.',
- 'alerts' => $aAlerts,
- 'tools' => $aTools,
- 'models' => $aModels,
- 'vector_stores' => $aStores,
- 'agents' => $aAgents,
- ];
- }
- protected function _create(array $aIn, bool $bDry): array
- {
- $sPreset = $this->_preset($aIn['preset'] ?? '');
- $sTitle = trim((string)($aIn['title'] ?? ''));
- if ($sTitle === '')
- return ['ok' => 0, 'error' => 'title is required'];
- $sPrompt = $this->_clip((string)($aIn['prompt_system'] ?? ''));
- if (trim($sPrompt) === '')
- return ['ok' => 0, 'error' => 'prompt_system is required — this is what the new agent will do'];
- $sTrigger = strtolower(trim((string)($aIn['trigger'] ?? '')));
- if ($sTrigger === '')
- $sTrigger = $this->_triggerForPreset($sPreset);
- if (!in_array($sTrigger, self::TRIGGERS, true))
- return ['ok' => 0, 'error' => 'unknown trigger. Use: ' . implode(', ', self::TRIGGERS)];
- $aDefaults = $this->_defaults();
- $iModel = (int)($aIn['model_id'] ?? 0) ?: (int)$aDefaults['model_id'];
- $sProfileIn = trim((string)($aIn['profile'] ?? ''));
- $sProfileNewIn = trim((string)($aIn['profile_new'] ?? ''));
- if ($sProfileIn === '' && $sProfileNewIn === '' && (int)($aIn['profile_id'] ?? 0) <= 0) {
- // never fall back to the operator's own profile silently — the user must pick a name or a new one
- $aProfiles = $this->_listProfiles('');
- $sSuggest = $this->_suggestProfileName($sPreset);
- return [
- 'ok' => 0,
- 'error' => _t('_sys_agents_profile_required_err', _t('_sys_agents_profile_btn_new', $sSuggest)),
- 'profiles' => $aProfiles,
- 'suggested_new_name' => $sSuggest,
- 'profile_buttons' => $this->_profileButtons($aProfiles, $sSuggest),
- ];
- }
- $aProf = $this->_resolveProfile($sProfileIn, $aIn['profile_id'] ?? 0, $sProfileNewIn, $bDry);
- if (!empty($aProf['error']))
- return ['ok' => 0, 'error' => $aProf['error'], 'candidates' => $aProf['candidates'] ?? []];
- $iProfile = (int)($aProf['profile_id'] ?? 0);
- $iStore = (int)($aIn['vector_store_id'] ?? 0) ?: (int)$aDefaults['vector_store_id'];
- if ($iModel <= 0)
- return ['ok' => 0, 'error' => 'no chat model. Pass model_id or activate a chatllm model.'];
- if ($iProfile <= 0 && empty($aProf['dry_create']))
- return ['ok' => 0, 'error' => 'profile could not be resolved. Pass profile=<name from catalog.profiles> or profile_new=<name>.'];
- $oDb = BxDolDb::getInstance();
- if (!$oDb->getRow("SELECT `id` FROM `sys_agents_models` WHERE `id` = :id AND `active` = 1 LIMIT 1", ['id' => $iModel]))
- return ['ok' => 0, 'error' => "model_id {$iModel} is missing or inactive"];
- $sAlert = '';
- $aAlertPick = null;
- if ($sTrigger === 'alert') {
- $aAlertPick = $this->_resolveAlert((string)($aIn['alert'] ?? ''), (string)($aIn['module'] ?? ''), $sPreset);
- if (!empty($aAlertPick['error']))
- return ['ok' => 0, 'error' => $aAlertPick['error'], 'candidates' => $aAlertPick['candidates'] ?? []];
- $sAlert = $aAlertPick['alert'];
- }
- $sToolsCsv = $this->_resolveToolsCsv((string)($aIn['tools'] ?? ''), $sPreset);
- if (is_array($sToolsCsv) && isset($sToolsCsv['error']))
- return $sToolsCsv;
- if ($sTrigger === 'message') {
- $aDup = $oDb->getRow("SELECT `id`, `name` FROM `sys_agents_agents` WHERE `trigger` = 'message' AND `profile_id` = :p LIMIT 1", ['p' => $iProfile]);
- if ($aDup)
- return ['ok' => 0, 'error' => "profile_id {$iProfile} already has message-agent {$aDup['id']} ({$aDup['name']}). One message-agent per profile."];
- }
- if ($sTrigger === 'scheduler' && trim((string)($aIn['scheduler_cron'] ?? '')) === '')
- return ['ok' => 0, 'error' => 'scheduler_cron is required for trigger=scheduler'];
- if ($sTrigger === 'webhook' && trim((string)($aIn['webhook_key'] ?? '')) === '')
- return ['ok' => 0, 'error' => 'webhook_key is required for trigger=webhook'];
- if ($sTrigger === 'form-input' && (trim((string)($aIn['form_object'] ?? '')) === '' || trim((string)($aIn['form_input'] ?? '')) === ''))
- return ['ok' => 0, 'error' => 'form_object and form_input are required for trigger=form-input'];
- $iAsync = $aIn['async'];
- if ($iAsync === null || $iAsync === '')
- $iAsync = in_array($sTrigger, ['alert', 'webhook', 'message'], true) ? 1 : 0;
- $iAsync = (int)$iAsync ? 1 : 0;
- if (in_array($sTrigger, ['manual', 'scheduler'], true))
- $iAsync = 0;
- $iActive = isset($aIn['active']) && $aIn['active'] !== '' && $aIn['active'] !== null
- ? ((int)$aIn['active'] ? 1 : 0)
- : 1;
- $sName = $this->_uniqueName((string)($aIn['name'] ?? ''), $sTitle);
- $sPromptTools = $this->_clip((string)($aIn['prompt_tools'] ?? ''));
- if ($sPreset === 'comment_reply') {
- $sPromptTools = trim($sPromptTools . "\n\n" . self::COMMENT_PROMPT_TOOLS);
- }
- $sPromptSteps = $this->_clip((string)($aIn['prompt_steps'] ?? ''));
- if ($sPreset === 'comment_reply' && $sPromptSteps === '') {
- $sPromptSteps = "1. Parse the alert JSON.\n2. Skip if the comment author is this agent's profile_id.\n3. Read the parent object and recent comments.\n4. Write one reply with comments_add.\n5. Stop. Do not loop.";
- }
- $aRow = [
- 'title' => $this->_clip($sTitle, 255),
- 'name' => $sName,
- 'description' => $this->_clip((string)($aIn['description'] ?? ''), 2000),
- 'icon' => '',
- 'model_id' => $iModel,
- 'profile_id' => $iProfile,
- 'acl_levels' => 0,
- 'trigger' => $sTrigger,
- 'async' => $iAsync,
- 'alert' => $sAlert,
- 'scheduler_cron' => trim((string)($aIn['scheduler_cron'] ?? '')),
- 'webhook_key' => trim((string)($aIn['webhook_key'] ?? '')),
- 'message_profile_id' => (int)($aIn['message_profile_id'] ?? 0),
- 'form_object' => trim((string)($aIn['form_object'] ?? '')),
- 'form_input' => trim((string)($aIn['form_input'] ?? '')),
- 'max_turns' => 40,
- 'chat_ttl_min' => 0,
- 'max_input_chars' => 2000,
- 'max_tokens' => 1024,
- 'max_sessions_per_hour' => 3,
- 'max_sessions_per_day' => 10,
- 'vector_store_id' => $iStore,
- 'chat_history_context' => $sTrigger === 'alert' ? 0 : 50000,
- 'tools' => $sToolsCsv,
- 'tools_max_run' => $sPreset === 'comment_reply' ? 8 : 10,
- 'prompt_system' => $sPrompt,
- 'prompt_steps' => $sPromptSteps,
- 'prompt_output' => $this->_clip((string)($aIn['prompt_output'] ?? '')),
- 'prompt_tools' => $sPromptTools,
- 'added' => time(),
- 'active' => $iActive,
- ];
- $aRow = $this->_onlyExistingColumns($oDb, 'sys_agents_agents', $aRow);
- $aOut = [
- 'ok' => 1,
- 'action' => $bDry ? 'dry_run' : 'create',
- 'preset' => $sPreset,
- 'studio' => '/studio/agents.php?page=agents',
- 'row' => [
- 'name' => $aRow['name'],
- 'title' => $aRow['title'],
- 'trigger' => $aRow['trigger'],
- 'alert' => $aRow['alert'] ?? '',
- 'async' => (int)($aRow['async'] ?? 0),
- 'active' => (int)$aRow['active'],
- 'model_id' => (int)$aRow['model_id'],
- 'profile_id' => (int)$aRow['profile_id'],
- 'profile_name' => $aProf['name'] ?? '',
- 'profile_created' => (int)($aProf['created'] ?? 0),
- 'vector_store_id' => (int)($aRow['vector_store_id'] ?? 0),
- 'tools' => $aRow['tools'],
- 'prompt_system_chars' => strlen($aRow['prompt_system']),
- ],
- ];
- if (!empty($aAlertPick['warning']))
- $aOut['warning'] = $aAlertPick['warning'];
- if ($bDry)
- return $aOut;
- $mixed = $oDb->query("INSERT INTO `sys_agents_agents` SET " . $oDb->arrayToSQL($aRow));
- if ($mixed === false)
- return ['ok' => 0, 'error' => 'INSERT failed'];
- $iId = (int)$oDb->lastId();
- $oDb->cleanCache('sys_agents_with_alert');
- $this->_logSql('insert', 'sys_agents_agents', 'id', (string)$iId, 'INSERT agent ' . $aRow['name'], null, $aRow);
- $aOut['id'] = $iId;
- $aOut['row']['id'] = $iId;
- $aOut['next'] = $iActive
- ? 'Agent is active. Test the event (post a comment) or open Studio Agents grid.'
- : 'Created inactive. action=update agent_id=' . $iId . ' active=1 to turn on.';
- return $aOut;
- }
- protected function _update(int $iId, array $aIn, bool $bDry): array
- {
- if ($iId <= 0)
- return ['ok' => 0, 'error' => 'agent_id is required for update'];
- $oDb = BxDolDb::getInstance();
- $aBefore = $oDb->getRow("SELECT * FROM `sys_agents_agents` WHERE `id` = :id LIMIT 1", ['id' => $iId]);
- if (!$aBefore)
- return ['ok' => 0, 'error' => "agent {$iId} not found"];
- $aSet = [];
- foreach (['title', 'description', 'prompt_system', 'prompt_steps', 'prompt_output', 'prompt_tools'] as $sF) {
- if (isset($aIn[$sF]) && $aIn[$sF] !== '' && $aIn[$sF] !== null)
- $aSet[$sF] = $this->_clip((string)$aIn[$sF], $sF === 'title' ? 255 : self::CLIP);
- }
- if (isset($aIn['tools']) && $aIn['tools'] !== '' && $aIn['tools'] !== null) {
- $sCsv = $this->_resolveToolsCsv((string)$aIn['tools'], $this->_preset($aIn['preset'] ?? ''));
- if (is_array($sCsv) && isset($sCsv['error']))
- return $sCsv;
- $aSet['tools'] = $sCsv;
- }
- if ((isset($aIn['alert']) && $aIn['alert'] !== '') || (isset($aIn['module']) && $aIn['module'] !== '')) {
- $aAlertPick = $this->_resolveAlert((string)($aIn['alert'] ?? ''), (string)($aIn['module'] ?? ''), $this->_preset($aIn['preset'] ?? 'comment_reply'));
- if (!empty($aAlertPick['error']))
- return ['ok' => 0, 'error' => $aAlertPick['error'], 'candidates' => $aAlertPick['candidates'] ?? []];
- $aSet['alert'] = $aAlertPick['alert'];
- $aSet['trigger'] = 'alert';
- }
- if ($aIn['async'] !== null && $aIn['async'] !== '')
- $aSet['async'] = (int)$aIn['async'] ? 1 : 0;
- if ($aIn['active'] !== null && $aIn['active'] !== '')
- $aSet['active'] = (int)$aIn['active'] ? 1 : 0;
- $bWantProfile = (isset($aIn['profile']) && $aIn['profile'] !== '' && $aIn['profile'] !== null)
- || (isset($aIn['profile_new']) && $aIn['profile_new'] !== '' && $aIn['profile_new'] !== null)
- || !empty($aIn['profile_id']);
- $aProf = ['name' => '', 'created' => 0];
- if ($bWantProfile) {
- $aProf = $this->_resolveProfile($aIn['profile'] ?? '', $aIn['profile_id'] ?? 0, $aIn['profile_new'] ?? '', $bDry);
- if (!empty($aProf['error']))
- return ['ok' => 0, 'error' => $aProf['error'], 'candidates' => $aProf['candidates'] ?? []];
- if (!empty($aProf['profile_id']))
- $aSet['profile_id'] = (int)$aProf['profile_id'];
- }
- if (!$aSet)
- return ['ok' => 0, 'error' => 'nothing to update'];
- $aSet = $this->_onlyExistingColumns($oDb, 'sys_agents_agents', $aSet);
- if ($bDry) {
- return ['ok' => 1, 'action' => 'dry_run', 'id' => $iId, 'set' => array_keys($aSet)];
- }
- $b = $oDb->query("UPDATE `sys_agents_agents` SET " . $oDb->arrayToSQL($aSet) . " WHERE `id` = :id", ['id' => $iId]);
- if ($b === false)
- return ['ok' => 0, 'error' => 'UPDATE failed'];
- $oDb->cleanCache('sys_agents_with_alert');
- $aAfter = $oDb->getRow("SELECT * FROM `sys_agents_agents` WHERE `id` = :id LIMIT 1", ['id' => $iId]);
- $this->_logSql('update', 'sys_agents_agents', 'id', (string)$iId, 'UPDATE agent ' . $iId, $aBefore, $aAfter ?: null);
- return [
- 'ok' => 1,
- 'action' => 'update',
- 'id' => $iId,
- 'name' => $aAfter['name'] ?? $aBefore['name'],
- 'changed' => array_keys($aSet),
- 'active' => (int)($aAfter['active'] ?? 0),
- 'profile_id' => (int)($aAfter['profile_id'] ?? $aBefore['profile_id'] ?? 0),
- 'profile_name' => $aProf['name'] ?? '',
- 'profile_created' => (int)($aProf['created'] ?? 0),
- 'studio' => '/studio/agents.php?page=agents',
- ];
- }
- protected function _preset($s): string
- {
- $s = strtolower(trim((string)$s));
- if ($s === '' || $s === 'custom')
- return 'custom';
- if (in_array($s, self::PRESETS, true))
- return $s;
- return 'custom';
- }
- protected function _triggerForPreset(string $sPreset): string
- {
- if ($sPreset === 'comment_reply')
- return 'alert';
- if ($sPreset === 'message')
- return 'message';
- if ($sPreset === 'manual')
- return 'manual';
- return 'alert';
- }
- protected function _resolveModule(string $s): string
- {
- $s = trim($s);
- if ($s === '')
- return '';
- $sLow = mb_strtolower($s);
- $aNames = $this->_moduleNames();
- if (isset($aNames[$sLow]))
- return $aNames[$sLow];
- if (preg_match('/^[a-z0-9_]+$/i', $s))
- return $s;
- return '';
- }
- /**
- * Labels that mean a module: machine name, uri, install title, and the current-language title from _t().
- * Enabled modules claim a label before disabled ones. A name or uri is never overwritten by a title.
- *
- * @return array<string,string>
- */
- protected function _moduleNames(): array
- {
- if ($this->_aModuleNames !== null)
- return $this->_aModuleNames;
- $this->_aModuleNames = [];
- $oDb = BxDolDb::getInstance();
- $aRows = $oDb->getAll("SELECT `name`, `uri`, `title` FROM `sys_modules` WHERE `name` <> '' ORDER BY `enabled` DESC, `name` ASC");
- foreach ($aRows as $aRow)
- $this->_rememberModule((string)$aRow['name'], (string)$aRow['name']);
- foreach ($aRows as $aRow)
- $this->_rememberModule((string)$aRow['uri'], (string)$aRow['name']);
- foreach ($aRows as $aRow) {
- $sName = (string)$aRow['name'];
- $this->_rememberModule((string)$aRow['title'], $sName);
- $sKey = BxDolModule::getTitleKey((string)$aRow['uri']);
- $sTitle = _t($sKey);
- if ($sTitle !== $sKey)
- $this->_rememberModule($sTitle, $sName);
- }
- return $this->_aModuleNames;
- }
- protected function _rememberModule(string $sLabel, string $sName): void
- {
- $sLabel = mb_strtolower(trim($sLabel));
- if ($sLabel === '' || isset($this->_aModuleNames[$sLabel]))
- return;
- $this->_aModuleNames[$sLabel] = $sName;
- }
- protected function _resolveAlert(string $sAlert, string $sModule, string $sPreset): array
- {
- $sAlert = trim($sAlert);
- $sAlert = str_replace([' ', '/', '|'], ':', $sAlert);
- $sAlert = preg_replace('/:+/', ':', $sAlert);
- if ($sAlert !== '' && preg_match('/^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+$/', $sAlert)) {
- $aWarn = $this->_alertInLog($sAlert) ? '' : 'Alert is not in sys_alerts_log yet (never fired). It will run once that unit:action happens.';
- return ['alert' => $sAlert, 'warning' => $aWarn];
- }
- $sMod = $this->_resolveModule($sModule !== '' ? $sModule : $sAlert);
- $aCandidates = $this->_commentAlerts($sMod);
- if ($sPreset === 'comment_reply' || $sMod !== '') {
- if (count($aCandidates) === 1)
- return ['alert' => $aCandidates[0]['alert']];
- if (count($aCandidates) > 1)
- return ['error' => 'Several comment alerts match. Pass alert=unit:action.', 'candidates' => $aCandidates];
- if ($sMod !== '')
- return ['error' => "No comment alert for module {$sMod} in sys_alerts_log. Someone must post a comment once, then retry, or pass alert explicitly (e.g. {$sMod}:commentPost).", 'candidates' => []];
- }
- return ['error' => 'Pass alert as unit:action (bx_forum:commentPost) or module=discussions / bx_forum. action=catalog preset=comment_reply to list.', 'candidates' => $aCandidates];
- }
- protected function _commentAlerts(string $sMod): array
- {
- $oDb = BxDolDb::getInstance();
- if (!$oDb->isTableExists('sys_alerts_log'))
- return [];
- $aParams = [];
- $sWhere = "`action` IN (" . $oDb->implode_escape(self::COMMENT_ACTIONS) . ")";
- if ($sMod !== '') {
- $sWhere .= " AND `unit` = :u";
- $aParams['u'] = $sMod;
- }
- $aRows = $oDb->getAll("SELECT `unit`, `action`, `counter_24h` FROM `sys_alerts_log` WHERE {$sWhere} ORDER BY `counter_24h` DESC LIMIT 20", $aParams);
- $aOut = [];
- foreach ($aRows as $aRow) {
- $aOut[] = [
- 'alert' => $aRow['unit'] . ':' . $aRow['action'],
- 'fired_24h' => (int)$aRow['counter_24h'],
- ];
- }
- return $aOut;
- }
- protected function _alertInLog(string $sAlert): bool
- {
- $oDb = BxDolDb::getInstance();
- if (!$oDb->isTableExists('sys_alerts_log'))
- return false;
- $a = explode(':', $sAlert, 2);
- $s = $oDb->getOne("SELECT CONCAT(`unit`, ':', `action`) FROM `sys_alerts_log` WHERE `unit` = :u AND `action` = :a LIMIT 1", [
- 'u' => $a[0] ?? '',
- 'a' => $a[1] ?? '',
- ]);
- return (bool)$s;
- }
- protected function _listProfiles(string $sQuery = ''): array
- {
- $aOut = [];
- $aSeen = [];
- $oDb = BxDolDb::getInstance();
- // each source is isolated: one failing query must not empty the whole list
- $aDefaults = $this->_defaults();
- $this->_pushProfile($aOut, $aSeen, (int)$aDefaults['profile_id'], '', 'default');
- try {
- // profiles of the preset Robot account (sys_profile_bot) go first — new profiles are created there
- $iBotAccount = $this->_botAccount();
- if ($iBotAccount > 0) {
- $aRows = $oDb->getAll("SELECT `id` FROM `sys_profiles` WHERE `account_id` = :a AND `status` = 'active' ORDER BY `id` ASC LIMIT 50", ['a' => $iBotAccount]);
- foreach ($aRows as $aRow)
- $this->_pushProfile($aOut, $aSeen, (int)$aRow['id'], '', 'robot');
- }
- } catch (Throwable $o) {
- bx_log('sys_agents', 'agent_create profiles (robot account): ' . $o->getMessage(), BX_LOG_WARN);
- }
- try {
- $iOpAccount = (int)$oDb->getOne("SELECT `account_id` FROM `sys_profiles` WHERE `id` = :id LIMIT 1", ['id' => (int)$aDefaults['profile_id']]);
- if ($iOpAccount > 0) {
- $aRows = $oDb->getAll("SELECT `id` FROM `sys_profiles` WHERE `account_id` = :a AND `status` = 'active' ORDER BY `id` ASC LIMIT 50", ['a' => $iOpAccount]);
- foreach ($aRows as $aRow)
- $this->_pushProfile($aOut, $aSeen, (int)$aRow['id'], '', 'operator');
- }
- } catch (Throwable $o) {
- bx_log('sys_agents', 'agent_create profiles (account): ' . $o->getMessage(), BX_LOG_WARN);
- }
- try {
- if (function_exists('bx_srv')) {
- $aOpts = bx_srv('system', 'get_options_agents_profile', [false], 'TemplServices');
- if (is_array($aOpts)) {
- foreach ($aOpts as $aOpt)
- $this->_pushProfile($aOut, $aSeen, (int)($aOpt['key'] ?? 0), (string)($aOpt['value'] ?? ''), 'operator');
- }
- }
- } catch (Throwable $o) {
- bx_log('sys_agents', 'agent_create profiles (service): ' . $o->getMessage(), BX_LOG_WARN);
- }
- try {
- $iRoleAdmin = defined('BX_DOL_ROLE_ADMIN') ? (int)BX_DOL_ROLE_ADMIN : 2;
- $aRows = $oDb->getAll("SELECT p.`id` FROM `sys_profiles` p INNER JOIN `sys_accounts` a ON a.`id` = p.`account_id` WHERE (a.`role` & :r) AND p.`status` = 'active' ORDER BY p.`id` ASC LIMIT 50", [
- 'r' => $iRoleAdmin,
- ]);
- foreach ($aRows as $aRow)
- $this->_pushProfile($aOut, $aSeen, (int)$aRow['id'], '', 'operator');
- } catch (Throwable $o) {
- bx_log('sys_agents', 'agent_create profiles (admins): ' . $o->getMessage(), BX_LOG_WARN);
- }
- try {
- $aUsed = $oDb->getColumn("SELECT DISTINCT `profile_id` FROM `sys_agents_agents` WHERE `profile_id` > 0 ORDER BY `profile_id` DESC LIMIT 30");
- foreach ($aUsed as $iId)
- $this->_pushProfile($aOut, $aSeen, (int)$iId, '', 'agent');
- } catch (Throwable $o) {
- bx_log('sys_agents', 'agent_create profiles (agents): ' . $o->getMessage(), BX_LOG_WARN);
- }
- $sQ = trim($sQuery);
- if ($sQ !== '' && mb_strlen($sQ) <= 80 && $oDb->isTableExists('bx_persons_data')) {
- $aRows = $oDb->getAll("SELECT p.`id`, d.`fullname` FROM `sys_profiles` p INNER JOIN `bx_persons_data` d ON d.`id` = p.`content_id` AND p.`type` = 'bx_persons' WHERE d.`fullname` LIKE :q AND p.`status` = 'active'" . $this->_allowedAccountsSql() . " ORDER BY d.`fullname` ASC LIMIT 20", [
- 'q' => '%' . $sQ . '%',
- ]);
- foreach ($aRows as $aRow)
- $this->_pushProfile($aOut, $aSeen, (int)$aRow['id'], (string)$aRow['fullname'], 'search');
- }
- return $aOut;
- }
- protected function _profileButtons(array $aProfiles, string $sSuggest): array
- {
- $aOut = [];
- foreach ($aProfiles as $aP) {
- $s = (string)($aP['name'] ?? '');
- if ($s !== '' && !in_array($s, $aOut, true))
- $aOut[] = $s;
- }
- $aOut[] = _t('_sys_agents_profile_btn_new', $sSuggest);
- return $aOut;
- }
- protected function _suggestProfileName(string $sPreset): string
- {
- return _t($this->_preset($sPreset) === 'comment_reply' ? '_sys_agents_profile_name_commenter' : '_sys_agents_profile_name_bot');
- }
- /**
- * Accounts whose profiles an agent may act as: the operators' (the profiles
- * Studio offers, get_options_agents_profile) and the Robot account's, where
- * profile_new creates them. Never a member's: an agent on a member's profile
- * would post, and as a message agent answer their messages, in their name.
- *
- * @return int[]
- */
- protected function _allowedAccounts(): array
- {
- if ($this->_aAllowedAccounts === null) {
- $aIds = array_map('intval', (array)BxDolAccountQuery::getInstance()->getOperators());
- $aIds[] = $this->_botAccount();
- $this->_aAllowedAccounts = array_values(array_unique(array_filter($aIds)));
- }
- return $this->_aAllowedAccounts;
- }
- protected function _isAllowedAgentProfile(int $iProfileId): bool
- {
- if ($iProfileId <= 0)
- return false;
- $iAccount = (int)BxDolDb::getInstance()->getOne("SELECT `account_id` FROM `sys_profiles` WHERE `id` = :id LIMIT 1", ['id' => $iProfileId]);
- return $iAccount > 0 && in_array($iAccount, $this->_allowedAccounts(), true);
- }
- /** `AND p.account_id IN (...)` for a query over `sys_profiles` aliased `p`. */
- protected function _allowedAccountsSql(): string
- {
- $aIds = $this->_allowedAccounts();
- return ' AND p.`account_id` IN (' . ($aIds ? implode(',', $aIds) : '0') . ')';
- }
- protected function _pushProfile(array &$aOut, array &$aSeen, int $iId, string $sFallback, string $sSource): void
- {
- if ($iId <= 0 || isset($aSeen[$iId]) || !$this->_isAllowedAgentProfile($iId))
- return;
- try {
- $sName = $this->_profileDisplayName($iId, $sFallback);
- } catch (Throwable $o) {
- $sName = $sFallback;
- }
- if ($sName === '')
- $sName = '#' . $iId;
- $aOut[] = ['id' => $iId, 'name' => $sName, 'source' => $sSource];
- $aSeen[$iId] = 1;
- }
- protected function _shortDisplayName(string $s): array
- {
- $s = trim(preg_replace('/\s+/u', ' ', $s));
- $sPrefix = trim(_t('_sys_agents_profile_btn_new', ''));
- if ($sPrefix !== '' && $sPrefix !== '_sys_agents_profile_btn_new')
- $s = trim((string)preg_replace('/^' . preg_quote($sPrefix, '/') . '\s*/iu', '', $s));
- if ($s === '')
- return ['error' => 'profile name is empty'];
- $iWords = count(preg_split('/\s+/u', $s, -1, PREG_SPLIT_NO_EMPTY));
- if (mb_strlen($s) > self::PROFILE_NAME_MAX || $iWords > 3 || preg_match('/[\r\n.!?—–;:]/u', $s)) {
- return ['error' => _t('_sys_agents_profile_new_err', _t('_sys_agents_profile_name_commenter'), _t('_sys_agents_profile_name_bot'))];
- }
- return ['name' => $s];
- }
- protected function _profileDisplayName(int $iId, string $sFallback = ''): string
- {
- $o = BxDolProfile::getInstance($iId);
- if ($o)
- return $o->getDisplayName();
- if ($sFallback !== '')
- return $sFallback;
- return '#' . $iId;
- }
- protected function _resolveProfile($sProfile, $mixedId, $sNew, bool $bDry): array
- {
- $sNew = trim((string)$sNew);
- if ($sNew !== '')
- return $this->_createPersonProfile($sNew, $bDry);
- $sProfile = trim((string)$sProfile);
- $iId = (int)$mixedId;
- if ($sProfile !== '' && preg_match('/^\d+$/', $sProfile))
- $iId = (int)$sProfile;
- if ($iId > 0) {
- $o = BxDolProfile::getInstance($iId);
- if (!$o)
- return ['error' => "profile {$iId} not found"];
- if (!$this->_isAllowedAgentProfile($iId))
- return ['error' => "profile {$iId} belongs to a member, and an agent may not act as a member. Pick one from catalog.profiles or pass profile_new=<name>."];
- return ['profile_id' => $iId, 'name' => $o->getDisplayName(), 'created' => 0];
- }
- if ($sProfile === '')
- return ['profile_id' => 0, 'name' => '', 'created' => 0];
- $sProfile = ltrim($sProfile, '@');
- $aHits = [];
- foreach ($this->_listProfiles($sProfile) as $aP) {
- if (mb_strtolower((string)$aP['name']) === mb_strtolower($sProfile) || (string)$aP['id'] === $sProfile)
- $aHits[(int)$aP['id']] = $aP;
- }
- $oDb = BxDolDb::getInstance();
- if ($oDb->isTableExists('bx_persons_data')) {
- $aRows = $oDb->getAll("SELECT p.`id`, d.`fullname` FROM `sys_profiles` p INNER JOIN `bx_persons_data` d ON d.`id` = p.`content_id` AND p.`type` = 'bx_persons' WHERE d.`fullname` = :n AND p.`status` = 'active'" . $this->_allowedAccountsSql() . " LIMIT 10", [
- 'n' => $sProfile,
- ]);
- foreach ($aRows as $aRow)
- $aHits[(int)$aRow['id']] = ['id' => (int)$aRow['id'], 'name' => $aRow['fullname']];
- if (!$aHits) {
- $aRows = $oDb->getAll("SELECT p.`id`, d.`fullname` FROM `sys_profiles` p INNER JOIN `bx_persons_data` d ON d.`id` = p.`content_id` AND p.`type` = 'bx_persons' WHERE d.`fullname` LIKE :n AND p.`status` = 'active'" . $this->_allowedAccountsSql() . " ORDER BY d.`fullname` ASC LIMIT 10", [
- 'n' => '%' . $sProfile . '%',
- ]);
- foreach ($aRows as $aRow)
- $aHits[(int)$aRow['id']] = ['id' => (int)$aRow['id'], 'name' => $aRow['fullname']];
- }
- }
- $aList = array_values($aHits);
- if (count($aList) === 1)
- return ['profile_id' => (int)$aList[0]['id'], 'name' => $aList[0]['name'], 'created' => 0];
- if (count($aList) > 1)
- return ['error' => 'Several profiles match. Pass profile=exact name from catalog.profiles.', 'candidates' => $aList];
- return ['error' => 'No profile named «' . $sProfile . '». Pick from catalog.profiles or pass profile_new=' . $sProfile . '.'];
- }
- protected function _createPersonProfile(string $sName, bool $bDry): array
- {
- $aName = $this->_shortDisplayName($sName);
- if (!empty($aName['error']))
- return $aName;
- $sName = $aName['name'];
- $oDb = BxDolDb::getInstance();
- if (!$oDb->isTableExists('bx_persons_data'))
- return ['error' => 'bx_persons_data is missing — cannot create a Persons profile'];
- // new bot profiles always live under the preset Robot account (sys_profile_bot), not under the operator.
- // author is that Robot profile's id; sys_profiles.account_id is the account that owns it.
- $aBot = $this->_botIdentity();
- $iAuthor = (int)$aBot['profile_id'];
- $iAccount = (int)$aBot['account_id'];
- if ($iAuthor <= 0 || $iAccount <= 0)
- return ['error' => 'Robot account not found: set sys_profile_bot (Studio → Settings → Bot profile), then retry.'];
- $aExist = $oDb->getRow("SELECT p.`id` FROM `sys_profiles` p INNER JOIN `bx_persons_data` d ON d.`id` = p.`content_id` AND p.`type` = 'bx_persons' WHERE p.`account_id` = :a AND d.`fullname` = :n LIMIT 1", [
- 'a' => $iAccount,
- 'n' => $sName,
- ]);
- if ($aExist)
- return ['profile_id' => (int)$aExist['id'], 'name' => $sName, 'created' => 0];
- if ($bDry)
- return ['profile_id' => 0, 'name' => $sName, 'created' => 1, 'dry_create' => 1];
- $iNow = time();
- $iPublic = defined('BX_DOL_PG_ALL') ? BX_DOL_PG_ALL : 3;
- $aPerson = $this->_onlyExistingColumns($oDb, 'bx_persons_data', [
- 'author' => $iAuthor,
- 'added' => $iNow,
- 'changed' => $iNow,
- 'fullname' => $sName,
- 'last_name' => '',
- 'allow_view_to' => $iPublic,
- 'allow_post_to' => $iPublic,
- 'allow_contact_to' => $iPublic,
- ]);
- $mixed = $oDb->query("INSERT INTO `bx_persons_data` SET " . $oDb->arrayToSQL($aPerson));
- if ($mixed === false)
- return ['error' => 'failed to insert bx_persons_data'];
- $iContent = (int)$oDb->lastId();
- $this->_logSql('insert', 'bx_persons_data', 'id', (string)$iContent, 'INSERT person ' . $sName, null, array_merge($aPerson, ['id' => $iContent]));
- $iAction = defined('BX_PROFILE_ACTION_AUTO') ? BX_PROFILE_ACTION_AUTO : 0;
- $iProfile = (int)BxDolProfile::add($iAction, $iAccount, $iContent, 'active', 'bx_persons');
- if ($iProfile <= 0)
- return ['error' => 'person row created but sys_profiles insert failed', 'content_id' => $iContent];
- $this->_logSql('insert', 'sys_profiles', 'id', (string)$iProfile, 'INSERT profile ' . $sName, null, [
- 'id' => $iProfile,
- 'account_id' => $iAccount,
- 'content_id' => $iContent,
- 'type' => 'bx_persons',
- 'status' => 'active',
- ]);
- return ['profile_id' => $iProfile, 'name' => $sName, 'created' => 1];
- }
- protected function _resolveToolsCsv(string $sTools, string $sPreset)
- {
- $oDb = BxDolDb::getInstance();
- $aWanted = [];
- $sTools = trim($sTools);
- if ($sTools !== '') {
- foreach (preg_split('/[,\s]+/', $sTools) as $sTok) {
- $sTok = trim($sTok);
- if ($sTok === '')
- continue;
- $aWanted[] = $sTok;
- }
- } else if ($sPreset === 'comment_reply') {
- $aWanted = self::COMMENT_TOOLS;
- }
- if (!$aWanted)
- return '';
- $aIds = [];
- $aMissing = [];
- foreach ($aWanted as $sTok) {
- if (preg_match('/^\d+$/', $sTok)) {
- $aRow = $oDb->getRow("SELECT `id`, `type`, `active` FROM `sys_agents_tools` WHERE `id` = :id LIMIT 1", ['id' => (int)$sTok]);
- } else {
- $aRow = $oDb->getRow("SELECT `id`, `type`, `active` FROM `sys_agents_tools` WHERE `type` = :t ORDER BY `active` DESC, `id` ASC LIMIT 1", ['t' => $sTok]);
- }
- if (!$aRow) {
- $aMissing[] = $sTok;
- continue;
- }
- if (in_array($aRow['type'], self::FORBIDDEN_TOOL_TYPES, true))
- return ['ok' => 0, 'error' => 'Refusing stock mysql_write on a created agent.'];
- if (in_array($aRow['type'], BxDolAiAgentTools::hiddenTypes(), true))
- continue;
- $aIds[(int)$aRow['id']] = (int)$aRow['id'];
- }
- if ($aMissing)
- return ['ok' => 0, 'error' => 'Unknown tools: ' . implode(', ', $aMissing) . '. action=catalog to list types.'];
- return implode(',', array_values($aIds));
- }
- protected function _uniqueName(string $sName, string $sTitle): string
- {
- $s = $sName !== '' ? $sName : $sTitle;
- $s = strtolower($s);
- $s = preg_replace('/[^a-z0-9_]+/', '_', $s);
- $s = trim($s, '_');
- if ($s === '')
- $s = 'agent';
- if (strlen($s) > 80)
- $s = substr($s, 0, 80);
- $oDb = BxDolDb::getInstance();
- $sBase = $s;
- $i = 2;
- while ($oDb->getOne("SELECT `id` FROM `sys_agents_agents` WHERE `name` = :n LIMIT 1", ['n' => $s])) {
- $s = $sBase . '_' . $i;
- $i++;
- if ($i > 99)
- $s = $sBase . '_' . bin2hex(random_bytes(3));
- }
- return $s;
- }
- protected function _defaults(): array
- {
- $oDb = BxDolDb::getInstance();
- $aOp = null;
- $aCtx = BxDolAIToolMysqlWrite::currentLogContext();
- $iFromChat = (int)($aCtx['agent_id'] ?? 0);
- if ($iFromChat > 0)
- $aOp = $oDb->getRow("SELECT `id`, `model_id`, `profile_id`, `vector_store_id` FROM `sys_agents_agents` WHERE `id` = :id LIMIT 1", ['id' => $iFromChat]);
- if (!$aOp) {
- $iOpt = (int)getParam('sys_agents_operator_agent');
- if ($iOpt > 0)
- $aOp = $oDb->getRow("SELECT `id`, `model_id`, `profile_id`, `vector_store_id` FROM `sys_agents_agents` WHERE `id` = :id LIMIT 1", ['id' => $iOpt]);
- }
- $iModel = (int)($aOp['model_id'] ?? 0) ?: (int)(getParam('sys_agents_model'));
- $iProfile = (int)($aOp['profile_id'] ?? 0);
- if ($iProfile <= 0)
- $iProfile = (int)getParam('sys_profile_bot');
- $iStore = (int)($aOp['vector_store_id'] ?? 0);
- if ($iStore <= 0 && $oDb->isTableExists('sys_agents_vector_store'))
- $iStore = (int)$oDb->getOne("SELECT `id` FROM `sys_agents_vector_store` WHERE `active` = 1 ORDER BY `id` ASC LIMIT 1");
- return [
- 'model_id' => $iModel,
- 'profile_id' => $iProfile,
- 'vector_store_id' => $iStore,
- 'from_agent_id' => (int)($aOp['id'] ?? 0),
- ];
- }
- /**
- * Preset Robot profile and the account that owns it: sys_profile_bot, then the operator agent's profile.
- *
- * @return array{profile_id:int,account_id:int}
- */
- protected function _botIdentity(): array
- {
- $oDb = BxDolDb::getInstance();
- $aCandidates = [
- (int)getParam('sys_profile_bot'),
- (int)($this->_defaults()['profile_id'] ?? 0),
- ];
- foreach ($aCandidates as $iProfile) {
- if ($iProfile <= 0)
- continue;
- $iAccount = (int)$oDb->getOne("SELECT `account_id` FROM `sys_profiles` WHERE `id` = :id LIMIT 1", ['id' => $iProfile]);
- if ($iAccount > 0)
- return ['profile_id' => $iProfile, 'account_id' => $iAccount];
- }
- return ['profile_id' => 0, 'account_id' => 0];
- }
- /**
- * Account of the preset Robot. See _botIdentity().
- */
- protected function _botAccount(): int
- {
- return (int)$this->_botIdentity()['account_id'];
- }
- protected function _onlyExistingColumns(BxDolDb $oDb, string $sTable, array $aRow): array
- {
- $aOut = [];
- foreach ($aRow as $sK => $mixedV) {
- if ($oDb->isFieldExists($sTable, $sK))
- $aOut[$sK] = $mixedV;
- }
- return $aOut;
- }
- protected function _clip(string $s, int $iMax = 0): string
- {
- $iMax = $iMax ?: self::CLIP;
- $s = trim($s);
- if (strlen($s) <= $iMax)
- return $s;
- // at most $iMax bytes, never half a character: substr() split UTF-8 text mid-letter
- return mb_strcut($s, 0, $iMax, 'UTF-8');
- }
- protected function _logSql(string $sOp, string $sTable, string $sPk, string $sPkValue, string $sSql, $mixedBefore, $mixedAfter): void
- {
- $oDb = BxDolDb::getInstance();
- if (!$oDb->isTableExists('sys_agents_sql_log'))
- return;
- $aCtx = BxDolAIToolMysqlWrite::currentLogContext();
- $aSet = [
- 'agent_id' => (int)($aCtx['agent_id'] ?? 0),
- 'profile_id' => (int)($aCtx['profile_id'] ?? 0),
- 'thread_id' => (string)($aCtx['thread_id'] ?? ''),
- 'op' => $sOp,
- 'table_name' => $sTable,
- 'pk_name' => $sPk,
- 'pk_value' => $sPkValue,
- 'sql_text' => $sSql,
- 'before_json' => $mixedBefore ? json_encode($mixedBefore, JSON_UNESCAPED_UNICODE) : null,
- 'after_json' => $mixedAfter ? json_encode($mixedAfter, JSON_UNESCAPED_UNICODE) : null,
- 'added' => time(),
- ];
- if ($oDb->isFieldExists('sys_agents_sql_log', 'undone'))
- $aSet['undone'] = 0;
- $oDb->query("INSERT INTO `sys_agents_sql_log` SET " . $oDb->arrayToSQL($aSet));
- }
- }
- /** @} */
|