Skip to content

Commit d8e8a39

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents 26175f6 + 8373270 commit d8e8a39

18 files changed

Lines changed: 405 additions & 186 deletions

File tree

Taskfile.yml

Lines changed: 0 additions & 27 deletions
This file was deleted.

adminforth/documentation/docs/tutorial/08-Plugins/26-agent.md

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,13 @@ import CompletionAdapterOpenAIChatGPT from '@adminforth/completion-adapter-open-
203203
plugins: [
204204
...
205205
new AdminForthAgent({
206+
// optional, can be used to suggest example prompts in the UI
207+
// placeholderMessages: async ({ adminUser, httpExtra }) => {
208+
// return [
209+
// 'What is a cars count in SQLite',
210+
// 'Build average car price by days chart in SQLite',
211+
// ];
212+
// },
206213
modes: [
207214
{
208215
name: 'Balanced',
@@ -260,6 +267,8 @@ plugins: [
260267
// optional
261268
// debugField: 'debug',
262269
},
270+
// optional, see the "Persistent checkpointer" section below
271+
// checkpointResource: { ... },
263272
}),
264273
]
265274
```
@@ -268,6 +277,218 @@ Each item in `modes` defines a user-selectable preset in the chat UI. The select
268277

269278
The plugin adds a chat surface to the admin UI, keeps session history per admin user, and shows a mode picker when `modes` are configured.
270279

280+
## Persistent checkpointer
281+
282+
If you do not configure `checkpointResource`, the plugin falls back to an in-memory `MemorySaver`. This is fine for local testing, but checkpoints are lost on process restart.
283+
284+
If you want persistent LangGraph checkpoints between requests, add a dedicated resource and pass it via `checkpointResource`.
285+
286+
You can use your own table and field names. The plugin does not require a specific schema name, only a mapping for these logical fields:
287+
288+
- `idField`
289+
- `threadIdField`
290+
- `checkpointNamespaceField`
291+
- `checkpointIdField`
292+
- `parentCheckpointIdField`
293+
- `rowKindField`
294+
- `taskIdField`
295+
- `sequenceField`
296+
- `createdAtField`
297+
- `checkpointPayloadField`
298+
- `metadataPayloadField`
299+
- `writesPayloadField`
300+
- `schemaVersionField`
301+
302+
Create a resource for checkpoint rows:
303+
304+
```ts title="./resources/agent_resources/checkpoints.ts"
305+
import { AdminForthDataTypes } from 'adminforth';
306+
import type { AdminForthResourceInput } from 'adminforth';
307+
308+
export default {
309+
dataSource: 'sqlite',
310+
table: 'checkpoints',
311+
resourceId: 'checkpoints',
312+
label: 'Checkpoints',
313+
recordLabel: (record) => record.id,
314+
options: {
315+
allowedActions: {
316+
create: false,
317+
edit: false,
318+
},
319+
},
320+
columns: [
321+
{
322+
name: 'id',
323+
primaryKey: true,
324+
type: AdminForthDataTypes.STRING,
325+
showIn: {
326+
edit: false,
327+
create: false,
328+
},
329+
},
330+
{
331+
name: 'thread_id',
332+
type: AdminForthDataTypes.STRING,
333+
},
334+
{
335+
name: 'checkpoint_ns',
336+
type: AdminForthDataTypes.STRING,
337+
},
338+
{
339+
name: 'checkpoint_id',
340+
type: AdminForthDataTypes.STRING,
341+
},
342+
{
343+
name: 'parent_checkpoint_id',
344+
type: AdminForthDataTypes.STRING,
345+
},
346+
{
347+
name: 'row_kind',
348+
type: AdminForthDataTypes.STRING,
349+
enum: [
350+
{ value: 'checkpoint', label: 'Checkpoint' },
351+
{ value: 'writes', label: 'Writes' },
352+
],
353+
},
354+
{
355+
name: 'task_id',
356+
type: AdminForthDataTypes.STRING,
357+
},
358+
{
359+
name: 'seq',
360+
type: AdminForthDataTypes.INTEGER,
361+
},
362+
{
363+
name: 'created_at',
364+
type: AdminForthDataTypes.DATETIME,
365+
showIn: {
366+
edit: false,
367+
create: false,
368+
},
369+
},
370+
{
371+
name: 'checkpoint_payload',
372+
type: AdminForthDataTypes.JSON,
373+
showIn: {
374+
list: false,
375+
},
376+
},
377+
{
378+
name: 'metadata_payload',
379+
type: AdminForthDataTypes.JSON,
380+
showIn: {
381+
list: false,
382+
},
383+
},
384+
{
385+
name: 'writes_payload',
386+
type: AdminForthDataTypes.JSON,
387+
showIn: {
388+
list: false,
389+
},
390+
},
391+
{
392+
name: 'schema_version',
393+
type: AdminForthDataTypes.INTEGER,
394+
},
395+
],
396+
} as AdminForthResourceInput;
397+
```
398+
399+
Add a matching table to your schema:
400+
401+
```prisma title='./schema.prisma'
402+
model checkpoints {
403+
id String @id
404+
thread_id String
405+
checkpoint_ns String
406+
checkpoint_id String
407+
parent_checkpoint_id String?
408+
row_kind String
409+
task_id String?
410+
seq Int
411+
created_at DateTime
412+
checkpoint_payload String?
413+
metadata_payload String?
414+
writes_payload String?
415+
schema_version Int
416+
417+
@@index([thread_id, checkpoint_ns, checkpoint_id])
418+
}
419+
```
420+
421+
The payload fields can be stored as strings. The plugin serializes and deserializes checkpoint JSON on its own. The composite index on `(thread_id, checkpoint_ns, checkpoint_id)` is recommended because the checkpointer filters rows by these columns.
422+
423+
Run migration:
424+
425+
```bash
426+
pnpm makemigration --name add-adminforth-agent-checkpoints ; pnpm migrate:local
427+
```
428+
429+
Register the resource in your app:
430+
431+
```ts title="./index.ts"
432+
import checkpoints_resource from './resources/agent_resources/checkpoints.js';
433+
import sessions_resource from './resources/agent_resources/sessions.js';
434+
import turns_resource from './resources/agent_resources/turns.js';
435+
436+
export const admin = new AdminForth({
437+
...
438+
resources: [
439+
...
440+
sessions_resource,
441+
turns_resource,
442+
checkpoints_resource,
443+
],
444+
...
445+
});
446+
```
447+
448+
Then connect it to the plugin:
449+
450+
```ts title="./resources/adminuser.ts"
451+
new AdminForthAgent({
452+
modes: [
453+
...
454+
],
455+
sessionResource: {
456+
resourceId: 'sessions',
457+
idField: 'id',
458+
titleField: 'title',
459+
turnsField: 'turns',
460+
askerIdField: 'asker_id',
461+
createdAtField: 'created_at',
462+
},
463+
turnResource: {
464+
resourceId: 'turns',
465+
idField: 'id',
466+
sessionIdField: 'session_id',
467+
createdAtField: 'created_at',
468+
promptField: 'prompt',
469+
responseField: 'response',
470+
},
471+
checkpointResource: {
472+
resourceId: 'checkpoints',
473+
idField: 'id',
474+
threadIdField: 'thread_id',
475+
checkpointNamespaceField: 'checkpoint_ns',
476+
checkpointIdField: 'checkpoint_id',
477+
parentCheckpointIdField: 'parent_checkpoint_id',
478+
rowKindField: 'row_kind',
479+
taskIdField: 'task_id',
480+
sequenceField: 'seq',
481+
createdAtField: 'created_at',
482+
checkpointPayloadField: 'checkpoint_payload',
483+
metadataPayloadField: 'metadata_payload',
484+
writesPayloadField: 'writes_payload',
485+
schemaVersionField: 'schema_version',
486+
},
487+
});
488+
```
489+
490+
If your existing checkpoint table already uses different column names, keep your schema and only change the field mapping in `checkpointResource`.
491+
271492
## Reverse proxy and CDN configuration for streaming
272493

273494
The agent streams responses from `<baseURL>/adminapi/v1/agent/response` using server-sent events, where `<baseURL>` is your AdminForth base path or an empty string when deployed at the domain root. If your proxy buffers responses, the UI will receive the answer only after generation is finished.

adminforth/documentation/src/pages/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import HomepageFeatures from '@site/src/components/HomepageFeatures';
66
import Heading from '@theme/Heading';
77
import styles from './index.module.css';
88

9-
const LIVE_DEMO_IFRAME_URL = `https://demo.adminforth.dev/overview?autologin=${encodeURIComponent('demo@adminfoth.dev:demo')}`;
9+
const LIVE_DEMO_IFRAME_URL = `https://demo.adminforth.dev/overview?autologin=${encodeURIComponent('demo@adminfoth.dev:demo')}&embedZoom=0.7`;
1010

1111

1212
const images = [
@@ -140,7 +140,7 @@ function HomepageHeader() {
140140
</Heading>
141141
<p className="hero__subtitle">{siteConfig.tagline}</p>
142142

143-
<div class="heroRow">
143+
<div className="heroRow">
144144
<div className={styles.buttons}>
145145
<Link
146146
className="button button--secondary button--outline button--lg"

adminforth/modules/configValidator.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {
2-
AdminForthConfig,
3-
AdminForthResource,
4-
IAdminForth, IConfigValidator,
1+
import {
2+
AdminForthConfig,
3+
AdminForthResource,
4+
IAdminForth, IConfigValidator,
55
AdminForthBulkAction,
6+
AdminForthActionInput,
67
AdminForthInputConfig,
78
AdminForthConfigCustomization,
89
AdminForthResourceInput,
@@ -388,7 +389,7 @@ export default class ConfigValidator implements IConfigValidator {
388389
});
389390
}
390391

391-
validateAndNormalizeCustomActions(resInput: AdminForthResourceInput, res: Partial<AdminForthResource>, errors: string[]): any[] {
392+
validateAndNormalizeCustomActions(resInput: AdminForthResourceInput, res: Partial<AdminForthResource>, errors: string[]): AdminForthActionInput[] {
392393
if (!resInput.options?.actions) {
393394
return [];
394395
}
@@ -430,13 +431,18 @@ export default class ConfigValidator implements IConfigValidator {
430431
action.showIn.showThreeDotsMenu = action.showIn.showThreeDotsMenu ?? false;
431432
}
432433

434+
if (typeof action.allowed === 'boolean') {
435+
const val = action.allowed;
436+
action.allowed = () => val;
437+
}
438+
433439
const shownInNonBulk = action.showIn.list || action.showIn.listThreeDotsMenu || action.showIn.showButton || action.showIn.showThreeDotsMenu;
434440
if (shownInNonBulk && !action.action && !action.url) {
435441
errors.push(`Resource "${res.resourceId}" action "${action.name}" has showIn enabled for non-bulk locations (list, listThreeDotsMenu, showButton, showThreeDotsMenu) but has no "action" or "url" handler. Either add an "action" handler or set those showIn flags to false.`);
436442
}
437443
});
438444

439-
return actions;
445+
return actions as AdminForthActionInput[];
440446
}
441447

442448
validateAndNormalizeResources(errors: string[], warnings: string[]): AdminForthResource[] {

0 commit comments

Comments
 (0)