Why I Built a Business Content Layer on Top of Laravel AI SDK

Published: (March 13, 2026 at 06:53 PM EDT)
6 min read
Source: Dev.to

Source: Dev.to

The problem with raw LLM calls in business apps

Here’s what generating a payment reminder looks like with laravel/ai directly:

$ai = app(\Laravel\Ai\Contracts\Ai::class);

$response = $ai->text(
    "You are a professional business assistant. Generate a payment reminder 
     email for a client. The invoice number is #1042. It is 30 days overdue. 
     The amount is 1500 EUR. The client name is Jean Martin. 
     Our company is Acme Corp.

     Return a JSON object with: subject, message, call_to_action.
     Do not invent any information not provided above.
     Use a firm but professional tone.
     Language: French."
);

// Now parse the response...
// Hope it returned valid JSON...
// Hope it didn't invent an amount...
// Hope the tone is right...

It works… until it doesn’t.

Why this approach breaks down in production

  • Hard‑coded context – prompts contain all the data, so there’s no reusable structure.
  • Fragile output parsing – the model sometimes returns prose instead of JSON.
  • Anti‑hallucination is only a suggestion – the model can still invent data.
  • Tone, language, audience – must be specified manually for every call.
  • No logging – you have no record of what was generated, when, or for which tenant.

All of this gets rewritten for every project and every feature.

What I wanted instead

A layer that handles the business concerns so I only describe what I want:

$response = BusinessAssistant::generate(new AssistantRequestData(
    task: AssistantTask::Email,
    preset: 'payment_reminder',
    goal: 'Invoice #1042, 30 days overdue, second reminder',
    language: 'fr',
    tone: Tone::Direct,
    context: [
        'company'  => true,
        'customer' => ['id' => 42],
        'billing'  => ['invoice_id' => 99],
    ],
));

echo $response->subject;        // "Rappel : Facture #1042 en attente de règlement"
echo $response->message;        // Full professional email body
echo $response->call_to_action; // "Procéder au règlement"

No prompt writing. No output parsing. No hallucination‑guard setup.

The architecture

The engine is a pipeline of contracts. Every step is an interface, so you can swap any layer without touching the rest.

AssistantRequestData


RequestValidator        — validates input fields


PresetRepository        — resolves the matching preset


ContextResolver         — calls your ContextProviders


ContextSanitizer        — strips sensitive keys, limits depth


PromptBuilder           — assembles system + user prompts


TextGenerator           — calls laravel/ai SDK


OutputNormalizer        — parses structured JSON response


GenerationLogger        — records to assistant_generations (best‑effort)


AssistantResponseData

Key design decision: the engine never knows about your data model – it receives plain arrays, and you decide what goes in.

ContextProviders — the bridge between your DB and the engine

Instead of hard‑coding data into prompts, you write simple provider classes:

final class CustomerContextProvider implements ContextProvider
{
    public function key(): string
    {
        return 'customer';
    }

    public function provide(AssistantRequestData $request, array $input = []): array
    {
        $customer = Customer::find($input['id']);

        return [
            'name'  => $customer->full_name,
            'email' => $customer->email,
            'plan'  => $customer->plan,
            'since' => $customer->created_at->format('Y'),
        ];
    }
}

Register the provider once in a config file; every generation call automatically injects the right data.
The engine sanitises the result, stripping any sensitive keys before anything reaches the prompt.

Anti‑hallucination — enforced structurally

The biggest problem with AI in business apps is that the model inventes data.
Example: a payment reminder that invents an amount (€2,340 instead of the real €1,500).

Solution: make hallucination impossible by design. If the data isn’t present in the context, the model can’t use it. Three layers enforce this:

  1. ContextSanitizer – removes anything that shouldn’t be exposed.
  2. Preset system‑prompt constraints – limit what the model may output.
  3. OutputNormalizer validation – ensures the final JSON matches the expected schema.

14 presets — what’s included

PresetDescription
emailProfessional business email
replyContextual client reply
payment_reminderOverdue invoice reminder
appointment_confirmationAppointment details + confirmation
support_replyEmpathetic support response
follow_upPost‑meeting follow‑up
reminderGeneric reminder
announcementCompany / product announcement
promotionCommercial promotional email
social_postPublishable social media post
internal_noteInternal team memo
summaryDocument or interaction summary
customer_onboardingWelcome / onboarding communication
feedback_requestRequest for customer feedback

