BuildPrompt($message, $form, $history, $files); // Make ONE single API call $response = $this->GetCaller()->Chat($prompt); return $this->ProcessResponse($response); } /** * Build the prompt messages array without calling the API. * Returns the prompt as an array of messages (system + user). */ public function BuildPrompt($message = null, $form = null, $history = null, $files = []) { $this->Form = $form; $this->History = $history; // 1. Extract all field IDs referenced in the message $referencedFieldIds = $this->ExtractFieldIds($message); // 2. Build field information text (datatype + comparison types) $fieldsText = $this->BuildFieldsText($referencedFieldIds); // 3. Build available settings text for referenced fields $settingsText = $this->BuildSettingsText($referencedFieldIds); // 4. Build conversation history $conversationHistory = $this->BuildConversationHistory(); // 5. Build the prompt if (!empty($this->PromptName)) { $promptFile = AllInOneForms()->GetLoader()->DIR . '/Managers/AIManagers/AIGenerators/prompts/' . $this->PromptName . '.txt'; } else { $promptFile = AllInOneForms()->GetLoader()->DIR . '/Managers/AIManagers/AIGenerators/AIContextualFormDesigner/ContextActions/ContextActionAll.txt'; } $prompt = file_get_contents($promptFile); $prompt = str_replace('{{fields}}', $fieldsText, $prompt); $prompt = str_replace('{{settings}}', $settingsText, $prompt); $prompt = str_replace('{{conversationHistory}}', $conversationHistory, $prompt); $prompt = str_replace('{{fieldDefinitions}}', AIManager::GetFieldDescriptor()->GetBasicFields(), $prompt); return [ $this->CreateMessage('system', $prompt), $this->CreateMessageWithFiles('user', $message, $files) ]; } /** * Process a raw JSON response string into the actions array the frontend expects. */ public function ProcessResponse($response) { $data = json_decode($response); if ($data == null || !isset($data->actions) || !is_array($data->actions)) { throw new FriendlyException('The AI could not understand the request, please rephrase and try again'); } $actions = []; foreach ($data->actions as $action) { if (isset($action->error)) { throw new FriendlyException($action->error); } $actionType = Sanitizer::GetStringValueFromPath($action, ['action_type']); switch ($actionType) { case 'condition': $result = $this->ProcessConditionAction($action); if ($result !== null) { $actions[] = $result; } break; case 'set_field_setting': $result = $this->ProcessSetFieldSettingAction($action); if ($result !== null) { $actions[] = $result; } break; case 'add_field': $result = $this->ProcessAddFieldAction($action); if ($result !== null) { $actions[] = $result; } break; case 'move_field': $result = $this->ProcessMoveFieldAction($action); if ($result !== null) { $actions[] = $result; } break; } } if (count($actions) == 0) { throw new FriendlyException('The AI could not understand the context of the message, please rephrase and try again'); } return $actions; } /** * Extract all field IDs from the message * Matches [Field 123] and [Field 123_subfield] patterns */ private function ExtractFieldIds($message) { $fieldIds = []; if (preg_match_all('/\[Field\s+(\d+(?:_[a-zA-Z0-9]+)?)\]/i', $message, $matches)) { foreach ($matches[1] as $fieldId) { if (!in_array($fieldId, $fieldIds)) { $fieldIds[] = $fieldId; } } } return $fieldIds; } /** * Build the field information text including datatype and available comparisons */ private function BuildFieldsText($fieldIds) { if (empty($fieldIds)) { return "No fields referenced"; } $descriptor = new AIFieldDescriptor(); $text = ''; foreach ($fieldIds as $fieldId) { $fieldParts = $this->AIFieldUtilities->GetIdParts($fieldId); $field = $this->Form->GetFieldById($fieldParts[0], true, false, 'FieldTemplate'); if ($field == null) { continue; } $fieldDescription = $descriptor->GetField($field->Options->Type); if ($fieldDescription == null) { continue; } $text .= '- ID: "' . $fieldId . '", FieldType: "' . $fieldDescription["datatype"] . '", Type: "' . $field->Options->Type . '"' . "\r\n"; } return $text; } /** * Build the available settings text for referenced fields */ private function BuildSettingsText($fieldIds) { if (empty($fieldIds)) { return "No settings available"; } $descriptor = new AIFieldDescriptor(); $settings = []; foreach ($fieldIds as $fieldId) { $fieldParts = $this->AIFieldUtilities->GetIdParts($fieldId); $field = $this->Form->GetFieldById($fieldParts[0], true, false, 'FieldTemplate'); if ($field == null) { continue; } $dataType = $field->Options->Type; $fieldSettings = $descriptor->GetFieldSettingsByFieldType($dataType); foreach ($fieldSettings as $currentSetting) { if (!isset($settings[$currentSetting['name']])) { $text = $currentSetting['name']; if (isset($currentSetting['usage']) && !empty($currentSetting['usage'])) { $text .= ". " . $currentSetting['usage']; } if (isset($currentSetting['instructions']) && !empty($currentSetting['instructions'])) { $text .= ". Instructions:" . $currentSetting['instructions']; } $settings[$currentSetting['name']] = [ 'text' => $text, 'used_by' => [] ]; } if (!in_array($dataType, $settings[$currentSetting['name']]['used_by'])) { $settings[$currentSetting['name']]['used_by'][] = $dataType; } } } $settingsText = ''; foreach ($settings as $currentSetting) { $settingsText .= $currentSetting['text'] . '. Used By: ' . implode(',', $currentSetting['used_by']) . "\r\n"; } return $settingsText; } /** * Build conversation history from previous user messages */ private function BuildConversationHistory() { $userMessages = $this->History->GetAllMessagesOfRole('user'); $messageHistory = ""; foreach ($userMessages as $userMessage) { if ($messageHistory != "") { $messageHistory .= "\n\n"; } $messageHistory .= $userMessage->content; } if (trim($messageHistory) == "") { $messageHistory = "No conversation history"; } return $messageHistory; } /** * Process a condition action from the AI response * Reuses sanitization logic from GenerateConditionContextAction */ private function ProcessConditionAction($action) { $targetField = Sanitizer::GetStringValueFromPath($action, ['target_field']); if (empty($targetField)) { return null; } // Validate target field exists $fieldParts = $this->AIFieldUtilities->GetIdParts($targetField); $field = $this->Form->GetFieldById($fieldParts[0], true, false, 'FieldTemplate'); if ($field == null) { return null; } $conditionOptions = $this->SanitizeCondition($targetField, $action); return [ 'ActionType' => 'CreateCondition', 'IntentText' => Sanitizer::GetStringValueFromPath($action, ['intent_text'], ''), 'Data' => [ 'ConditionOptions' => $conditionOptions, 'TargetField' => $targetField, ] ]; } /** * Sanitize a condition action - adapted from GenerateConditionContextAction::Sanitize */ private function SanitizeCondition($targetField, $data) { $type = Sanitizer::GetStringValueFromPath($data, ['ConditionType']); $conditions = Sanitizer::GetValueFromPath($data, ['Condition']); $rootFormula = trim(Sanitizer::GetStringValueFromPath($data, ['RootFormula'], '')); if ($type == null) { throw new FriendlyException('The type of condition does not exist'); } if ($rootFormula == '' && ($conditions == null || !is_array($conditions) || count($conditions) == 0)) { throw new FriendlyException('No conditions found'); } $options = new stdClass(); switch ($type) { case 'Hide': $type = 'ShowHide'; $options->ShowWhenTrue = false; break; case 'Required': $type = 'Required'; break; case 'Validation': $options->InvalidMessage = Sanitizer::GetStringValueFromPath($data, ['ErrorMessage'], 'Invalid Field'); $type = 'Validation'; break; default: throw new FriendlyException('The type of condition does not exist'); } $options->Type = $type; $options->ConditionGroups = []; if ($rootFormula != '') { $options->RootFormula = $rootFormula; $conditions = []; } foreach ($conditions as $conditionGroup) { if (!is_array($conditionGroup) || count($conditionGroup) == 0) { throw new FriendlyException('The condition is not valid'); } $optionConditionGroup = new stdClass(); $optionConditionGroup->ConditionLines = []; $options->ConditionGroups[] = $optionConditionGroup; foreach ($conditionGroup as $conditionLine) { $fieldId = Sanitizer::GetStringValueFromPath($conditionLine, ['Field']); $comparisonType = Sanitizer::GetStringValueFromPath($conditionLine, ['ComparisonType']); $value = Sanitizer::GetValueFromPath($conditionLine, ['Value']); $field = $this->Form->GetFieldById($this->AIFieldUtilities->GetIdParts($fieldId)[0], true, false, 'FieldTemplate'); if ($field == null) { throw new FriendlyException('One of the fields used in a condition was not found'); } $fieldDescription = $this->FieldDescription->GetField($field->Options->Type); $subType = 'Standard'; if (in_array($field->Options->Type, ['checkbox', 'radio', 'dropdown', 'buttonselection', 'imagepicker'])) { $subType = 'MultipleValues'; } if (in_array($field->Options->Type, ['googlemaps'])) { $subType = 'GoogleMaps'; } if (in_array($field->Options->Type, ['slider', 'numeric'])) { $subType = 'Numeric'; } if (in_array($field->Options->Type, ['datepicker', 'daterange'])) { $subType = 'Date'; } if (in_array($field->Options->Type, ['list'])) { $subType = 'List'; } if (in_array($field->Options->Type, ['switch'])) { $subType = 'Checkbox'; } if (in_array($field->Options->Type, ['actionbutton'])) { $subType = 'Button'; } if ($subType == 'MultipleValues') { if (!is_array($value)) { $value = [$value]; } $value = array_map(function ($item) { return intval($item); }, $value); } $fieldType = $fieldDescription['datatype']; $supportedComparisons = [ 'composed' => [ 'EqualTo', 'NotEqualTo', 'Contains', 'NotContains', 'IsEmpty', 'IsNotEmpty' ], 'text' => [ 'EqualTo', 'NotEqualTo', 'Contains', 'NotContains', 'IsEmpty', 'IsNotEmpty' ], 'number' => [ 'EqualTo', 'NotEqualTo', 'IsEmpty', 'IsNotEmpty', 'GreaterThan', 'LessThan', 'GreaterOrEqualThan', 'LessOrEqualThan' ], 'date' => [ 'EqualTo', 'NotEqualTo', 'IsEmpty', 'IsNotEmpty', 'GreaterThan', 'LessThan', 'GreaterOrEqualThan', 'LessOrEqualThan' ], 'multiple_options' => [ 'ContainsOption', 'NotContainsOption', 'IsEmpty', 'IsNotEmpty' ], 'boolean' => [ 'IsChecked', 'IsNotChecked' ] ]; if (!array_key_exists($fieldType, $supportedComparisons) || !in_array($comparisonType, $supportedComparisons[$fieldType])) { throw new FriendlyException('The comparison type is not valid'); } if ($comparisonType == 'EqualTo') $comparisonType = 'Equal'; if ($comparisonType == 'NotEqualTo') $comparisonType = 'NotEqual'; if ($comparisonType == 'ContainsOption') $comparisonType = 'Contains'; if ($comparisonType == 'NotContainsOption') $comparisonType = 'NotContains'; $optionConditionLine = new stdClass(); $optionConditionLine->Type = 'Field'; $optionConditionLine->SubType = $subType; $optionConditionLine->FieldId = $fieldId; $optionConditionLine->Comparison = $comparisonType; if (isset($conditionLine->Formula)) { $optionConditionLine->Value = ''; $optionConditionLine->Formula = $conditionLine->Formula; } else { $optionConditionLine->Value = $value; } $optionConditionGroup->ConditionLines[] = $optionConditionLine; } } return $options; } /** * Process a set_field_setting action from the AI response * Reuses sanitization logic from SetFieldSettingContextAction */ private function ProcessSetFieldSettingAction($action) { $targetField = Sanitizer::GetStringValueFromPath($action, ['target_field']); if (empty($targetField)) { return null; } if (isset($action->error) && !empty($action->error)) { throw new FriendlyException($action->error); } $field = $this->Form->GetFieldById($targetField, true, false, 'FieldTemplate'); if ($field === null) { throw new FriendlyException('The change can not be done because the field with ID "' . $targetField . '" was not found in the form'); } $descriptor = new AIFieldDescriptor(); $dataType = $field->Options->Type; $fieldDescription = $descriptor->GetField($dataType); if ( !array_filter($fieldDescription['settings'], function ($setting) use ($action) { return $setting === $action->setting || strpos($setting, $action->setting . ':') === 0; }) ) { throw new FriendlyException('The setting ' . $action->setting . ' is not valid for the field of type ' . $dataType); } $setting = $descriptor->GetSetting($action->setting); $settingName = $action->setting; if (isset($setting['realName'])) { $settingName = $setting['realName']; } return [ 'ActionType' => 'SetFieldSetting', 'Data' => (object) [ 'target_field' => $targetField, 'setting' => $settingName, 'value' => Sanitizer::GetStringValueFromPath($action, ['value'], ''), 'formula' => Sanitizer::GetStringValueFromPath($action, ['formula'], '') ] ]; } /** * Process an add_field action from the AI response */ private function ProcessAddFieldAction($action) { $field = isset($action->field) ? $action->field : null; if ($field == null || !isset($field->name) || !isset($field->label)) { return null; } $positionField = Sanitizer::GetStringValueFromPath($action, ['position_field'], ''); $position = Sanitizer::GetStringValueFromPath($action, ['position'], 'below'); // Validate position value if (!in_array($position, ['below', 'above', 'left', 'right'])) { $position = 'below'; } // Validate position field exists if provided if (!empty($positionField)) { $fieldParts = $this->AIFieldUtilities->GetIdParts($positionField); $existingField = $this->Form->GetFieldById($fieldParts[0], true, false, 'FieldTemplate'); if ($existingField == null) { return null; } } return [ 'ActionType' => 'AddField', 'Data' => (object) [ 'position_field' => $positionField, 'position' => $position, 'field' => $field ] ]; } /** * Process a move_field action from the AI response */ private function ProcessMoveFieldAction($action) { $sourceField = Sanitizer::GetStringValueFromPath($action, ['source_field'], ''); $targetField = Sanitizer::GetStringValueFromPath($action, ['target_field'], ''); $position = Sanitizer::GetStringValueFromPath($action, ['position'], 'below'); if (empty($sourceField) || empty($targetField)) { return null; } // Validate position value if (!in_array($position, ['below', 'above', 'left', 'right'])) { $position = 'below'; } // Validate source field exists $sourceParts = $this->AIFieldUtilities->GetIdParts($sourceField); $existingSource = $this->Form->GetFieldById($sourceParts[0], true, false, 'FieldTemplate'); if ($existingSource == null) { return null; } // Validate target field exists $targetParts = $this->AIFieldUtilities->GetIdParts($targetField); $existingTarget = $this->Form->GetFieldById($targetParts[0], true, false, 'FieldTemplate'); if ($existingTarget == null) { return null; } return [ 'ActionType' => 'MoveField', 'Data' => (object) [ 'source_field' => $sourceField, 'target_field' => $targetField, 'position' => $position ] ]; } }