rmh/laravel-openui

OpenUI Generative UI integration for the Laravel AI SDK - inject OpenUI Lang system prompts, define component libraries, and stream structured UI responses.

Maintainers

Package info

github.com/RanaMoizHaider/laravel-openui

pkg:composer/rmh/laravel-openui

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.1 2026-03-13 22:17 UTC

This package is auto-updated.

Last update: 2026-03-13 22:18:25 UTC


README

Latest Version on Packagist Tests License

OpenUI Generative UI integration for the Laravel AI SDK.

This package connects OpenUI — the open standard for Generative UI — with the Laravel AI SDK. It handles system prompt injection, component library management, caching, and streaming, so your agents can respond with structured, interactive UI instead of plain text.

How It Works

OpenUI works in four steps:

Component Library → System Prompt → LLM → OpenUI Lang → Frontend Renderer
  1. You define which React components the LLM is allowed to generate.
  2. This package injects a system prompt into your agent's instructions describing those components and the OpenUI Lang syntax.
  3. The LLM responds in OpenUI Lang (a compact, streaming-native format).
  4. Your frontend's <Renderer /> from @openuidev/react-lang parses and renders it live.

Installation

composer require rmh/laravel-openui

Publish the config file:

php artisan vendor:publish --tag=openui

Quick Start

Option 1: Global Middleware (zero-touch, all agents)

Register the middleware once in your AppServiceProvider and every agent will automatically receive the OpenUI system prompt:

// app/Providers/AppServiceProvider.php

use RMH\OpenUI\Middleware\InjectOpenUIPrompt;
use Laravel\Ai\Facades\Ai;

public function boot(): void
{
    Ai::middleware([InjectOpenUIPrompt::class]);
}

Your agents need no changes. Stream the response back to your OpenUI frontend:

Route::post('/chat', function (Request $request) {
    return MyAgent::make()
        ->stream($request->input('message'))
        ->usingVercelDataProtocol();
});

Option 2: GeneratesUI Trait (explicit opt-in per agent)

Add the trait to agents that should produce UI. Rename instructions() to agentInstructions():

use RMH\OpenUI\Concerns\GeneratesUI;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;

class HotelSearchAgent implements Agent
{
    use Promptable, GeneratesUI;

    protected function agentInstructions(): string
    {
        return 'You are a helpful hotel search assistant.';
    }
}

Toggle UI per call:

// UI enabled (default)
HotelSearchAgent::make()->prompt('Find hotels in Paris');

// UI disabled for this call
HotelSearchAgent::make()->withoutUI()->prompt('Find hotels in Paris');

Option 3: Per-agent Middleware

Add the middleware only to specific agents:

use RMH\OpenUI\Middleware\InjectOpenUIPrompt;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasMiddleware;
use Laravel\Ai\Promptable;

class HotelSearchAgent implements Agent, HasMiddleware
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a helpful hotel search assistant.';
    }

    public function middleware(): array
    {
        return [new InjectOpenUIPrompt];
    }
}

Option 4: Anonymous Agents / Inline Calls

use RMH\OpenUI\Facades\OpenUI;
use function Laravel\Ai\{agent};

$response = agent(
    instructions: 'You are a travel assistant.' . "\n\n" . OpenUI::systemPrompt(),
)->prompt('Find hotels in Paris');

Defining Your Component Library

In config/openui.php

'components' => [
    'Card' => [
        'title'       => 'string',
        'description' => 'string?',   // ? = optional
        'imageUrl'    => 'string?',
        'ctaLabel'    => 'string?',
    ],
    'Carousel' => [
        'cards' => 'Card[]',           // nested component reference
    ],
    'Table' => [
        'columns' => 'array',
        'rows'    => 'array',
    ],
    'Alert' => [
        'type'    => 'enum:info|success|warning|error',
        'title'   => 'string',
        'message' => 'string?',
    ],
],

Supported prop types: string, int, float, bool, array, ComponentName[], enum:a|b|c. Append ? to mark any type as optional.

Via the Facade (programmatic / runtime)

use RMH\OpenUI\Facades\OpenUI;

// Register one component
OpenUI::component('PriceTag', [
    'amount'   => 'string',
    'currency' => 'string?',
]);

