2023-03-14 11:01:36 +00:00
< ? php
namespace App\Console\Commands ;
use App\Models\Template ;
use App\Service\OpenAi\GptCompleter ;
use Illuminate\Console\Command ;
use Illuminate\Support\Facades\Http ;
use Illuminate\Support\Str ;
class GenerateTemplate extends Command
{
/**
* The name and signature of the console command .
*
* @ var string
*/
protected $signature = 'ai:make-form-template {prompt}' ;
/**
* The console command description .
*
* @ var string
*/
protected $description = 'Generates a new form template from a prompt' ;
2023-09-08 11:00:28 +00:00
const MAX_RELATED_TEMPLATES = 8 ;
2023-03-14 11:01:36 +00:00
const FORM_STRUCTURE_PROMPT = <<< EOD
2023-09-08 11:00:28 +00:00
You are an AI assistant for OpnForm , a form builder and your job is to build a form for our user .
Forms are represented as Json objects . Here ' s an example form :
2023-03-14 11:01:36 +00:00
`` ` json
{
2023-03-26 10:54:12 +00:00
" title " : " Contact Us " ,
2023-03-14 11:01:36 +00:00
" properties " : [
{
" help " : null ,
" name " : " What's your name? " ,
" type " : " text " ,
" hidden " : false ,
" required " : true ,
2023-09-08 11:00:28 +00:00
" placeholder " : " Steve Jobs "
2023-03-14 11:01:36 +00:00
},
{
2023-09-08 11:00:28 +00:00
" help " : " We will never share your email with anyone else. " ,
2023-03-14 11:01:36 +00:00
" name " : " Email " ,
" type " : " email " ,
" hidden " : false ,
" required " : true ,
2023-09-08 11:00:28 +00:00
" placeholder " : " steve@apple.com "
2023-03-14 11:01:36 +00:00
},
{
2023-09-08 11:00:28 +00:00
" help " : null ,
" name " : " How would you rate your overall experience? " ,
" type " : " select " ,
" hidden " : false ,
" select " : {
" options " : [
{ " name " : 1 , " value " : 1 },
{ " name " : 2 , " value " : 2 },
{ " name " : 3 , " value " : 3 },
{ " name " : 4 , " value " : 4 },
{ " name " : 5 , " value " : 5 }
]
},
" prefill " : 5 ,
" required " : true ,
" placeholder " : null
},
2023-03-14 11:01:36 +00:00
{
" help " : null ,
" name " : " Subject " ,
" type " : " text " ,
" hidden " : false ,
" required " : true ,
" placeholder " : null
},
{
" help " : null ,
" name " : " How can we help? " ,
" type " : " text " ,
" hidden " : false ,
" required " : true ,
" multi_lines " : true ,
" placeholder " : null ,
" generates_uuid " : false ,
" max_char_limit " : " 2000 " ,
" hide_field_name " : false ,
2023-09-08 11:00:28 +00:00
" show_char_limit " : false
2023-03-14 11:01:36 +00:00
},
{
2023-09-08 11:00:28 +00:00
" help " : " Upload any relevant files here. " ,
2023-03-14 11:01:36 +00:00
" name " : " Have any attachments? " ,
" type " : " files " ,
" hidden " : false ,
" placeholder " : null
}
],
" description " : " <p>Looking for a real person to speak to?</p><p>We're here for you! Just drop in your queries below and we'll connect with you as soon as we can.</p> " ,
" re_fillable " : false ,
" use_captcha " : false ,
" redirect_url " : null ,
" submitted_text " : " <p>Great, we've received your message. We'll get back to you as soon as we can :)</p> " ,
" uppercase_labels " : false ,
" submit_button_text " : " Submit " ,
" re_fill_button_text " : " Fill Again " ,
2023-09-08 11:00:28 +00:00
" color " : " #64748b "
2023-03-14 11:01:36 +00:00
}
`` `
2023-09-08 11:00:28 +00:00
The form properties can only have one of the following types : 'text' , 'number' , 'select' , 'multi_select' , 'date' , 'files' , 'checkbox' , 'url' , 'email' , 'phone_number' , 'signature' .
2023-03-14 11:01:36 +00:00
All form properties objects need to have the keys 'help' , 'name' , 'type' , 'hidden' , 'placeholder' , 'prefill' .
2023-09-08 11:00:28 +00:00
The placeholder property is optional ( can be " null " ) and is used to display a placeholder text in the input field .
The help property is optional ( can be " null " ) and is used to display extra information about the field .
2023-03-14 11:01:36 +00:00
For the type " select " and " multi_select " , the input object must have a key " select " ( or " multi_select " ) that ' s mapped to an object like this one :
`` ` json
{
" options " : [
2023-09-08 11:00:28 +00:00
{ " name " : 1 , " value " : 1 },
{ " name " : 2 , " value " : 2 },
{ " name " : 3 , " value " : 3 },
{ " name " : 4 , " value " : 4 }
2023-03-14 11:01:36 +00:00
]
}
`` `
2023-09-08 11:00:28 +00:00
For numerical rating inputs , use a " number " type input and set the property " is_rating " to " true " to turn it into a star rating input . Ex :
`` ` json
{
" name " : " How would you rate your overall experience? " ,
" type " : " number " ,
" is_rating " : true
}
`` `
2023-03-23 13:07:55 +00:00
If the form is too long , you can paginate it by adding a page break block in the list of properties :
`` ` json
{
" name " : " Page Break " ,
" next_btn_text " : " Next " ,
" previous_btn_text " : " Previous " ,
" type " : " nf-page-break " ,
}
`` `
2023-09-08 11:00:28 +00:00
If you need to add more context to the form , you can add text blocks :
`` ` json
{
" name " : " My Text " ,
" type " : " nf-text " ,
" content " : " <p>This is a text block.</p> "
}
`` `
Give me the valid JSON object only , representing the following form : " [REPLACE] "
Do not ask me for more information about required properties or types , only suggest me a form structure .
2023-03-14 11:01:36 +00:00
EOD ;
const FORM_DESCRIPTION_PROMPT = <<< EOD
2023-09-08 11:00:28 +00:00
You are an AI assistant for OpnForm , a form builder and your job is to help us build form templates for our users .
Give me some valid html code ( using only h2 , p , ul , li html tags ) for the following form template page : " [REPLACE] " .
The html code should have the following structure :
- A paragraph explaining what the template is about
- A paragraph explaining why and when to use such a form
- A paragraph explaining who is the target audience and why it ' s a great idea to build this form
- A paragraph explaining that OpnForm is the best tool to build this form . They can duplicate this template in a few seconds , and integrate with many other tools through our webhook or zapier integration .
Each paragraph ( except for the first one ) MUST start with with a h2 tag containing a title for this paragraph .
EOD ;
const FORM_SHORT_DESCRIPTION_PROMPT = <<< EOD
I own a form builder online named OpnForm . It ' s free to use .
Give me a 1 sentence description for the following form template page : " [REPLACE] " . It should be short and concise , but still explain what the form is about .
EOD ;
const FORM_INDUSTRY_PROMPT = <<< EOD
You are an AI assistant for OpnForm , a form builder and your job is to help us build form templates for our users .
I am creating a form template : " [REPLACE] " . You must assign the template to industries . Return a list of industries ( minimum 1 , maximum 3 but only if very relevant ) and order them by relevance ( most relevant first ) .
Here are the only industries you can choose from : [ INDUSTRIES ]
Do no make up any new type , only use the ones listed above .
Reply only with a valid JSON , being an array of string . Order assigned industries from the most relevant to the less relevant .
Ex : [ " banking_forms " , " customer_service_forms " ]
EOD ;
const FORM_TYPES_PROMPT = <<< EOD
You are an AI assistant for OpnForm , a form builder and your job is to help us build form templates for our users .
I am creating a form template : " [REPLACE] " . You must assign the template to one or more types . Return a list of types ( minimum 1 , maximum 3 but only if very accurate ) and order them by relevance ( most relevant first ) .
Here are the only types you can choose from : [ TYPES ]
Do no make up any new type , only use the ones listed above .
Reply only with a valid JSON , being an array of string . Order assigned types from the most relevant to the less relevant .
Ex : [ " consent_forms " , " award_forms " ]
2023-03-14 11:01:36 +00:00
EOD ;
const FORM_QAS_PROMPT = <<< EOD
2023-09-08 11:00:28 +00:00
Now give me 4 to 6 question and answers to put on the form template page . The questions should be about the reasons for this template ( when to use , why , target audience , goal etc . ) .
The questions should also explain why OpnForm is the best option to create this form ( open - source , free to use , integrations etc ) .
Reply only with a valid JSON , being an array of object containing the keys " question " and " answer " .
2023-03-14 11:01:36 +00:00
EOD ;
const FORM_TITLE_PROMPT = <<< EOD
2023-09-08 11:00:28 +00:00
Finally give me a title for the template . It must contain or end with " template " . It should be short and to the point , without any quotes .
2023-03-14 11:01:36 +00:00
EOD ;
const FORM_IMG_KEYWORDS_PROMPT = <<< EOD
2023-09-08 11:00:28 +00:00
I want to add an image to illustrate this form template page . Give me a relevant search query for unsplash . Reply only with a valid JSON like this :
2023-03-14 11:01:36 +00:00
`` ` json
{
" search_query " : " "
}
`` `
EOD ;
/**
* Execute the console command .
*
* @ return int
*/
public function handle ()
{
2023-09-08 11:00:28 +00:00
// Get form structure
2023-03-23 13:07:55 +00:00
$completer = ( new GptCompleter ( config ( 'services.openai.api_key' )))
2023-09-08 11:00:28 +00:00
-> setAiModel ( 'gpt-3.5-turbo-16k' )
-> useStreaming ()
-> setSystemMessage ( 'You are an assistant helping to generate forms.' );
2024-01-29 09:25:00 +00:00
$completer -> expectsJson () -> completeChat ([
2023-03-14 11:01:36 +00:00
[ " role " => " user " , " content " => Str :: of ( self :: FORM_STRUCTURE_PROMPT ) -> replace ( '[REPLACE]' , $this -> argument ( 'prompt' )) -> toString ()]
2023-09-08 11:00:28 +00:00
], 6000 );
2023-03-14 11:01:36 +00:00
$formData = $completer -> getArray ();
2024-01-29 09:25:00 +00:00
$completer -> doesNotExpectJson ();
2023-03-14 11:01:36 +00:00
$formDescriptionPrompt = Str :: of ( self :: FORM_DESCRIPTION_PROMPT ) -> replace ( '[REPLACE]' , $this -> argument ( 'prompt' )) -> toString ();
2023-09-08 11:00:28 +00:00
$formShortDescription = $completer -> completeChat ([
[ " role " => " user " , " content " => Str :: of ( self :: FORM_SHORT_DESCRIPTION_PROMPT ) -> replace ( '[REPLACE]' , $this -> argument ( 'prompt' )) -> toString ()]
]) -> getString ();
// If description is between quotes, remove quotes
$formShortDescription = Str :: of ( $formShortDescription ) -> replaceMatches ( '/^"(.*)"$/' , '$1' ) -> toString ();
// Get industry & types
2024-01-29 09:25:00 +00:00
$completer -> expectsJson ();
2023-09-08 11:00:28 +00:00
$industry = $this -> getIndustries ( $completer , $this -> argument ( 'prompt' ));
$types = $this -> getTypes ( $completer , $this -> argument ( 'prompt' ));
// Get Related Templates
$relatedTemplates = $this -> getRelatedTemplates ( $industry , $types );
// Now get description and QAs
2024-01-29 09:25:00 +00:00
$completer -> doesNotExpectJson ();
2023-03-14 11:01:36 +00:00
$formDescription = $completer -> completeChat ([
[ " role " => " user " , " content " => $formDescriptionPrompt ]
2023-09-08 11:00:28 +00:00
]) -> getHtml ();
2024-01-29 09:25:00 +00:00
$completer -> expectsJson ();
2023-09-08 11:00:28 +00:00
$formCoverKeywords = $completer -> completeChat ([
[ " role " => " user " , " content " => $formDescriptionPrompt ],
[ " role " => " assistant " , " content " => $formDescription ],
[ " role " => " user " , " content " => self :: FORM_IMG_KEYWORDS_PROMPT ]
]) -> getArray ();
$imageUrl = $this -> getImageCoverUrl ( $formCoverKeywords [ 'search_query' ]);
2023-03-14 11:01:36 +00:00
$formQAs = $completer -> completeChat ([
[ " role " => " user " , " content " => $formDescriptionPrompt ],
[ " role " => " assistant " , " content " => $formDescription ],
[ " role " => " user " , " content " => self :: FORM_QAS_PROMPT ]
]) -> getArray ();
2024-01-29 09:25:00 +00:00
$completer -> doesNotExpectJson ();
2023-03-14 11:01:36 +00:00
$formTitle = $completer -> completeChat ([
[ " role " => " user " , " content " => $formDescriptionPrompt ],
[ " role " => " assistant " , " content " => $formDescription ],
[ " role " => " user " , " content " => self :: FORM_TITLE_PROMPT ]
]) -> getString ();
2023-09-08 11:00:28 +00:00
$template = $this -> createFormTemplate (
$formData ,
$formTitle ,
$formDescription ,
$formShortDescription ,
$formQAs ,
$imageUrl ,
$industry ,
$types ,
$relatedTemplates
);
$this -> info ( '/form-templates/' . $template -> slug );
// Set reverse related Templates
$this -> setReverseRelatedTemplates ( $template );
2023-03-14 11:01:36 +00:00
return Command :: SUCCESS ;
}
/**
* Get an image cover URL for the template using unsplash API
*/
private function getImageCoverUrl ( $searchQuery ) : ? string
{
2023-09-08 11:00:28 +00:00
$url = 'https://api.unsplash.com/search/photos?query=' . urlencode ( $searchQuery ) . '&client_id=' . config ( 'services.unsplash.access_key' );
2023-03-14 11:01:36 +00:00
$response = Http :: get ( $url ) -> json ();
2023-09-08 11:00:28 +00:00
$photoIndex = rand ( 0 , max ( count ( $response [ 'results' ]) - 1 , 10 ));
if ( isset ( $response [ 'results' ][ $photoIndex ][ 'urls' ][ 'regular' ])) {
return Str :: of ( $response [ 'results' ][ $photoIndex ][ 'urls' ][ 'regular' ]) -> replace ( 'w=1080' , 'w=600' ) -> toString ();
2023-03-14 11:01:36 +00:00
}
return null ;
}
2023-09-08 11:00:28 +00:00
private function getIndustries ( GptCompleter $completer , string $formPrompt ) : array
{
$industriesString = Template :: getAllIndustries () -> pluck ( 'slug' ) -> join ( ', ' );
return $completer -> completeChat ([
[ " role " => " user " , " content " => Str :: of ( self :: FORM_INDUSTRY_PROMPT )
-> replace ( '[REPLACE]' , $formPrompt )
-> replace ( '[INDUSTRIES]' , $industriesString )
-> toString ()]
]) -> getArray ();
}
private function getTypes ( GptCompleter $completer , string $formPrompt ) : array
{
$typesString = Template :: getAllTypes () -> pluck ( 'slug' ) -> join ( ', ' );
return $completer -> completeChat ([
[ " role " => " user " , " content " => Str :: of ( self :: FORM_TYPES_PROMPT )
-> replace ( '[REPLACE]' , $formPrompt )
-> replace ( '[TYPES]' , $typesString )
-> toString ()]
]) -> getArray ();
}
private function getRelatedTemplates ( array $industries , array $types ) : array
2023-03-14 11:01:36 +00:00
{
2023-09-08 11:00:28 +00:00
$templateScore = [];
Template :: chunk ( 100 , function ( $otherTemplates ) use ( $industries , $types , & $templateScore ) {
foreach ( $otherTemplates as $otherTemplate ) {
$industryOverlap = count ( array_intersect ( $industries ? ? [], $otherTemplate -> industry ? ? []));
$typeOverlap = count ( array_intersect ( $types ? ? [], $otherTemplate -> types ? ? []));
$score = $industryOverlap + $typeOverlap ;
if ( $score > 1 ) {
$templateScore [ $otherTemplate -> slug ] = $score ;
}
}
});
arsort ( $templateScore ); // Sort by Score
return array_slice ( array_keys ( $templateScore ), 0 , self :: MAX_RELATED_TEMPLATES );
}
private function createFormTemplate (
array $formData ,
string $formTitle ,
string $formDescription ,
string $formShortDescription ,
array $formQAs ,
? string $imageUrl ,
array $industry ,
array $types ,
array $relatedTemplates
)
{
// Add property uuids, improve form with options
2023-03-14 11:01:36 +00:00
foreach ( $formData [ 'properties' ] as & $property ) {
2023-09-08 11:00:28 +00:00
$property [ 'id' ] = Str :: uuid () -> toString (); // Column ID
// Fix ratings
if ( $property [ 'type' ] == 'number' && ( $property [ 'is_rating' ] ? ? false )) {
$property [ 'rating_max_value' ] = 5 ;
}
if (( $property [ 'type' ] == 'select' && count ( $property [ 'select' ][ 'options' ]) <= 4 )
|| ( $property [ 'type' ] == 'multi_select' && count ( $property [ 'multi_select' ][ 'options' ]) <= 4 )) {
$property [ 'without_dropdown' ] = true ;
}
2023-03-14 11:01:36 +00:00
}
2023-03-23 13:07:55 +00:00
// Clean data
$formTitle = Str :: of ( $formTitle ) -> replace ( '"' , '' ) -> toString ();
2023-03-14 11:01:36 +00:00
return Template :: create ([
'name' => $formTitle ,
'description' => $formDescription ,
2023-09-08 11:00:28 +00:00
'short_description' => $formShortDescription ,
2023-03-14 11:01:36 +00:00
'questions' => $formQAs ,
'structure' => $formData ,
'image_url' => $imageUrl ,
2023-09-08 11:00:28 +00:00
'publicly_listed' => true ,
'industries' => $industry ,
'types' => $types ,
'related_templates' => $relatedTemplates
2023-03-14 11:01:36 +00:00
]);
}
2023-09-08 11:00:28 +00:00
private function setReverseRelatedTemplates ( Template $newTemplate )
{
if ( ! $newTemplate || count ( $newTemplate -> related_templates ) === 0 ) return ;
$templates = Template :: whereIn ( 'slug' , $newTemplate -> related_templates ) -> get ();
foreach ( $templates as $template ) {
if ( count ( $template -> related_templates ) < self :: MAX_RELATED_TEMPLATES ) {
$template -> update ([ 'related_templates' => array_merge ( $template -> related_templates , [ $newTemplate -> slug ])]);
}
}
}
2023-03-14 11:01:36 +00:00
}