AI Toolkit (Enterprise)

The AI Toolkit in apex-grid-enterprise turns a natural-language prompt into a grid change (sort, filter, group, pivot, …) or an answer about the data. By default it runs on a built-in deterministic rule engine: offline, no API key, no network calls. You can add an optional LLM (a first-class Claude reasoner is included) that the grid escalates to only when the rule engine is not confident.

Every planned change is validated against the grid's schema and applied through the defensive setState(), with a one-click undo(). Read-only questions never mutate the grid.

Changed in 0.6.0: the 0.5.0 adapter API (aiAdapter, createClaudeAdapter, createMockAdapter) has been removed. The toolkit now runs offline out of the box with no adapter, and an optional LLM is wired through aiReasoner. See Migrating from the 0.5.0 adapter API.

Running a prompt

The rule engine is active with no configuration. runPrompt(prompt, options?) returns a discriminated AIResult:

// control mode (default): the prompt becomes a state change, which is applied
const result = await grid.runPrompt('group by region, then sort by revenue, highest first');
if (result.mode === 'control') {
  console.log(result.applied);   // e.g. ['group', 'sort'] — what changed
  console.log(result.skipped);   // slices present but not applied
  console.log(result.warnings);  // anything dropped or unmapped, each with a reason
  result.undo();                 // idempotent one-click revert
}

// ask mode: get an answer, change nothing
const answer = await grid.runPrompt('which region has the highest average revenue?', { mode: 'ask' });
if (answer.mode === 'ask') console.log(answer.answer);

runPrompt validates every planned change against the schema (dropping anything out of vocabulary, with a reported reason), applies it via setState(), and returns an idempotent undo() that restores the pre-prompt snapshot.

RunPromptOptionsDescription
mode'control' (default) applies a state change; 'ask' returns an answer only
signalAbortSignal forwarded to any LLM reasoner for cancellation
maxDataRowsRows sampled into the model context for this call

The control result carries plan (the inspectable plan that ran), applied, skipped, warnings, and undo(). The ask result carries plan, answer, and an abstained flag (see below). To plan without applying, use previewPrompt(prompt, options?), which returns the Plan and changes nothing.

What the rule engine understands

Use these and they work instantly and offline; anything outside the set escalates to your LLM reasoner (if configured) or is reported honestly.

  • Sort: "sort by revenue, highest first", "order by name", "reverse the sort".
  • Group / ungroup: "group by region", "ungroup".
  • Filter: "filter status = open", "only show EMEA", "remove rows where salary is under 70000".
  • Columns: "hide the notes column", "show salary", "pin name to the left".
  • Search: "search Acme".
  • Pivot / aggregate: "pivot on region", "sum of revenue".
  • Pagination / export: "page 2", "page size 50", "export as csv".
  • Reset / undo: "reset", "undo".
  • Read-only questions: "how many rows", "highest / lowest / average salary", "min, max and median of bonus", "average salary by department", "who has the highest salary", "top 5 by revenue".

Compound requests work in one sentence: "group by department, then sort by salary and remove all rows under 70000" applies three steps atomically with a single undo().

Read-only analytics

Ask-mode questions run through a deterministic, read-only analytics layer that answers min / max / median, grouped, and ranked questions with no LLM, straight from the grid data:

const a = await grid.runPrompt('average salary by department', { mode: 'ask' });
// a.mode === 'ask', a.answer holds the text reply; the grid is unchanged

Honest abstention (no silent no-ops)

When a prompt cannot be mapped to a grid action and no LLM reasoner is configured, runPrompt abstains rather than silently doing nothing or guessing:

const result = await grid.runPrompt('teleport the widget sideways');
// result.mode === 'ask', result.abstained === true
// result.answer is a short "I could not turn that into a grid action" note

Confidence is calibrated on how much the prompt actually grounds against your schema, so a near-miss command scores low and either escalates to your LLM reasoner or abstains, instead of returning a confident wrong answer.

Optional LLM escalation

Leave runPrompt as is for the offline rule engine. To also handle prompts the rules cannot map, assign a Reasoner to aiReasoner. The grid tries the rule engine first and escalates only when it is unsure, so simple requests stay instant and offline.

import { createClaudeReasoner, createLLMReasoner } from 'apex-grid-enterprise';

