Decouple title from title block (#696)
* Decouple title from title block * fix lint * remove dry run for FormTitleMigration * Skip form title migration for forms with hidden titles * Refactor AI Form Generation with Dedicated Prompt Services - Extract AI form generation logic from GenerateTemplate command into dedicated prompt service classes - Update GenerateAiForm job to use new prompt generation services - Improve GptCompleter with more robust error handling and token tracking - Add error field to AiFormCompletion model for better error logging - Simplify command signature from 'ai:make-form-template' to 'form:generate' * Consolidate Template Metadata Generation with Unified Prompt Service - Create GenerateTemplateMetadataPrompt to centralize template metadata generation - Update GenerateTemplate command to use new consolidated metadata generation approach - Enhance GptCompleter to support strict JSON schema validation - Increase form prompt max length to support more complex form descriptions - Refactor form generation to simplify metadata extraction and processing * Implement Form Mode Strategy for Flexible Form Rendering - Introduce FormModeStrategy to centralize form rendering logic - Add support for different form modes: LIVE, PREVIEW, TEST, EDIT, PREFILL - Refactor components to use mode-based rendering strategy - Remove legacy boolean props like adminPreview and creating - Enhance form component flexibility and reusability * Refine Form Mode Strategy Display Behavior - Update FormModeStrategy to hide hidden fields in PREVIEW mode - Add FormMode getter in UrlFormPrefill component for mode-specific rendering - Clarify mode-specific validation and display logic in form strategies * Enhance Form Template Generation with Advanced Field Options - Update GenerateTemplate command to use front_url for template URL output - Expand GenerateFormPrompt with comprehensive field configuration options - Add support for advanced field types: date with time, toggle switches, radio/checkbox selections - Introduce field width configuration and HTML formatting for text elements - Re-enable select, multi-select, and matrix field type definitions with enhanced configurations * Remove Deprecated Template Metadata Generation Services - Delete multiple AI prompt services related to template metadata generation - Simplify GenerateTemplate command to use default values instead of complex metadata generation - Remove GenerateTemplateMetadataPrompt and related classes like GenerateTemplateDescriptionPrompt, GenerateTemplateImageKeywordsPrompt, etc. - Update form template generation to use basic fallback metadata generation approach * Restore GenerateTemplateMetadataPrompt for Comprehensive Template Generation - Reintroduce GenerateTemplateMetadataPrompt to replace default metadata generation - Update GenerateTemplate command to use consolidated metadata generation approach - Extract detailed metadata components including title, description, industries, and image search query - Improve template generation with more dynamic and AI-generated metadata * Refactor Template Preview Section Layout - Remove unnecessary nested div in template preview section - Simplify HTML structure for the template preview component - Maintain existing styling and functionality while improving code readability * Refactor Constructor and Code Formatting in AI Form Generation and Prompt Classes - Updated the constructor in GenerateAiForm.php to use a block structure for improved readability and consistency. - Added a blank line in the Prompt.php file to enhance code formatting and maintain consistency with PHP coding standards. - Modified the migration file to use a more concise class declaration syntax, improving clarity. These changes aim to enhance code readability and maintainability across the affected files. --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
578
api/app/Service/AI/Prompts/Form/GenerateFormPrompt.php
Normal file
578
api/app/Service/AI/Prompts/Form/GenerateFormPrompt.php
Normal file
@@ -0,0 +1,578 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\AI\Prompts\Form;
|
||||
|
||||
use App\Service\AI\Prompts\Prompt;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class GenerateFormPrompt extends Prompt
|
||||
{
|
||||
protected float $temperature = 0.81;
|
||||
|
||||
protected int $maxTokens = 3000;
|
||||
|
||||
/**
|
||||
* The prompt template for generating forms
|
||||
*/
|
||||
public const PROMPT_TEMPLATE = <<<'EOD'
|
||||
Help me build the json structure for the form described below, be as accurate as possible.
|
||||
|
||||
<form_description>
|
||||
{formPrompt}
|
||||
</form_description>
|
||||
|
||||
Forms are represented as Json objects. There are several input types and layout block types (type start with nf-).
|
||||
You can use for instance nf-text to add a title or text to the form using some basic html (h1, p, b, i, u etc).
|
||||
Order of blocks matters.
|
||||
|
||||
Available field types:
|
||||
- text: Text input (use multi_lines: true for multi-line text)
|
||||
- rich_text: Rich text input
|
||||
- date: Date picker (use with_time: true to include time selection)
|
||||
- url: URL input with validation
|
||||
- phone_number: Phone number input
|
||||
- email: Email input with validation
|
||||
- checkbox: Single checkbox for yes/no (use use_toggle_switch: true for toggle switch)
|
||||
- select: Dropdown selection (use without_dropdown: true for radio buttons, recommended for <5 options)
|
||||
- multi_select: Multiple selection (use without_dropdown: true for checkboxes, recommended for <5 options)
|
||||
- matrix: Matrix input with rows and columns
|
||||
- number: Numeric input
|
||||
- rating: Star rating
|
||||
- scale: Numeric scale
|
||||
- slider: Slider selection
|
||||
- files: File upload
|
||||
- signature: Signature pad
|
||||
- barcode: Barcode scanner
|
||||
- nf-text: Rich text content (not an input field)
|
||||
- nf-page-break: Page break for multi-page forms
|
||||
- nf-divider: Visual divider (not an input field)
|
||||
- nf-image: Image element
|
||||
- nf-code: Code block
|
||||
|
||||
HTML formatting for nf-text:
|
||||
- Headers: <h1>, <h2> for section titles and subtitles
|
||||
- Text formatting: <b> or <strong> for bold, <i> or <em> for italic, <u> for underline, <s> for strikethrough
|
||||
- Links: <a href="url">link text</a> for hyperlinks
|
||||
- Lists: <ul><li>item</li></ul> for bullet lists, <ol><li>item</li></ol> for numbered lists
|
||||
- Colors: <span style="color: #hexcode">colored text</span> for colored text
|
||||
- Paragraphs: <p>paragraph text</p> for text blocks with spacing
|
||||
Use these HTML tags to create well-structured and visually appealing form content.
|
||||
|
||||
Field width options:
|
||||
- width: "full" (default, takes entire width)
|
||||
- width: "1/2" (takes half width)
|
||||
- width: "1/3" (takes a third of the width)
|
||||
- width: "2/3" (takes two thirds of the width)
|
||||
- width: "1/4" (takes a quarter of the width)
|
||||
- width: "3/4" (takes three quarters of the width)
|
||||
Fields with width less than "full" will be placed on the same line if there's enough room. For example:
|
||||
- Two 1/2 width fields will be placed side by side
|
||||
- Three 1/3 width fields will be placed on the same line
|
||||
- etc.
|
||||
No need for lines width to be complete. Don't abuse putting multiple fields on the same line if it doens't make sense. For First name and Last name, it works well for instance.
|
||||
|
||||
If the form is too long, you can paginate it by adding one or multiple page breaks (nf-page-break).
|
||||
|
||||
Create a complete form with appropriate fields based on the description. Include:
|
||||
- A clear `title` (internal for form admin)
|
||||
- `nf-text` blocks to add a title or text to the form using some basic html (h1, p, b, i, u etc)
|
||||
- Logical field grouping
|
||||
- Required fields where necessary
|
||||
- Help text for complex fields
|
||||
- Appropriate validation
|
||||
- Customized submission text
|
||||
EOD;
|
||||
|
||||
/**
|
||||
* JSON schema for form output
|
||||
*/
|
||||
protected ?array $jsonSchema = [
|
||||
'type' => 'object',
|
||||
'required' => ['title', 'properties', 're_fillable', 'use_captcha', 'redirect_url', 'submitted_text', 'uppercase_labels', 'submit_button_text', 're_fill_button_text', 'color'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'title' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The title of the form (default: "New Form")'
|
||||
],
|
||||
're_fillable' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether the form can be refilled after submission (default: false)'
|
||||
],
|
||||
'use_captcha' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether to use CAPTCHA for spam protection (default: false)'
|
||||
],
|
||||
'redirect_url' => [
|
||||
'type' => ['string', 'null'],
|
||||
'description' => 'URL to redirect to after submission (default: null)'
|
||||
],
|
||||
'submitted_text' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Text to display after form submission (default: "<p>Thank you for your submission!</p>")'
|
||||
],
|
||||
'uppercase_labels' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether to display field labels in uppercase (default: false)'
|
||||
],
|
||||
'submit_button_text' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Text for the submit button (default: "Submit")'
|
||||
],
|
||||
're_fill_button_text' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Text for the refill button (default: "Fill Again")'
|
||||
],
|
||||
'color' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Primary color for the form (default: "#64748b")'
|
||||
],
|
||||
'properties' => [
|
||||
'type' => 'array',
|
||||
'description' => 'Array of form fields and elements',
|
||||
'items' => [
|
||||
'anyOf' => [
|
||||
['$ref' => '#/definitions/textProperty'],
|
||||
['$ref' => '#/definitions/richTextProperty'],
|
||||
['$ref' => '#/definitions/dateProperty'],
|
||||
['$ref' => '#/definitions/urlProperty'],
|
||||
['$ref' => '#/definitions/phoneNumberProperty'],
|
||||
['$ref' => '#/definitions/emailProperty'],
|
||||
['$ref' => '#/definitions/checkboxProperty'],
|
||||
['$ref' => '#/definitions/selectProperty'],
|
||||
['$ref' => '#/definitions/multiSelectProperty'],
|
||||
['$ref' => '#/definitions/matrixProperty'],
|
||||
['$ref' => '#/definitions/numberProperty'],
|
||||
['$ref' => '#/definitions/ratingProperty'],
|
||||
['$ref' => '#/definitions/scaleProperty'],
|
||||
['$ref' => '#/definitions/sliderProperty'],
|
||||
['$ref' => '#/definitions/filesProperty'],
|
||||
['$ref' => '#/definitions/signatureProperty'],
|
||||
['$ref' => '#/definitions/barcodeProperty'],
|
||||
['$ref' => '#/definitions/nfTextProperty'],
|
||||
['$ref' => '#/definitions/nfPageBreakProperty'],
|
||||
['$ref' => '#/definitions/nfDividerProperty'],
|
||||
['$ref' => '#/definitions/nfImageProperty'],
|
||||
['$ref' => '#/definitions/nfCodeProperty']
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'definitions' => [
|
||||
'option' => [
|
||||
'type' => 'object',
|
||||
'required' => ['name', 'id'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'name' => ['type' => 'string'],
|
||||
'id' => ['type' => 'string']
|
||||
]
|
||||
],
|
||||
'selectOptions' => [
|
||||
'type' => 'object',
|
||||
'required' => ['options'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'options' => [
|
||||
'type' => 'array',
|
||||
'items' => ['$ref' => '#/definitions/option'],
|
||||
'description' => 'Options for select fields'
|
||||
]
|
||||
]
|
||||
],
|
||||
'baseProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['name', 'help', 'hidden', 'required', 'placeholder', 'width'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'name' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The name/label of the field'
|
||||
],
|
||||
'help' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Help text for the field (default: null)'
|
||||
],
|
||||
'hidden' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether the field is hidden (default: false)'
|
||||
],
|
||||
'required' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether the field is required (default: false)'
|
||||
],
|
||||
'placeholder' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Placeholder text for the field. Leave empty if not needed (default: "")'
|
||||
],
|
||||
'width' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['full', '1/2', '1/3', '2/3', '1/4', '3/4'],
|
||||
'description' => 'Width of the field in the form layout. "full" takes the entire width, "1/2" takes half width, "1/3" takes a third, etc. (default: "full")',
|
||||
]
|
||||
]
|
||||
],
|
||||
'textProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['type', 'core', 'multi_lines', 'generates_uuid', 'max_char_limit', 'hide_field_name', 'show_char_limit'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['text']],
|
||||
'multi_lines' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether the text field should have multiple lines (default: false)',
|
||||
],
|
||||
'generates_uuid' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether the field should generate a UUID (default: false)'
|
||||
],
|
||||
'max_char_limit' => [
|
||||
'type' => ['integer', 'string'],
|
||||
'description' => 'Maximum character limit for text fields (default: 500)'
|
||||
],
|
||||
'hide_field_name' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether to hide the field name (default: false)'
|
||||
],
|
||||
'show_char_limit' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether to show the character limit (default: false)'
|
||||
],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
]
|
||||
],
|
||||
'richTextProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['type', 'core', 'max_char_limit'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['rich_text']],
|
||||
'max_char_limit' => [
|
||||
'type' => ['integer', 'string'],
|
||||
'description' => 'Maximum character limit for rich text fields (default: 1000)'
|
||||
],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
]
|
||||
],
|
||||
'dateProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'with_time'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['date']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'with_time' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether to include time selection with the date (default: false)',
|
||||
]
|
||||
]
|
||||
],
|
||||
'urlProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'max_char_limit'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['url']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'max_char_limit' => [
|
||||
'type' => ['integer', 'string'],
|
||||
'description' => 'Maximum character limit for URL fields (default: 500)'
|
||||
]
|
||||
]
|
||||
],
|
||||
'phoneNumberProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['phone_number']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
]
|
||||
],
|
||||
'emailProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'max_char_limit'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['email']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'max_char_limit' => [
|
||||
'type' => ['integer', 'string'],
|
||||
'description' => 'Maximum character limit for email fields (default: 320)'
|
||||
]
|
||||
]
|
||||
],
|
||||
'checkboxProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'use_toggle_switch'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['checkbox']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'use_toggle_switch' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether to display the checkbox as a toggle switch (default: false)',
|
||||
]
|
||||
]
|
||||
],
|
||||
'selectProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'select', 'without_dropdown'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['select']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'select' => ['$ref' => '#/definitions/selectOptions'],
|
||||
'without_dropdown' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether to display select options as radio buttons instead of a dropdown using FlatSelectInput. Recommended for small choices (<5 options) (default: false)',
|
||||
]
|
||||
]
|
||||
],
|
||||
'multiSelectProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'multi_select', 'without_dropdown'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['multi_select']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'multi_select' => ['$ref' => '#/definitions/selectOptions'],
|
||||
'without_dropdown' => [
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether to display multi-select options as checkboxes instead of a dropdown using FlatSelectInput. Recommended for small choices (<5 options) (default: false)',
|
||||
]
|
||||
]
|
||||
],
|
||||
'matrixProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'rows', 'columns'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['matrix']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'rows' => [
|
||||
'type' => 'array',
|
||||
'items' => ['type' => 'string'],
|
||||
'description' => 'Rows for matrix fields (ex: ["Row 1"])'
|
||||
],
|
||||
'columns' => [
|
||||
'type' => 'array',
|
||||
'items' => ['type' => 'string'],
|
||||
'description' => 'Columns for matrix fields (ex: ["1", "2", "3"])'
|
||||
]
|
||||
]
|
||||
],
|
||||
'numberProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['number']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
]
|
||||
],
|
||||
'ratingProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'rating_max_value'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['rating']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'rating_max_value' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Maximum rating for rating fields (default: 5)'
|
||||
]
|
||||
]
|
||||
],
|
||||
'scaleProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'scale_min_value', 'scale_max_value', 'scale_step_value'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['scale']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'scale_min_value' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Minimum value for scale fields (default: 1)'
|
||||
],
|
||||
'scale_max_value' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Maximum value for scale fields (default: 5)'
|
||||
],
|
||||
'scale_step_value' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Step value for scale fields (default: 1)'
|
||||
]
|
||||
]
|
||||
],
|
||||
'sliderProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'slider_min_value', 'slider_max_value', 'slider_step_value'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['slider']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'slider_min_value' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Minimum value for slider fields (default: 0)'
|
||||
],
|
||||
'slider_max_value' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Maximum value for slider fields (default: 50)'
|
||||
],
|
||||
'slider_step_value' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Step value for slider fields (default: 1)'
|
||||
]
|
||||
]
|
||||
],
|
||||
'filesProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['files']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
]
|
||||
],
|
||||
'signatureProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['signature']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
]
|
||||
],
|
||||
'barcodeProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'decoders'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['barcode']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'decoders' => [
|
||||
'type' => 'array',
|
||||
'items' => ['type' => 'string'],
|
||||
'description' => 'Decoders for barcode fields (default: ["ean_reader", "ean_8_reader"])'
|
||||
]
|
||||
]
|
||||
],
|
||||
'nfTextProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'content'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['nf-text']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'content' => [
|
||||
'type' => 'string',
|
||||
'description' => 'HTML content for text elements. Supports headers (<h1>, <h2>), formatting (<b>, <i>, <u>, <s>), links (<a>), lists (<ul>, <ol>), colors (<span style="color: #hexcode">), and paragraphs (<p>). Example: "<h1>Form Title</h1><p>Please fill out this form.</p>"'
|
||||
]
|
||||
]
|
||||
],
|
||||
'nfPageBreakProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type', 'next_btn_text', 'previous_btn_text'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['nf-page-break']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
'next_btn_text' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Text for the next button in page breaks (default: "Next")'
|
||||
],
|
||||
'previous_btn_text' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Text for the previous button in page breaks (default: "Previous")'
|
||||
]
|
||||
]
|
||||
],
|
||||
'nfDividerProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['nf-divider']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
]
|
||||
],
|
||||
'nfImageProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['nf-image']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
]
|
||||
],
|
||||
'nfCodeProperty' => [
|
||||
'type' => 'object',
|
||||
'required' => ['core', 'type'],
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'type' => ['type' => 'string', 'enum' => ['nf-code']],
|
||||
'core' => ['$ref' => '#/definitions/baseProperty'],
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
public string $formPrompt
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function getSystemMessage(): ?string
|
||||
{
|
||||
return 'You are an AI assistant specialized in creating form structures. Design intuitive, user-friendly forms that capture all necessary information based on the provided description.';
|
||||
}
|
||||
|
||||
protected function getPromptTemplate(): string
|
||||
{
|
||||
return self::PROMPT_TEMPLATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the execute method to automatically process the output
|
||||
*/
|
||||
public function execute(): array
|
||||
{
|
||||
$formData = parent::execute();
|
||||
return $this->processOutput($formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the AI output to ensure it meets our requirements
|
||||
*/
|
||||
public function processOutput(array $formData): array
|
||||
{
|
||||
// Add unique identifiers to properties
|
||||
if (isset($formData['properties']) && is_array($formData['properties'])) {
|
||||
foreach ($formData['properties'] as $index => $property) {
|
||||
// Add a unique ID to each property
|
||||
$formData['properties'][$index]['id'] = Str::uuid()->toString();
|
||||
|
||||
// Flatten core properties if they exist
|
||||
if (isset($property['core']) && is_array($property['core'])) {
|
||||
foreach ($property['core'] as $coreKey => $coreValue) {
|
||||
$formData['properties'][$index][$coreKey] = $coreValue;
|
||||
}
|
||||
// Remove the core property after flattening
|
||||
unset($formData['properties'][$index]['core']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean title data
|
||||
if (isset($formData['title'])) {
|
||||
// Remove quotes if the title is enclosed in them
|
||||
$formData['title'] = preg_replace('/^["\'](.*)["\']$/', '$1', $formData['title']);
|
||||
}
|
||||
|
||||
ray($formData);
|
||||
|
||||
return $formData;
|
||||
}
|
||||
}
|
||||
146
api/app/Service/AI/Prompts/Prompt.php
Normal file
146
api/app/Service/AI/Prompts/Prompt.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\AI\Prompts;
|
||||
|
||||
use App\Service\OpenAi\GptCompleter;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use ReflectionClass;
|
||||
use ReflectionProperty;
|
||||
|
||||
abstract class Prompt
|
||||
{
|
||||
protected GptCompleter $completer;
|
||||
|
||||
protected ?array $jsonSchema = null;
|
||||
|
||||
protected float $temperature = 0.7;
|
||||
|
||||
protected int $maxTokens = 4096;
|
||||
|
||||
protected string $model = 'gpt-4o';
|
||||
|
||||
protected bool $useStreaming = false;
|
||||
|
||||
/**
|
||||
* Static method to create and execute a prompt in one step
|
||||
*
|
||||
* @param array $args Arguments to pass to the prompt constructor
|
||||
* @return mixed The result of executing the prompt
|
||||
*/
|
||||
public static function run(...$args): mixed
|
||||
{
|
||||
$reflection = new ReflectionClass(static::class);
|
||||
$instance = $reflection->newInstanceArgs($args);
|
||||
return $instance->execute();
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->completer = new GptCompleter(null, 2, $this->model);
|
||||
}
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
if ($this->jsonSchema) {
|
||||
$this->completer->setJsonSchema($this->jsonSchema);
|
||||
}
|
||||
|
||||
if ($this->getSystemMessage()) {
|
||||
$this->completer->setSystemMessage($this->getSystemMessage());
|
||||
}
|
||||
|
||||
if ($this->useStreaming) {
|
||||
$this->completer->useStreaming();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to set a custom system message
|
||||
*/
|
||||
protected function getSystemMessage(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Must return the prompt template with placeholders
|
||||
*/
|
||||
abstract protected function getPromptTemplate(): string;
|
||||
|
||||
public function getCompleter(): GptCompleter
|
||||
{
|
||||
return $this->completer;
|
||||
}
|
||||
|
||||
public function execute(): mixed
|
||||
{
|
||||
$this->initialize();
|
||||
$prompt = $this->buildPrompt();
|
||||
|
||||
try {
|
||||
$this->completer->completeChat(
|
||||
[['role' => 'user', 'content' => $prompt]],
|
||||
$this->maxTokens,
|
||||
$this->temperature
|
||||
);
|
||||
|
||||
return $this->jsonSchema ? $this->completer->getArray() : $this->completer->getString();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error while executing prompt', [
|
||||
'exception' => $e,
|
||||
'prompt' => $prompt,
|
||||
'json_schema' => $this->jsonSchema ?? null
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildPrompt(): string
|
||||
{
|
||||
$template = $this->getPromptTemplate();
|
||||
$variables = $this->getPromptVariables();
|
||||
|
||||
return strtr($template, $variables);
|
||||
}
|
||||
|
||||
protected function getPromptVariables(): array
|
||||
{
|
||||
$variables = [];
|
||||
$reflection = new ReflectionClass($this);
|
||||
|
||||
foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
|
||||
$name = $property->getName();
|
||||
$value = $property->getValue($this);
|
||||
|
||||
if (is_scalar($value)) {
|
||||
$variables['{' . $name . '}'] = (string) $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
public function setTemperature(float $temperature): self
|
||||
{
|
||||
$this->temperature = $temperature;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMaxTokens(int $maxTokens): self
|
||||
{
|
||||
$this->maxTokens = $maxTokens;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setGptCompleter(GptCompleter $completer): self
|
||||
{
|
||||
$this->completer = $completer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function useStreaming(): self
|
||||
{
|
||||
$this->useStreaming = true;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\AI\Prompts\Template;
|
||||
|
||||
use App\Models\Template;
|
||||
use App\Service\AI\Prompts\Prompt;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class GenerateTemplateMetadataPrompt extends Prompt
|
||||
{
|
||||
protected float $temperature = 0.81;
|
||||
|
||||
protected int $maxTokens = 3000;
|
||||
|
||||
/**
|
||||
* Available industries and types loaded from the database
|
||||
*/
|
||||
protected array $availableIndustries = [];
|
||||
protected array $availableTypes = [];
|
||||
|
||||
/**
|
||||
* The prompt template for generating template metadata
|
||||
*/
|
||||
public const PROMPT_TEMPLATE = <<<'EOD'
|
||||
I need to generate metadata for a form template about: "{templatePrompt}"
|
||||
|
||||
Please generate the following information for this template:
|
||||
|
||||
1. SHORT DESCRIPTION:
|
||||
Create a single-sentence description that is concise, clearly explains what the form is about, highlights the main purpose or benefit, and is suitable for OpnForm (a free-to-use form builder). Do not use quotation marks.
|
||||
|
||||
2. DETAILED DESCRIPTION:
|
||||
Create detailed HTML content with the following structure:
|
||||
- A paragraph explaining what the template is about
|
||||
- A section with heading (h2) explaining why and when to use such a form
|
||||
- A section with heading (h2) explaining who the target audience is and why it's beneficial
|
||||
- A section with heading (h2) explaining that OpnForm is the best tool to build this form
|
||||
Use only h2, p, ul, and li HTML tags. Each section (except for the first) MUST start with an h2 tag.
|
||||
|
||||
3. TITLE:
|
||||
Create a title that contains or ends with the word "template", is short and to the point, doesn't include quotes, is optimized for SEO, and clearly describes the purpose of the form.
|
||||
|
||||
4. INDUSTRY CLASSIFICATION:
|
||||
Classify the template into appropriate industries (minimum 1, maximum 3 but only if very relevant) from this list: {availableIndustries}
|
||||
Order them by relevance (most relevant first).
|
||||
|
||||
5. TYPE CLASSIFICATION:
|
||||
Classify the template into appropriate types (minimum 1, maximum 3 but only if very accurate) from this list: {availableTypes}
|
||||
Order them by relevance (most relevant first).
|
||||
|
||||
6. IMAGE SEARCH QUERY:
|
||||
Provide a concise search query for Unsplash that would be visually representative of the form's purpose.
|
||||
|
||||
7. Q&A CONTENT:
|
||||
Create 4 to 6 question and answer pairs covering:
|
||||
- The purpose and benefits of this form template
|
||||
- When and why to use this form
|
||||
- Who the target audience is
|
||||
- Why OpnForm is the best option to create this form
|
||||
|
||||
Reply with a valid JSON object containing all these elements.
|
||||
EOD;
|
||||
|
||||
/**
|
||||
* JSON schema for template metadata output
|
||||
*/
|
||||
protected ?array $jsonSchema = null;
|
||||
|
||||
public function __construct(
|
||||
public string $templatePrompt
|
||||
) {
|
||||
// Load available industries and types from the database
|
||||
$this->loadAvailableOptions();
|
||||
|
||||
parent::__construct();
|
||||
$this->buildJsonSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load available industries and types from the Template model
|
||||
*/
|
||||
protected function loadAvailableOptions(): void
|
||||
{
|
||||
$this->availableIndustries = Template::getAllIndustries()->pluck('slug')->toArray();
|
||||
$this->availableTypes = Template::getAllTypes()->pluck('slug')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically build the JSON schema with enums for industries and types
|
||||
*/
|
||||
protected function buildJsonSchema(): void
|
||||
{
|
||||
$this->jsonSchema = [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'short_description' => [
|
||||
'type' => 'string',
|
||||
'description' => 'A concise single-sentence description of the form template'
|
||||
],
|
||||
'detailed_description' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Detailed HTML content describing the form template'
|
||||
],
|
||||
'title' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The title of the form template'
|
||||
],
|
||||
'industries' => [
|
||||
'type' => 'array',
|
||||
'description' => 'List of industry slugs for the template',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'enum' => $this->availableIndustries
|
||||
]
|
||||
],
|
||||
'types' => [
|
||||
'type' => 'array',
|
||||
'description' => 'List of type slugs for the template',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'enum' => $this->availableTypes
|
||||
]
|
||||
],
|
||||
'image_search_query' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Search query for Unsplash to find a relevant image'
|
||||
],
|
||||
'qa_content' => [
|
||||
'type' => 'array',
|
||||
'description' => 'Q&A content for the template',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'question' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The question about the form template'
|
||||
],
|
||||
'answer' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The answer to the question'
|
||||
]
|
||||
],
|
||||
'required' => ['question', 'answer'],
|
||||
'additionalProperties' => false
|
||||
]
|
||||
]
|
||||
],
|
||||
'required' => [
|
||||
'short_description',
|
||||
'detailed_description',
|
||||
'title',
|
||||
'industries',
|
||||
'types',
|
||||
'image_search_query',
|
||||
'qa_content'
|
||||
],
|
||||
'additionalProperties' => false
|
||||
];
|
||||
}
|
||||
|
||||
protected function getSystemMessage(): ?string
|
||||
{
|
||||
return 'You are an assistant helping to generate comprehensive metadata for form templates. Create well-structured, informative content that explains the purpose, benefits, and target audience of the form template.';
|
||||
}
|
||||
|
||||
protected function getPromptTemplate(): string
|
||||
{
|
||||
return self::PROMPT_TEMPLATE;
|
||||
}
|
||||
|
||||
protected function buildPrompt(): string
|
||||
{
|
||||
$template = $this->getPromptTemplate();
|
||||
|
||||
$industriesString = implode(', ', $this->availableIndustries);
|
||||
$typesString = implode(', ', $this->availableTypes);
|
||||
|
||||
return Str::of($template)
|
||||
->replace('{templatePrompt}', $this->templatePrompt)
|
||||
->replace('{availableIndustries}', $industriesString)
|
||||
->replace('{availableTypes}', $typesString)
|
||||
->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the initialize method to ensure the options are loaded
|
||||
*/
|
||||
protected function initialize(): void
|
||||
{
|
||||
if (empty($this->availableIndustries) || empty($this->availableTypes)) {
|
||||
throw new \InvalidArgumentException('Failed to load available industries and types from the database');
|
||||
}
|
||||
|
||||
parent::initialize();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user