Skip to content
104 changes: 81 additions & 23 deletions src/AI.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,105 @@

namespace Backstage\AI;

use Backstage\AI\Prism\SystemMessages\Forms\Components\BaseInstructions;
use Backstage\AI\Prism\SystemMessages\Forms\Components\DateTimePicker;
use Backstage\AI\Prism\SystemMessages\Forms\Components\MarkdownEditor;
use Backstage\AI\Prism\SystemMessages\Forms\Components\RichEditor;
use Backstage\AI\Prism\SystemMessages\Forms\Components\Select;
use Backstage\AI\Prism\SystemMessages\Forms\Components\TextInput;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Field;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Prism\Prism\Enums\Provider;
use Prism\Prism\Exceptions\PrismException;
use Prism\Prism\Prism;

class AI
{
public static function registerMacro(): void
public static function registerFormMacro(): void
{
Field::macro('withAI', function ($prompt = null) {
return $this->hintAction(
function (Set $set, Field $component) use ($prompt) {
Forms\Components\Field::macro('withAI', function ($prompt = null, $hint = true) {
return $this->{$hint ? 'hintAction' : 'suffixAction'}(
function (Set $set, Forms\Components\Field $component) use ($prompt, $hint) {
return Action::make('ai')
->visible(fn($operation) => $operation !== 'view')
->icon(config('backstage.ai.action.icon'))
->label(config('backstage.ai.action.label'))
->modalHeading(config('backstage.ai.action.modal.heading'))
->modalSubmitActionLabel('Generate')
->modalSubmitActionLabel(__('Generate'))
->extraAttributes($hint ? [
'x-show' => 'focused || hover',
'x-cloak' => '',
] : [])
->form([
Select::make('model')
->label('Model')
Forms\Components\Select::make('model')
->label(__('AI Model'))
->options(
collect(config('backstage.ai.providers'))
->mapWithKeys(fn ($provider, $model) => [
->mapWithKeys(fn(Provider $provider, $model) => [
$model => $model . ' (' . $provider->name . ')',
]),
)
->default(key(config('backstage.ai.providers'))),

Textarea::make('prompt')
->label('Prompt')
Forms\Components\Textarea::make('prompt')
->label(__('Instructions'))
->autosize()
->default($prompt),

Section::make('configuration')
->heading('Configuration')
Forms\Components\Section::make('configuration')
->heading(__('Configuration'))
->schema([
TextInput::make('temperature')
Forms\Components\TextInput::make('temperature')
->numeric()
->label('Temperature')
->label(__('AI Temperature'))
->default(config('backstage.ai.configuration.temperature'))
->helperText('The higher the temperature, the more creative the text')
->maxValue(1)
->minValue(0)
->step('0.1'),
TextInput::make('max_tokens')

Forms\Components\TextInput::make('max_tokens')
->numeric()
->label('Max tokens')
->label(__('Max Tokens'))
->default(config('backstage.ai.configuration.max_tokens'))
->helperText('The maximum number of tokens to generate')
->step('10')
->minValue(0)
->suffixAction(
Action::make('increase')
->icon('heroicon-o-plus')
->action(fn (Set $set, Get $get) => $set('max_tokens', $get('max_tokens') + 100)),
->action(fn(Set $set, Get $get) => $set('max_tokens', $get('max_tokens') + 100)),
),
])
->columns(2)
->collapsed()
->collapsible(),
])
->action(function ($data) use ($component, $set) {
$systemPrompts = AI::getSystemPrompts($component);

try {
$response = Prism::text()
->using(config('backstage.ai.providers.' . $data['model']), $data['model'])
->withPrompt($data['prompt'])
->withSystemPrompts($systemPrompts)
->asText();

$fieldState = $component->getState();

if ($fieldState === $response->text) {
Notification::make()
->title(__('AI generated text is the same as the current state'))
->body(__('Please be more specific with your prompt or try again.'))
->danger()
->send();

return;
}

$set($component->getName(), $response->text);
} catch (PrismException $exception) {
Notification::make()
Expand All @@ -87,7 +111,41 @@ function (Set $set, Field $component) use ($prompt) {
}
});
}
);
)
->extraFieldWrapperAttributes([
'x-data' => '{focused: false, hover: false}',
'x-on:mouseover' => 'hover = true',
'x-on:mouseout' => 'hover = false',
])->extraInputAttributes([
'x-on:focus' => 'focused = true',
'x-on:blur' => 'focused = false',
]);
});
}

/**
* Checking the type of the component and returning the specific instructions for each type.
* Allowed types are:
*
* @var Forms\Components\RichEditor
* @var Forms\Components\MarkdownEditor
* @var Forms\Components\DateTimePicker
* @var Forms\Components\TextInput
* @var Forms\Components\Select
*/
public static function getSystemPrompts(Forms\Components\Field $component): array
{
$baseInstructions = BaseInstructions::ask($component);

$componentInstructions = match (true) {
$component instanceof Forms\Components\RichEditor => RichEditor::ask($component),
$component instanceof Forms\Components\MarkdownEditor => MarkdownEditor::ask($component),
$component instanceof Forms\Components\DateTimePicker => DateTimePicker::ask($component),
$component instanceof Forms\Components\TextInput => TextInput::ask($component),
$component instanceof Forms\Components\Select => Select::ask($component),
default => [],
};

return array_merge($baseInstructions, $componentInstructions);
}
}
2 changes: 1 addition & 1 deletion src/AIServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function configurePackage(Package $package): void

public function packageBooted(): void
{
AI::registerMacro();
AI::registerFormMacro();
}

/**
Expand Down
19 changes: 19 additions & 0 deletions src/Prism/SystemMessages/Forms/Components/BaseInstructions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Backstage\AI\Prism\SystemMessages\Forms\Components;

use Prism\Prism\ValueObjects\Messages\SystemMessage;

class BaseInstructions
{
public static function ask(\Filament\Forms\Components\Field $component): array
{
$baseInstructions = [
new SystemMessage('You are a helpful assistant. That\'s inside a Filament form field. This is the state of the field: ' . json_encode($component->getState())),
new SystemMessage('You must only return the value of the field.'),
new SystemMessage('No yapping, no explanations, no extra text.'),
];

return $baseInstructions;
}
}
20 changes: 20 additions & 0 deletions src/Prism/SystemMessages/Forms/Components/DateTimePicker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Backstage\AI\Prism\SystemMessages\Forms\Components;

use Prism\Prism\ValueObjects\Messages\SystemMessage;

class DateTimePicker
{
public static function ask(\Filament\Forms\Components\DateTimePicker $component): array
{
$format = $component->getFormat();

$instructions = [
new SystemMessage('You must return a date as output.'),
new SystemMessage('The date format is: ' . $format),
];

return $instructions;
}
}
18 changes: 18 additions & 0 deletions src/Prism/SystemMessages/Forms/Components/MarkdownEditor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Backstage\AI\Prism\SystemMessages\Forms\Components;

use Prism\Prism\ValueObjects\Messages\SystemMessage;

class MarkdownEditor
{
public static function ask(\Filament\Forms\Components\MarkdownEditor $component): array
{
$instructions = [
new SystemMessage('You must return Markdown as output. This is the field that will implement the Markdown (state) that you will return: https://filamentphp.com/docs/3.x/forms/fields/markdown-editor.'),
new SystemMessage("Don\'t return the markdown with markdown syntax like opening the markdown and closing it. For example: ```markdown... ```"),
];

return $instructions;
}
}
19 changes: 19 additions & 0 deletions src/Prism/SystemMessages/Forms/Components/RichEditor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Backstage\AI\Prism\SystemMessages\Forms\Components;

use Prism\Prism\ValueObjects\Messages\SystemMessage;

class RichEditor
{
public static function ask(\Filament\Forms\Components\RichEditor $component): array
{
$instructions = [
new SystemMessage('You must return pure HTML as output.'),
new SystemMessage('This is the field that will implement the HTML (state) that you will return: https://filamentphp.com/docs/3.x/forms/fields/rich-editor.'),
new SystemMessage('Do not return any <h1> tags.'),
];

return $instructions;
}
}
19 changes: 19 additions & 0 deletions src/Prism/SystemMessages/Forms/Components/Select.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Backstage\AI\Prism\SystemMessages\Forms\Components;

use Prism\Prism\ValueObjects\Messages\SystemMessage;

class Select
{
public static function ask(\Filament\Forms\Components\Select $component): array
{
$instructions = [
new SystemMessage('You must return a value from the select as output.'),
new SystemMessage('The options are: ' . json_encode($component->getOptions())),
new SystemMessage('You must return the key of the option as output.'),
];

return $instructions;
}
}
27 changes: 27 additions & 0 deletions src/Prism/SystemMessages/Forms/Components/TextInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Backstage\AI\Prism\SystemMessages\Forms\Components;

use Prism\Prism\ValueObjects\Messages\SystemMessage;

class TextInput
{
public static function ask(\Filament\Forms\Components\TextInput $component): array
{
$instructions = [];

if ($component->isPassword()) {
$instructions = [
new SystemMessage('You must return a password as output.'),
];
}

if ($component->isEmail()) {
$instructions = [
new SystemMessage('You must return an email as output.'),
];
}

return $instructions;
}
}
Loading