// Production: your backend holds the key and calls Anthropic; the browser never sees it.
grid.aiReasoner = createClaudeReasoner({ endpoint: '/api/grid-ai' });

// Development only: call Anthropic from the browser (exposes the key to the page).
grid.aiReasoner = createClaudeReasoner({ apiKey: '...', dangerouslyAllowBrowser: true });

// Or bring any provider by implementing a single completion function.
grid.aiReasoner = createLLMReasoner({ complete: async (req) => ({ patch: /* … */ }) });
ClaudeReasonerConfigDescription
endpointProduction transport: POST { prompt, mode, schema, data } to your backend, which returns { patch?, answer? }
apiKeyDev transport: call Anthropic directly (requires dangerouslyAllowBrowser)
dangerouslyAllowBrowserAcknowledge the in-browser key is exposed to the page (dev only)
modelModel id; defaults to claude-opus-4-8

The Claude direct transport dynamically imports @anthropic-ai/sdk (an optional peer dependency), and uses tool use so the model returns a state patch shaped by the grid's schema. Install the SDK only when you use the direct transport:

npm install @anthropic-ai/sdk

The AI runtime is loaded on demand, on the first runPrompt / previewPrompt, so a grid that never runs a prompt bundles none of it.

Prompt panel (<apex-grid-ai>)

You do not have to build your own prompt UI. <apex-grid-ai> is a ready-made panel that drives runPrompt for you: it shows a source badge (rule engine vs AI), a plan preview, a transcript of what changed (with an Undo button), the answer in ask mode, or a distinct abstention message when a prompt could not be mapped. Bind it to a grid through its grid property:

<apex-grid-ai mode="inline"></apex-grid-ai>
document.querySelector('apex-grid-ai').grid = grid;

mode="inline" renders in place; mode="dialog" (the default) is a floating, draggable panel. The enterprise grid also adds an "Ask AI" toolbar button that opens the panel in a dialog. The element is registered by apex-grid-enterprise/define and needs no adapter; set an aiReasoner only to add LLM escalation.

How it stays safe

The control path is guarded in layers: an LLM is constrained by the grid's schema, anything out of vocabulary is stripped before it is applied (each drop reported), the defensive setState() drops and reports the rest, and every change is one click undoable. Ask mode is read-only, and an unmappable prompt abstains instead of acting.

Migrating from the 0.5.0 adapter API

0.6.0 replaces the adapter API with the reasoner pipeline; there is no back-compat shim.

Removed in 0.6.0Use instead
grid.aiAdapter = …Nothing for the offline rule engine; grid.aiReasoner = … to add an LLM
createClaudeAdapter({ endpoint })createClaudeReasoner({ endpoint })
createClaudeAdapter({ apiKey, dangerouslyAllowBrowser })createClaudeReasoner({ apiKey, dangerouslyAllowBrowser })
createMockAdapter({ rules })Removed; the built-in rule engine covers offline demos and tests with no config
AIAdapter, AIRequest, AIResponse typesReasoner, createLLMReasoner({ complete })

runPrompt no longer rejects when nothing is wired: with no reasoner set it runs entirely on the rule engine. The AIResult shape changed to { mode, plan, applied, skipped, warnings, undo } (control) and { mode, plan, answer, abstained? } (ask); undo() now returns void.

React example

import { useEffect, useRef, useState } from 'react'
import 'apex-grid-enterprise/define'
import { createClaudeReasoner } from 'apex-grid-enterprise'

export default function AIGrid() {
  const ref = useRef<any>(null)
  const [prompt, setPrompt] = useState('')

  useEffect(() => {
    const grid = ref.current
    grid.columns = columns
    grid.data = data
    // Optional: add LLM escalation. Omit for the offline rule engine.
    grid.aiReasoner = createClaudeReasoner({ endpoint: '/api/grid-ai' })
  }, [])

  const run = async () => {
    const result = await ref.current.runPrompt(prompt)
    if (result.mode === 'control') console.log('applied', result.applied)
  }

  return (
    <div>
      <input value={prompt} onChange={(e) => setPrompt(e.target.value)} />
      <button onClick={run}>Run</button>
      <apex-grid-enterprise ref={ref} style={{ height: 480 }} />
    </div>
  )
}