baconfy/prompt

Manage AI prompts in Laravel with Markdown + YAML front matter, multiple drivers (file/database), and Blade rendering.

Maintainers

Package info

github.com/baconfy/prompt

pkg:composer/baconfy/prompt

Statistics

Installs: 31

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.1 2026-05-15 23:58 UTC

This package is auto-updated.

Last update: 2026-05-16 00:06:38 UTC


README

Prompt

Tests Latest Version License Total Downloads PHP Version

Manage AI prompts in Laravel as Markdown files (with optional YAML front matter) or database records, rendered through Blade.

Why

LLM prompts are configuration. They drift across the codebase, get duplicated, and end up hardcoded as long strings inside services. This package treats prompts as first-class assets:

  • One file or DB record per prompt
  • YAML front matter for model/temperature/required variables
  • Blade rendering for variable interpolation
  • Drivers for file and database storage
  • Strict validation of required variables

Requirements

  • PHP 8.3+
  • Laravel 12+

Installation

composer require baconfy/prompt

The service provider auto-registers. Publish what you need:

php artisan vendor:publish --tag=prompt-config
php artisan vendor:publish --tag=prompt-migrations    # only if you plan to use the database driver
php artisan migrate

Quick start

Create a prompt at resources/prompts/welcome.md:

---
model: claude-opus-4-5
temperature: 0.7
required: [name]
---
You are a helpful assistant. Greet {{ $name }} warmly and ask how you can help today.

Render it:

$prompt = prompt('welcome', ['name' => 'John']);

$prompt->content;           // rendered string
$prompt->metadata['model']; // 'claude-opus-4-5'
(string) $prompt;           // same as ->content (implements Stringable)

Use the metadata to drive your LLM call:

$prompt = prompt('welcome', ['name' => 'John']);

$response = $anthropic->messages()->create([
    'model'       => $prompt->metadata['model'],
    'temperature' => $prompt->metadata['temperature'],
    'messages'    => [['role' => 'user', 'content' => (string) $prompt]],
]);

Front matter

Front matter is an optional YAML block at the top of the prompt:

---
model: claude-opus-4-5
temperature: 0.7
required: [user_name, context]
description: Onboarding greeting
tags: [onboarding, greeting]
---
Hello {{ $user_name }}! Considering {{ $context }}, welcome aboard.

Behavior:

  • If the file does not start with ---, it is treated as plain content (no metadata, no validation, just Blade).
  • required: [...] is enforced. Missing variables throw MissingRequiredVariablesException.
  • Anything else is metadata. The package does not interpret it; read it via $prompt->metadata['anything'].

Drivers

File driver

