Improve Templates (#183)

* Improve Templates

* Fix test case

* Update AI GenerateTemplate

* update openai client and GPT completer

* composer.lock

* Update types and list json with script

* Template changes

* fix on draft template

* Finish opnform templates

---------

Co-authored-by: Forms Dev <chirag+new@notionforms.io>
Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala
2023-09-08 16:30:28 +05:30
committed by GitHub
parent d93eca7410
commit 8e47b49e9a
36 changed files with 3130 additions and 1381 deletions

View File

@@ -24,8 +24,12 @@ class GenerateTemplate extends Command
*/
protected $description = 'Generates a new form template from a prompt';
const MAX_RELATED_TEMPLATES = 8;
const FORM_STRUCTURE_PROMPT = <<<EOD
I created a form builder. Forms are represented as Json objects. Here's an example form:
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:
```json
{
"title": "Contact Us",
@@ -35,50 +39,40 @@ class GenerateTemplate extends Command
"name": "What's your name?",
"type": "text",
"hidden": false,
"prefill": null,
"required": true,
"placeholder": null
"placeholder": "Steve Jobs"
},
{
"help": null,
"help": "We will never share your email with anyone else.",
"name": "Email",
"type": "email",
"hidden": false,
"prefill": null,
"required": true,
"placeholder": null
"placeholder": "steve@apple.com"
},
{
"help": null,
"name": "How would you rate your overall experience?",
"type": "select",
"hidden": false,
"select": {
"options": [
{
"id": "Below Average",
"name": "Below Average"
},
{
"id": "Average",
"name": "Average"
},
{
"id": "Above Average",
"name": "Above Average"
}
]
},
"prefill": null,
"required": true,
"placeholder": null,
},
"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
},
{
"help": null,
"name": "Subject",
"type": "text",
"hidden": false,
"prefill": null,
"required": true,
"placeholder": null
},
@@ -87,22 +81,19 @@ class GenerateTemplate extends Command
"name": "How can we help?",
"type": "text",
"hidden": false,
"prefill": null,
"required": true,
"multi_lines": true,
"placeholder": null,
"generates_uuid": false,
"max_char_limit": "2000",
"hide_field_name": false,
"show_char_limit": false,
"generates_auto_increment_id": false
"show_char_limit": false
},
{
"help": null,
"help": "Upload any relevant files here.",
"name": "Have any attachments?",
"type": "files",
"hidden": false,
"prefill": null,
"placeholder": null
}
],
@@ -114,23 +105,34 @@ class GenerateTemplate extends Command
"uppercase_labels": false,
"submit_button_text": "Submit",
"re_fill_button_text": "Fill Again",
"color": "#3B82F6"
"color": "#64748b"
}
```
The form properties can have one of the following types: 'text', 'number', 'select', 'multi_select', 'date', 'files', 'checkbox', 'url', 'email', 'phone_number'.
The form properties can only have one of the following types: 'text', 'number', 'select', 'multi_select', 'date', 'files', 'checkbox', 'url', 'email', 'phone_number', 'signature'.
All form properties objects need to have the keys 'help', 'name', 'type', 'hidden', 'placeholder', 'prefill'.
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.
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": [
{"id":"Option 1","name":"Option 1"},
{"id":"Pption 2","name":"Option 2"}
{"name": 1, "value": 1},
{"name": 2, "value": 2},
{"name": 3, "value": 3},
{"name": 4, "value": 4}
]
}
```
For the type "number" you can set the property "is_rating" to "true" to turn it into a star rating input.
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
}
```
If the form is too long, you can paginate it by adding a page break block in the list of properties:
```json
@@ -142,24 +144,70 @@ class GenerateTemplate extends Command
}
```
Give me the JSON code only, for the following form: "[REPLACE]"
Do not ask me for more information about required properties or types, suggest me a form structure instead.
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.
EOD;
const FORM_DESCRIPTION_PROMPT = <<<EOD
I own a form builder online named OpnForm. It's free to use. Give me a description for a template page for the following form: [REPLACE]. Explain what the form is about, and that it takes seconds to duplicate the template to create your own version it and to start getting some submissions.
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"]
EOD;
const FORM_QAS_PROMPT = <<<EOD
Now give me 3 to 5 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.) and OpnForm's usage. Reply only with a valid JSON, being an array of object containing the keys "question" and "answer".
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".
EOD;
const FORM_TITLE_PROMPT = <<<EOD
Finally give me a title for the template. It should be short and to the point, without any quotes.
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.
EOD;
const FORM_IMG_KEYWORDS_PROMPT = <<<EOD
I want to add an image to illustrate this form template page. Give me a releveant search query for unsplash. Reply only with a valid JSON like this:
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:
```json
{
"search_query": ""
@@ -174,19 +222,42 @@ class GenerateTemplate extends Command
*/
public function handle()
{
// Get form structture
// Get form structure
$completer = (new GptCompleter(config('services.openai.api_key')))
->setSystemMessage('You are a robot helping to generate forms.');
->setAiModel('gpt-3.5-turbo-16k')
->useStreaming()
->setSystemMessage('You are an assistant helping to generate forms.');
$completer->completeChat([
["role" => "user", "content" => Str::of(self::FORM_STRUCTURE_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString()]
], 3000);
], 6000);
$formData = $completer->getArray();
// Now get description and QAs
$formDescriptionPrompt = Str::of(self::FORM_DESCRIPTION_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString();
$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
$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
$formDescription = $completer->completeChat([
["role" => "user", "content" => $formDescriptionPrompt]
])->getString();
])->getHtml();
$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']);
$formQAs = $completer->completeChat([
["role" => "user", "content" => $formDescriptionPrompt],
["role" => "assistant", "content" => $formDescription],
@@ -198,16 +269,22 @@ class GenerateTemplate extends Command
["role" => "user", "content" => self::FORM_TITLE_PROMPT]
])->getString();
// Finally get keyworks for image cover
$formCoverKeyworks = $completer->completeChat([
["role" => "user", "content" => $formDescriptionPrompt],
["role" => "assistant", "content" => $formDescription],
["role" => "user", "content" => self::FORM_IMG_KEYWORDS_PROMPT]
])->getArray();
$imageUrl = $this->getImageCoverUrl($formCoverKeyworks['search_query']);
$template = $this->createFormTemplate($formData, $formTitle, $formDescription, $formQAs, $imageUrl);
$this->info('/templates/' . $template->slug);
$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);
return Command::SUCCESS;
}
@@ -217,19 +294,81 @@ class GenerateTemplate extends Command
*/
private function getImageCoverUrl($searchQuery): ?string
{
$url = 'https://api.unsplash.com/search/photos?query=' . urlencode($searchQuery) . '&client_id=' . config('services.unslash.access_key');
$url = 'https://api.unsplash.com/search/photos?query=' . urlencode($searchQuery) . '&client_id=' . config('services.unsplash.access_key');
$response = Http::get($url)->json();
if (isset($response['results'][0]['urls']['regular'])) {
return $response['results'][0]['urls']['regular'];
$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();
}
return null;
}
private function createFormTemplate(array $formData, string $formTitle, string $formDescription, array $formQAs, ?string $imageUrl)
private function getIndustries(GptCompleter $completer, string $formPrompt): array
{
// Add property uuids
$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
{
$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
foreach ($formData['properties'] as &$property) {
$property['id'] = Str::uuid()->toString();
$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;
}
}
// Clean data
@@ -238,9 +377,26 @@ class GenerateTemplate extends Command
return Template::create([
'name' => $formTitle,
'description' => $formDescription,
'short_description' => $formShortDescription,
'questions' => $formQAs,
'structure' => $formData,
'image_url' => $imageUrl,
'publicly_listed' => true,
'industries' => $industry,
'types' => $types,
'related_templates' => $relatedTemplates
]);
}
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])]);
}
}
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Http\Controllers\Forms;
use App\Console\Commands\GenerateTemplate;
use App\Http\Controllers\Controller;
use App\Http\Requests\AiGenerateFormRequest;
use App\Models\Forms\AI\AiFormCompletion;

View File

@@ -42,9 +42,9 @@ class SitemapController extends Controller
private function addTemplatesUrls(Sitemap $sitemap)
{
Template::chunk(100, function ($templates) use ($sitemap) {
Template::where('publicly_listed', true)->chunk(100, function ($templates) use ($sitemap) {
foreach ($templates as $template) {
$sitemap->add($this->createUrl('/templates/' . $template->slug, 0.7));
$sitemap->add($this->createUrl('/form-templates/' . $template->slug, 0.8));
}
});
}

View File

@@ -3,19 +3,28 @@
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\Templates\CreateTemplateRequest;
use App\Http\Resources\TemplateResource;
use Illuminate\Http\Request;
use App\Http\Requests\Templates\FormTemplateRequest;
use App\Http\Resources\FormTemplateResource;
use App\Models\Template;
use Illuminate\Http\Request;
class TemplateController extends Controller
{
public function index()
public function index(Request $request)
{
return TemplateResource::collection(Template::all());
$limit = null;
if ($request->offsetExists('limit') && $request->get('limit') > 0) {
$limit = (int) $request->get('limit');
}
return FormTemplateResource::collection(
Template::where('publicly_listed', true)
->orderByDesc('created_at')
->limit($limit)
->get()
);
}
public function create(CreateTemplateRequest $request)
public function create(FormTemplateRequest $request)
{
$this->authorize('create', Template::class);
@@ -24,8 +33,41 @@ class TemplateController extends Controller
$template->save();
return $this->success([
'message' => 'Template created.',
'message' => 'Template was created.',
'template_id' => $template->id
]);
}
public function update(FormTemplateRequest $request, string $id)
{
$template = Template::findOrFail($id);
$this->authorize('update', $template);
$template->update($request->all());
return $this->success([
'message' => 'Template was updated.',
'template_id' => $template->id,
'data' => new FormTemplateResource($template)
]);
}
public function destroy($id)
{
$template = Template::findOrFail($id);
$this->authorize('delete', $template);
$template->delete();
return $this->success([
'message' => 'Template was deleted.',
]);
}
public function show(string $slug)
{
return new FormTemplateResource(
Template::whereSlug($slug)->firstOrFail()
);
}
}

View File

@@ -5,7 +5,7 @@ namespace App\Http\Requests\Templates;
use App\Models\Template;
use Illuminate\Foundation\Http\FormRequest;
class CreateTemplateRequest extends FormRequest
class FormTemplateRequest extends FormRequest
{
const IGNORED_KEYS = [
'id',
@@ -48,12 +48,21 @@ class CreateTemplateRequest extends FormRequest
*/
public function rules()
{
$slugRule = '';
if($this->id){
$slugRule = ','.$this->id;
}
return [
'form' => 'required|array',
'publicly_listed' => 'boolean',
'name' => 'required|string|max:60',
'slug' => 'required|string|unique:templates',
'slug' => 'required|string|alpha_dash|unique:templates,slug'.$slugRule,
'short_description' => 'required|string|max:1000',
'description' => 'required|string|max:2000',
'image_url' => 'required|string',
'types' => 'nullable|array',
'industries' => 'nullable|array',
'related_templates' => 'nullable|array',
'questions' => 'array',
];
}
@@ -66,12 +75,18 @@ class CreateTemplateRequest extends FormRequest
unset($structure[$key]);
}
}
return new Template([
'publicly_listed' => $this->publicly_listed,
'name' => $this->name,
'slug' => $this->slug,
'short_description' => $this->short_description,
'description' => $this->description,
'image_url' => $this->image_url,
'structure' => $structure,
'types' => $this->types ?? [],
'industries' => $this->industries ?? [],
'related_templates' => $this->related_templates ?? [],
'questions' => $this->questions ?? []
]);
}

View File

@@ -2,10 +2,9 @@
namespace App\Http\Resources;
use App\Http\Requests\Templates\CreateTemplateRequest;
use Illuminate\Http\Resources\Json\JsonResource;
class TemplateResource extends JsonResource
class FormTemplateResource extends JsonResource
{
/**
* Transform the resource into an array.
@@ -15,10 +14,8 @@ class TemplateResource extends JsonResource
*/
public function toArray($request)
{
$data = parent::toArray($request);
foreach (CreateTemplateRequest::IGNORED_KEYS as $key) {
unset($data[$key]);
}
return $data;
return array_merge(parent::toArray($request), [
'is_new' => $this->created_at->isAfter(now()->subDays(7))
]);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
use Stevebauman\Purify\Facades\Purify;
@@ -16,14 +17,28 @@ class Template extends Model
'name',
'slug',
'description',
'short_description',
'image_url',
'structure',
'questions',
'publicly_listed',
'industries',
'types',
'related_templates'
];
protected $casts = [
'structure' => 'array',
'questions' => 'array',
'industries' => 'array',
'types' => 'array',
'related_templates' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected $attributes = [
'publicly_listed' => false,
];
public function setDescriptionAttribute($value)
@@ -32,6 +47,11 @@ class Template extends Model
$this->attributes['description'] = Purify::clean($value);
}
public function scopePubliclyListed($query)
{
return $this->where('publicly_listed', true);
}
/**
* Config/options
*/
@@ -42,4 +62,40 @@ class Template extends Model
->generateSlugsFrom('name')
->saveSlugsTo('slug');
}
public function getTypes(): Collection
{
return self::getAllTypes()->filter(function ($type) {
return in_array($type['slug'], $this->types);
});
}
public function getIndustries(): Collection
{
return self::getAllIndustries()->filter(function ($type) {
return in_array($type['slug'], $this->industries);
});
}
public static function getAllTypes(): Collection
{
return collect(
array_values(
json_decode(
file_get_contents(resource_path('data/forms/templates/types.json')),
true)
)
)->values();
}
public static function getAllIndustries(): Collection
{
return collect(
array_values(
json_decode(
file_get_contents(resource_path('data/forms/templates/industries.json')),
true)
)
)->values();
}
}

View File

@@ -18,6 +18,30 @@ class TemplatePolicy
*/
public function create(User $user)
{
return $user->template_editor;
return $user->admin || $user->template_editor;
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $user
* @param \App\Models\Template $template
* @return mixed
*/
public function update(User $user, Template $template)
{
return $user->admin || $user->template_editor;
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $user
* @param \App\Models\Template $template
* @return mixed
*/
public function delete(User $user, Template $template)
{
return $user->admin || $user->template_editor;
}
}

View File

@@ -14,7 +14,7 @@ use OpenAI\Exceptions\ErrorException;
*/
class GptCompleter
{
const AI_MODEL = 'gpt-3.5-turbo';
const AI_MODEL = 'gpt-4';
protected Client $openAi;
protected mixed $result;
@@ -22,19 +22,32 @@ class GptCompleter
protected ?string $systemMessage;
protected int $tokenUsed = 0;
protected bool $useStreaming = false;
public function __construct(string $apiKey, protected int $retries = 2)
public function __construct(string $apiKey, protected int $retries = 2, protected string $model = self::AI_MODEL)
{
$this->openAi = \OpenAI::client($apiKey);
}
public function setAiModel(string $model)
{
$this->model = $model;
return $this;
}
public function setSystemMessage(string $systemMessage): self
{
$this->systemMessage = $systemMessage;
return $this;
}
public function completeChat(array $messages, int $maxTokens = 512, float $temperature = 0.81): self
public function useStreaming(): self
{
$this->useStreaming = true;
return $this;
}
public function completeChat(array $messages, int $maxTokens = 4096, float $temperature = 0.81): self
{
$this->computeChatCompletion($messages, $maxTokens, $temperature)
->queryCompletion();
@@ -56,30 +69,40 @@ class GptCompleter
public function getArray(): array
{
$payload = Str::of($this->result)->trim();
if ($payload->contains('```json')) {
$payload = $payload->after('```json')->before('```');
} else if ($payload->contains('```')) {
$payload = $payload->after('```')->before('```');
}
$payload = $payload->toString();
$exception = null;
for ($i = 0; $i < $this->retries; $i++) {
$payload = Str::of($this->result)->trim();
if ($payload->contains('```json')) {
$payload = $payload->after('```json')->before('```');
} else if ($payload->contains('```')) {
$payload = $payload->after('```')->before('```');
}
$payload = $payload->toString();
$exception = null;
try {
$payload = (new JsonFixer)->fix($payload);
return json_decode($payload, true);
$newPayload = (new JsonFixer)->fix($payload);
return json_decode($newPayload, true);
} catch (\Aws\Exception\InvalidJsonException $e) {
$exception = $e;
Log::warning("Invalid JSON, retrying:");
Log::warning($payload);
Log::warning(json_encode($this->completionInput));
$this->queryCompletion();
}
}
throw $exception;
}
public function getHtml(): string
{
$payload = Str::of($this->result)->trim();
if ($payload->contains('```html')) {
$payload = $payload->after('```html')->before('```');
} else if ($payload->contains('```')) {
$payload = $payload->after('```')->before('```');
}
return $payload->toString();
}
public function getString(): string
{
return trim($this->result);
@@ -90,7 +113,7 @@ class GptCompleter
return $this->tokenUsed;
}
protected function computeChatCompletion(array $messages, int $maxTokens = 512, float $temperature = 0.81): self
protected function computeChatCompletion(array $messages, int $maxTokens = 4096, float $temperature = 0.81): self
{
if (isset($this->systemMessage) && $messages[0]['role'] !== 'system') {
$messages = array_merge([[
@@ -100,7 +123,7 @@ class GptCompleter
}
$completionInput = [
'model' => self::AI_MODEL,
'model' => $this->model,
'messages' => $messages,
'max_tokens' => $maxTokens,
'temperature' => $temperature
@@ -110,7 +133,12 @@ class GptCompleter
return $this;
}
protected function queryCompletion(): self {
protected function queryCompletion(): self
{
if ($this->useStreaming) {
return $this->queryStreamedCompletion();
}
try {
Log::debug("Open AI query: " . json_encode($this->completionInput));
$response = $this->openAi->chat()->create($this->completionInput);
@@ -123,4 +151,19 @@ class GptCompleter
$this->result = $response->choices[0]->message->content;
return $this;
}
protected function queryStreamedCompletion(): self
{
Log::debug("Open AI query: " . json_encode($this->completionInput));
$this->result = '';
$response = $this->openAi->chat()->createStreamed($this->completionInput);
foreach ($response as $chunk) {
$choice = $chunk->choices[0];
if (is_null($choice->delta->role)) {
$this->result .= $choice->delta->content;
}
}
return $this;
}
}

View File

@@ -95,6 +95,7 @@ class JsonFixer
*/
public function fix($json)
{
$json = preg_replace('/(?<!\\\\)(?:\\\\{2})*\p{C}+/u', '', $json);
list($head, $json, $tail) = $this->trim($json);
if (empty($json) || $this->isValid($json)) {
@@ -124,7 +125,7 @@ class JsonFixer
protected function isValid($json)
{
\json_decode($json);
\json_decode($json,true,512,JSON_INVALID_UTF8_SUBSTITUTE);
return \JSON_ERROR_NONE === \json_last_error();
}
@@ -265,6 +266,10 @@ class JsonFixer
return $json;
}
\Log::debug('Broken json received: ', [
'json' => $json
]);
throw new InvalidJsonException(
\sprintf('Could not fix JSON (tried padding `%s`)', \substr($tmpJson, $length), $json)
);

View File

@@ -34,8 +34,8 @@ class SeoMetaResolver
'privacy_policy' => '/privacy-policy',
'terms_conditions' => '/terms-conditions',
'integrations' => '/integrations',
'templates' => '/templates',
'template_show' => '/templates/{slug}',
'templates' => '/form-templates',
'templates_show' => '/form-templates/{slug}',
];
/**
@@ -62,7 +62,7 @@ class SeoMetaResolver
],
'templates' => [
'title' => 'Templates',
'description' => 'Free templates to quickly create beautiful forms for free!'
'description' => 'Our collection of beautiful templates to create your own forms!'
],
];
@@ -182,13 +182,13 @@ class SeoMetaResolver
return $meta;
}
private function getTemplateShowMeta(): array
private function getTemplatesShowMeta(): array
{
$template = Template::whereSlug($this->patternData['slug'])->firstOrFail();
return [
'title' => $template->name . $this->titleSuffix(),
'description' => Str::of($template->description)->limit(160),
'description' => Str::of($template->short_description)->limit(140) . ' | Customize any template and create your own form in minutes.',
'image' => $template->image_url
];
}