// Register many at once
OpenUI::library([
    'Chart'     => ['type' => 'enum:bar|line|pie', 'data' => 'array'],
    'MapPin'    => ['lat' => 'float', 'lng' => 'float', 'label' => 'string?'],
]);

The cache is automatically invalidated when you add components via the facade.

Via the Artisan Command

Scaffold a typed component definition class:

php artisan openui:component Card --props="title:string,description:string?,imageUrl:string?"

This generates app/Ai/OpenUI/Components/CardComponent.php. Register it in your service provider:

use RMH\OpenUI\Facades\OpenUI;
use App\Ai\OpenUI\Components\CardComponent;

OpenUI::component((new CardComponent)->name(), (new CardComponent)->props());

Export for Frontend

Export your component definitions to TypeScript/Zod for your frontend:

php artisan openui:export

This generates resources/js/openui.ts with Zod schemas:

// Auto-generated by laravel-openui
import { z } from 'zod';

export const CardSchema = z.object({
  title: z.string(),
  description: z.string().optional(),
  imageUrl: z.string().optional(),
});

export const openUISchema = z.object({
  Card: CardSchema,
});

Export options:

Option Description
--path=... Custom output path (default: resources/js/openui.ts)
--format=zod|ts|json Output format (default: zod)
--types TypeScript types only (shorthand for --format=ts)
--force Overwrite existing file

Examples:

# Export to custom path
php artisan openui:export --path=frontend/src/openui.ts

# Export TypeScript types only (no Zod)
php artisan openui:export --types

# Export as JSON
php artisan openui:export --format=json --path=schema.json

# Force overwrite
php artisan openui:export --force

Configuration Reference

// config/openui.php

'enabled'                => env('OPENUI_ENABLED', true),
'design_system'          => env('OPENUI_DESIGN_SYSTEM', 'shadcn'),
'stream_protocol'        => env('OPENUI_STREAM_PROTOCOL', 'vercel'),
'system_prompt_placement'=> 'append',  // 'append' | 'prepend'
'custom_system_prompt'   => null,      // override the entire generated prompt

'components' => [...],

'allowed_agents' => [],               // empty = all agents; list FQCN to restrict

'cache' => [
    'enabled' => env('OPENUI_CACHE_ENABLED', true),
    'store'   => env('OPENUI_CACHE_STORE', null),
    'ttl'     => env('OPENUI_CACHE_TTL', 3600),
],

Frontend Integration

Option 1: Use Exported Schema (Recommended)

Export your component definitions and import them directly:

php artisan openui:export

Then in your frontend:

import { Renderer, createLibrary, defineComponent } from '@openuidev/react-lang';
import { CardSchema, CarouselSchema } from './openui';
import { Card, Carousel } from './components';

const components = [
  defineComponent({
    name: 'Card',
    props: CardSchema,
    component: ({ props }) => <Card {...props} />,
  }),
  defineComponent({
    name: 'Carousel',
    props: CarouselSchema,
    component: ({ props }) => <Carousel {...props} />,
  }),
];

export const library = createLibrary({ root: 'Card', components });

<Renderer library={library} stream={streamFromLaravel} />

Option 2: Manual Schema Definition

Define schemas manually on the frontend:

import { Renderer, createLibrary, defineComponent } from '@openuidev/react-lang';
import { z } from 'zod';

const Card = defineComponent({
    name: 'Card',
    props: z.object({
        title: z.string(),
        description: z.string().optional(),
        imageUrl: z.string().optional(),
        ctaLabel: z.string().optional(),
    }),
    component: ({ props }) => <YourCardComponent {...props} />,
});

export const library = createLibrary({ root: 'Card', components: [Card] });

<Renderer library={library} stream={streamFromLaravel} />

Inspecting the Generated System Prompt

use RMH\OpenUI\Facades\OpenUI;

// View what's being injected
dd(OpenUI::systemPrompt());

// Flush and regenerate
OpenUI::flushCache();

Testing

When writing tests for agents that use OpenUI, you can temporarily disable injection:

config(['openui.enabled' => false]);

Or assert on the generated prompt:

use RMH\OpenUI\Facades\OpenUI;

$this->assertStringContainsString('Card', OpenUI::systemPrompt());

Changelog

Please see CHANGELOG.md.

License

MIT — see LICENSE.md.