Default. Reads from resources/prompts/*.md. Dot notation maps to subfolders:

prompt('auth.login');               // resources/prompts/auth/login.md
prompt('emails.welcome.subject');   // resources/prompts/emails/welcome/subject.md

Database driver

Stores prompts in a prompts table with name and content columns. The content column holds raw markdown — exactly the same format as the file driver. Front matter, when present, sits inline at the top of content.

use Baconfy\Prompt\Models\Prompt;

Prompt::create([
    'name'    => 'welcome',
    'content' => <<<'MD'
        ---
        model: claude-opus-4-5
        required: [name]
        ---
        Hello {{ $name }}!
        MD,
]);

prompt('welcome', ['name' => 'John']);   // works the same way

Migrating prompts between file and database drivers is a copy/paste — the storage format is identical.

Switch the default driver in .env:

PROMPTS_DRIVER=database
PROMPTS_CONNECTION=mysql       # optional, falls back to DB_CONNECTION
PROMPTS_TABLE=prompts          # optional

Or use both side by side:

// config/prompt.php
'drivers' => [
    'system' => [
        'driver' => 'file',
        'folder' => resource_path('prompts/system'),
    ],
    'user' => [
        'driver' => 'database',
        'table'  => 'user_prompts',
    ],
],
Prompt::driver('system')->find('welcome');
Prompt::driver('user')->find('welcome');

API

Helper

prompt(string $name, array $data = []): RenderedPrompt

Facade

use Baconfy\Prompt\Facades\Prompt;

Prompt::get('welcome', ['name' => 'John']);   // RenderedPrompt
Prompt::source('welcome');                     // ParsedFrontMatter|null
Prompt::driver('database');                    // Driver instance (defaults to active)

RenderedPrompt

$prompt->content;    // rendered string
$prompt->metadata;   // array<string, mixed>
(string) $prompt;    // same as ->content

ParsedFrontMatter

What Prompt::source() returns — pre-render. Useful when you want metadata without rendering Blade:

$source = Prompt::source('welcome');
$source->metadata['model'];   // 'claude-opus-4-5'
$source->content;             // raw template, with Blade tags untouched

Prompt model

Eloquent model on the prompts table. Use it to seed, update, or otherwise manage DB-backed prompts:

use Baconfy\Prompt\Models\Prompt as PromptModel;

PromptModel::create([
    'name'    => 'welcome',
    'content' => <<<'MD'
        ---
        model: claude-opus-4-5
        ---
        Hello {{ $name }}!
        MD,
]);

PromptModel::where('name', 'welcome')->update(['content' => 'Hi {{ $name }}!']);

The driver itself does not depend on this model — it reads via Query Builder. The model is a convenience for your CRUD layer.

CLI

Three Artisan commands ship with the package:

php artisan prompt:list                              # list prompts across all configured drivers
php artisan prompt:list database                     # list prompts for a specific driver
php artisan prompt:show welcome                      # show metadata and raw content for a single prompt
php artisan prompt:show welcome --driver=database    # target a specific driver
php artisan prompt:make welcome                      # scaffold resources/prompts/welcome.md

prompt:list accepts an optional driver argument (the name of any driver defined in config/prompt.php); omitting it iterates all drivers. prompt:show accepts a --driver= option to target a specific named driver instead of the active default. prompt:make is file-driver only — database prompts are created directly via the prompts table.

prompt:show is your debug companion: confirm what Prompt::source('welcome') will return before rendering anywhere.

Testing

Faking prompts

In your application's tests, replace the real driver with a stub so prompts don't hit the filesystem or database:

use Baconfy\Prompt\Facades\Prompt;
use Baconfy\Prompt\RenderedPrompt;

Prompt::fake([
    'welcome' => 'Hello stub!',
    'auth.login' => new RenderedPrompt('Stub login.', ['model' => 'gpt-4']),
]);

// code under test
prompt('welcome', ['name' => 'whatever']);

Prompt::assertCalled('welcome');
Prompt::assertNotCalled('checkout');

A plain string stub is wrapped in a RenderedPrompt with empty metadata; pass a RenderedPrompt instance when your code reads $prompt->metadata.

Factories

The Prompt model ships with an Eloquent factory for seeding test data when you use the database driver:

use Baconfy\Prompt\Models\Prompt;

Prompt::factory()->create();
Prompt::factory()->count(5)->create();
Prompt::factory()->create(['name' => 'welcome', 'content' => 'Hi!']);

Configuration

config/prompt.php:

return [
    'default' => env('PROMPTS_DRIVER', 'file'),

    'drivers' => [
        'file' => [
            'driver' => 'file',
            'folder' => env('PROMPTS_FOLDER', resource_path('prompts')),
        ],
        'database' => [
            'driver'     => 'database',
            'connection' => env('PROMPTS_CONNECTION'),
            'table'      => env('PROMPTS_TABLE', 'prompts'),
        ],
    ],
];

The drivers array supports any number of named entries. Each one has a driver field (file or database) plus the keys that driver needs. The same type can appear under multiple names (e.g. two file folders for system vs. user prompts).

Exceptions

use Baconfy\Prompt\Exceptions\PromptNotFoundException;
use Baconfy\Prompt\Exceptions\MissingRequiredVariablesException;
  • PromptNotFoundException — thrown by Prompt::get() when the name is not found by the active driver. Exposes ->name.
  • MissingRequiredVariablesException — thrown when the prompt declares required in its metadata and any variable is missing from $data. Exposes ->variables (the list of missing names).

Admin Panel

Prompt

Optional admin panel for managing prompts stored in the database driver. The file driver remains read-only and is meant to be managed via Git.

The panel ships as plain Blade views styled with Tailwind via CDN — no front-end build step, no JavaScript framework, no extra dependencies. Routes are mounted at /_prompts by default whenever prompt.panel.enabled is true.

Authorization

Two equivalent options (the callback wins if both are present):

// AppServiceProvider::boot() — closure style (like Horizon::auth)
use Baconfy\Prompt\Panel;

Panel::auth(fn ($user = null) => $user?->email === 'you@example.com');
// or via a regular Gate
use Illuminate\Support\Facades\Gate;

Gate::define('viewPrompts', fn ($user = null) => $user?->isAdmin());

If neither is defined, the panel responds 403.

Features

  • Index at /_prompts — paginated list of every prompt name (latest version per name) with a vN badge and live search.
  • Editor at /_prompts/create and /_prompts/{prompt}/edit — form with live preview that renders the prompt through Blade using JSON variables. Refuses to save if the content is identical to the current latest version.
  • Versions at /_prompts/{prompt}/versions — accordion of every revision with a unified diff against the current version. Each older row can be Restored (creates a new version on top with the chosen content) or Deleted.

Versioning model

Every save inserts a new row in the prompts table; existing rows are never updated. Rows sharing a name form a version chain:

  • First save → new row with root_id = null (the root, v1).
  • Subsequent saves → new row with root_id pointing to the v1 row.
  • Prompt::get('welcome') always resolves to the latest version (MAX(id) per name).

Configuration

// config/prompt.php
'panel' => [
    'enabled'    => env('PROMPTS_PANEL_ENABLED', true),
    'path'       => env('PROMPTS_PANEL_PATH', '_prompts'),
    'gate'       => 'viewPrompts',
    'middleware' => ['web'],
],

Publish the views to customize them:

php artisan vendor:publish --tag=prompt-views

Security

Blade compiles prompt content. Do not load prompt content from untrusted sources. A prompt containing {{ system('rm -rf /') }} would execute that PHP if rendered. Treat prompts as code, not user input.

The panel is gated by Panel::auth() / Gate and ships behind the web middleware group by default — it is not safe to expose publicly without authorization.

Development

Run the package test suite:

composer test            # pest
composer test:coverage   # 100% required
composer test:types      # phpstan
composer format          # pint

Boot a local dev environment (Orchestra Testbench workbench + SQLite):

composer dev             # builds the workbench DB and serves at http://127.0.0.1:8000

The workbench wires Panel::auth() to always allow, so / redirects to /_prompts ready to use.

Credits

License

See LICENSE for details.