PHPackages                             opscale/fields - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. opscale/fields

ActiveLibrary

opscale/fields
==============

Family of custom fields for Laravel Nova. Includes AI: a conversational assistant that proposes values for the fields of a resource form.

1.0.0(1mo ago)00MITPHPPHP ^8.3CI passing

Since Jul 11Pushed 1mo agoCompare

[ Source](https://github.com/opscale-co/nova-ai-field)[ Packagist](https://packagist.org/packages/opscale/fields)[ RSS](/packages/opscale-fields/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (13)Versions (2)Used By (0)

opscale/fields
==============

[](#opscalefields)

Family of custom fields for [Laravel Nova](https://nova.laravel.com). The first member is **`AI`**: a conversational assistant that lives inside a resource form, chats with the user, and **proposes values for the other fields of the form**. The user accepts or discards each proposal — the AI never writes anything by itself.

```
use Opscale\Fields\AI;

public function fields(NovaRequest $request): array
{
    return [
        Text::make('Name'),
        Textarea::make('Description'),
        BelongsTo::make('Category', 'category'),

        AI::make('Assistant')
            ->fields(['name', 'description', 'category'])
            ->agent(ProductCopilot::class)
            ->context(fn (NovaRequest $r) => ['tenant_id' => $r->user()->tenant_id])
            ->onlyOnForms(),
    ];
}
```

- `->fields([...])` — which form attributes the assistant may propose values for. The `proposeFields` tool schema handed to the agent is derived from this single list, so it can never drift from the form.
- `->agent(...)` — a [laravel/ai](https://github.com/laravel/ai) agent class extending `Opscale\Fields\AI\Agents\FieldAgent`.
- `->context(...)` — extra **server-side** context (tenant, user...). Client input is never trusted for authorization.
- `->maxDuration(120)` — seconds after which the stream self-terminates (see footguns).

The field is headless: no database column, a no-op on save, forms only. If the panel's JavaScript ever fails, the form keeps working — the assistant is strictly additive and can be ignored entirely.

Requirements
------------

[](#requirements)

DependencyVersionWhy pinnedPHP^8.3required by laravel/aiLaravel^12 | ^13required by laravel/ailaravel/nova**~5.9.0 (pinned)**accepted values are pushed into native fields through Nova's *internal* event bus (`{formUniqueId}-{attribute}-value` / `-change`). That naming is undocumented API; re-verify it in `vendor/laravel/nova/public/app.js` (`getFieldAttributeValueEventName`, `listenToValueChanges`) and the `laravel-nova` npm mixins (`src/mixins/FormField.js`) before raising the constraint.laravel/ai^0.9streams the **Vercel UI Message Stream v1** (`x-vercel-ai-ui-message-stream: v1`)@ai-sdk/vue (bundled)~4.0.22 + ai ~7.0.22consumes exactly that protocol version — bump both sides of the protocol togetherInstallation
------------

[](#installation)

```
composer require opscale/fields
```

Publish laravel/ai's config and conversation tables (the field persists chat history with `RemembersConversations`):

```
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
```

Configure your provider key **on the server** — it never reaches the browser:

```
OPENAI_API_KEY=sk-...        # or ANTHROPIC_API_KEY, etc. (config/ai.php)
```

Writing an agent
----------------

[](#writing-an-agent)

Extend `FieldAgent`: describe your domain in `persona()` and expose lookup tools for relationship fields. Everything else (proposal schema, current form values, server context, the `proposeFields` tool, conversation memory) is injected by the field's endpoint.

```
use Opscale\Fields\AI\Agents\FieldAgent;

final class ProductCopilot extends FieldAgent
{
    protected function persona(): string
    {
        return 'You are a product catalog copilot... derive name, description, '
            .'price, SKU, stock and category from a short merchant description.';
    }

    protected function lookupTools(): iterable
    {
        yield new SearchCategories; // returns real category IDs from the DB
    }
}
```

Relationship fields are proposed as **ID + display label**: the ID is what fills the field, the label is what the user sees before accepting. The base instructions forbid inventing IDs — the model must pick them from a lookup tool.

The compression ratio
---------------------

[](#the-compression-ratio)

If the assistant asked one question per field, this would just be a slower form. The base prompt pushes the opposite: **few high-level questions, many derived fields**. Real exchange with the `ProductCopilot` demo (2 user answers → 6 fields proposed):

> **User:** I want to list the wireless mouse we just received. **Assistant:** What price point and roughly how many units did you receive? **User:** Around $49.99, we got 200 units. **Assistant:** *(proposes via `proposeFields`)*
>
> - Name → "Wireless Pro Mouse"
> - Description → "Precision wireless mouse with ergonomic design..."
> - Price → 49.99
> - SKU → "WPM-WL-001"
> - Stock → 200
> - Category → Peripherals *(ID 1, from `searchCategories`)*

The user accepts all six, edits any of them by hand, or types "make the description more formal" to get a refined re-proposal — all inside the panel.

How it works
------------

[](#how-it-works)

```
Vue panel (useChat, @ai-sdk/vue)
  └── POST nova-vendor/nova-ai-field/{resource}/ai/chat   ← Nova middleware
        └── ChatController re-resolves the AI field on the resource
              └── FieldAgent (laravel/ai) + RemembersConversations
                    ├── lookup tools (real DB queries)
                    └── proposeFields tool → inert handle(), no server effect
        └── ->stream()->usingVercelDataProtocol()   (SSE, UI Message Stream v1)

Panel reads proposeFields tool invocations from the typed message parts
  └── proposal list: field → value → [accept] [discard]
        └── accept: Nova.$emit(`{formUniqueId}-{attribute}-value`, value)

```

The model never returns text that needs parsing: proposals arrive as a typed `tool-input-available` part. `proposeFields.handle()` exists because laravel/ai's `Tool` contract requires it, but it is deliberately inert — it only acknowledges, executing nothing.

Footguns
--------

[](#footguns)

1. **Proxy buffering.** The endpoint already sends `X-Accel-Buffering: no`, but nginx setups with `proxy_buffering on` will still swallow the stream and deliver everything at once when it finishes. It looks like a code bug; it is not. Configure:

    ```
    location /nova-vendor/nova-ai-field/ {
        proxy_buffering off;
    }
    ```
2. **PHP-FPM workers.** Every open SSE stream holds a worker. Fine for a Nova panel (internal users, low concurrency), but keep a bound on it: the field passes `maxDuration` (default 120s) as the stream timeout so a stuck stream frees its worker. Tune per field with `->maxDuration(seconds)` and size `pm.max_children` accordingly.
3. **Nova is pinned (`~5.9.0`)** because accepting a proposal uses Nova's internal event bus (see Requirements). When bumping Nova, re-check the event names before widening the constraint.
4. **Relationship fields.** A `BelongsTo` is filled with an ID, never a label, so proposals carry both (`value` = ID, `display` = label). Known v1 limitation: Nova 5.9's BelongsTo form component does not subscribe to the `-value` event bus, so accepting a relation proposal may not visually fill the combobox — the user sees the proposed record's name in the panel and can select it manually. Text, textarea, number, markdown and similar native fields do subscribe and fill instantly.

Out of scope (v1)
-----------------

[](#out-of-scope-v1)

- Inline ✨ buttons on native Nova fields.
- "Pending state" styling over native fields.
- Any override of Nova's Vue components (fragile across upgrades — everything lives inside this field's panel).

Development
-----------

[](#development)

```
composer serve      # build workbench + Nova dev server (admin@laravel.com / password)
npm run quality     # duster + phpstan + pest
npm run watch       # rebuild JS on change
```

The workbench ships a `Product` resource wired to the `ProductCopilot` demo agent (`workbench/app/Agents`).

Package layout
--------------

[](#package-layout)

```
src/
├── FieldServiceProvider.php   shared provider: one route group + ONE asset bundle
├── AI.php                      the field class (Opscale\Fields\AI)
└── AI/                         the field's support code (Opscale\Fields\AI\*)
    ├── Agents/FieldAgent.php
    ├── Http/Controllers/ChatController.php
    └── Tools/ProposeFields.php
resources/js/
├── field.js                    single entrypoint registering every field's components
└── ai/FormField.vue
routes/api.php                  grouped under nova-vendor/nova-ai-field

```

`src/AI.php` (class) and `src/AI/` (support namespace) coexist on purpose — valid PSR-4, do not flatten. The package is multi-field by design: new fields add components to the same bundle, routes to the same group, and registrations to the same provider.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance90

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 50% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

50d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3722594?v=4)[opscale](/maintainers/opscale)[@opscale](https://github.com/opscale)

---

Top Contributors

[![opscale-development](https://avatars.githubusercontent.com/u/181295122?v=4)](https://github.com/opscale-development "opscale-development (1 commits)")[![semantic-release-bot](https://avatars.githubusercontent.com/u/32174276?v=4)](https://github.com/semantic-release-bot "semantic-release-bot (1 commits)")

---

Tags

laravelaiformfieldnovacopilotassistant

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/opscale-fields/health.svg)

```
[![Health](https://phpackages.com/badges/opscale-fields/health.svg)](https://phpackages.com/packages/opscale-fields)
```

###  Alternatives

[unopim/unopim

UnoPim Laravel PIM

10.8k2.5k](/packages/unopim-unopim)[outl1ne/nova-sortable

This Laravel Nova package allows you to reorder models in a Nova resource's index view using drag &amp; drop.

2852.3M9](/packages/outl1ne-nova-sortable)[optimistdigital/nova-sortable

This Laravel Nova package allows you to reorder models in a Nova resource's index view using drag &amp; drop.

2852.2M6](/packages/optimistdigital-nova-sortable)[outl1ne/nova-simple-repeatable

A Laravel Nova simple repeatable rows field.

75429.1k](/packages/outl1ne-nova-simple-repeatable)[dniccum/phone-number

A Laravel Nova phone number field with input masking and validation support.

70484.9k](/packages/dniccum-phone-number)[markwalet/nova-modal-response

A Laravel Nova asset for Modal responses on an action.

17984.6k](/packages/markwalet-nova-modal-response)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