(Feel free to add more presets as your product evolves.)

TL;DR

  • Wrap raw laravel/ai calls in a pipeline that handles validation, preset resolution, context injection, sanitisation, prompt building, generation, normalisation, and logging.
  • Use ContextProviders to pull data from your database in a reusable, testable way.
  • Enforce anti‑hallucination by never exposing data that isn’t explicitly provided.
  • Define presets for each common business‑type generation to keep prompts DRY and maintainable.

With this structure you stop rewriting the same scaffolding on every project and get a reliable, auditable AI‑powered assistant for all your business‑logic generation needs.

Overview

  • stomer_summary – Customer briefing for agents
  • rewrite – Text reformulation

Real examples

SaaS billing – second payment reminder

BusinessAssistant::generate(new AssistantRequestData(
    task: AssistantTask::Email,
    preset: 'payment_reminder',
    goal: 'Second reminder. Invoice INV-2024-0112, 30 days overdue.',
    tone: Tone::Direct,
    language: 'fr',
    context: [
        'company'  => true,
        'customer' => ['id' => 14],
        'billing'  => ['invoice_id' => 5501],
    ],
));

Driving school – lesson confirmation

BusinessAssistant::generate(new AssistantRequestData(
    task: AssistantTask::Email,
    preset: 'appointment_confirmation',
    goal: 'Confirm driving lesson. Remind student to bring permit.',
    tone: Tone::Friendly,
    language: 'fr',
    context: [
        'company'     => true,
        'customer'    => ['id' => 88],
        'appointment' => ['lesson_id' => 334],
    ],
));

CRM support – billing dispute

BusinessAssistant::generate(new AssistantRequestData(
    task: AssistantTask::Reply,
    preset: 'support_reply',
    goal: 'Client was billed twice. Acknowledge, apologize, confirm investigation.',
    tone: Tone::Reassuring,
    language: 'en',
    context: [
        'company'   => true,
        'customer'  => ['id' => 42],
        'documents' => ['ticket_id' => 1091],
    ],
));

Provider flexibility

Switch providers with a single .env change – no code modification required:

# Anthropic Claude
BUSINESS_ASSISTANT_PROVIDER=anthropic
BUSINESS_ASSISTANT_MODEL=claude-haiku-4-5-20251001

# OpenAI
BUSINESS_ASSISTANT_PROVIDER=openai
BUSINESS_ASSISTANT_MODEL=gpt-4o-mini

# Local Ollama
BUSINESS_ASSISTANT_PROVIDER=ollama
BUSINESS_ASSISTANT_MODEL=qwen2.5:3b

Multi‑tenant support

Built inside a multi‑tenant ERP, so tenant scoping is first‑class:

$request = new AssistantRequestData(
    task: AssistantTask::Email,
    goal: '...',
    userIdentifier:   (string) auth()->id(),
    tenantIdentifier: (string) $tenant->id,
);

Every generation is logged to assistant_generations with tenant and user identifiers – ready for quota tracking and per‑tenant reporting.

Debug without calling the LLM

Preview the full prompt without making an API call:

$prompt = BusinessAssistant::preview($request);

echo $prompt->systemPrompt; // full system instructions
echo $prompt->userPrompt;   // user prompt with injected context

Installation

composer require fsdev/laravel-business-assistant
php artisan vendor:publish --tag=business-assistant-config
php artisan migrate
php artisan business-assistant:doctor
php artisan business-assistant:demo

Honest about what this is

  • Commercial package (not MIT).
  • Lifetime release – Laravel 12, laravel/ai ~0.2.6.
  • One‑time purchase, no updates guaranteed.
  • Not a replacement for laravel/ai or Prism – it sits on top and handles the business layer they deliberately don’t.

payhip.com/b/TkFob — €49 solo / €149 agency

Happy to answer questions about the architecture or the preset system in the comments.

Built by Fsdev — tematahotoa.tini@gmail.com

0 views
Back to Blog

Related posts

Read more »

Travigo

Travel as fast as you speak with Gemini! Where live agents meet immersive storytelling & 3D navigation. This project was created for entering the Gemini Live Ag...

Micro games

Hey Gamers! 👾 As part of the Rapid Games Prototyping module, we are tasked with reviewing a peer's game. The challenge is to analyse a prototype built in just...