Skip to content

Commit 2f299e9

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents 294a655 + da416f7 commit 2f299e9

15 files changed

Lines changed: 428 additions & 143 deletions

File tree

adminforth/dataConnectors/baseConnector.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ type AdminForthFilterNormalizationResult = {
2222
normalizedFilters?: AdminForthFilterInput;
2323
};
2424

25+
async function publishShowPageUpdate(resource: AdminForthResource, recordId: string, updates: Record<string, any>) {
26+
await global.adminforth.websocket.publish(`/showPage/${resource.resourceId}/${String(recordId)}`, {
27+
resourceId: resource.resourceId,
28+
recordId,
29+
updates,
30+
});
31+
}
32+
2533

2634
export default class AdminForthBaseConnector implements IAdminForthDataSourceConnectorBase {
2735

@@ -578,6 +586,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
578586
afLogger.trace(`🪲✏️ updating record id:${recordId}, values: ${JSON.stringify(recordWithOriginalValues)}`);
579587

580588
await this.updateRecordOriginalValues({ resource, recordId, newValues: recordWithOriginalValues });
589+
await publishShowPageUpdate(resource, recordId, newValues);
581590

582591
return { ok: true };
583592
}

adminforth/documentation/docs/tutorial/03-Customization/16-websocket.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ On server you can publish data to the topic by calling
2020
admin.websocket.publish('/topic-name', {some: 'data'});
2121
```
2222

23+
If you need to unsubscribe from a whole family of topics, for example when route changes can leave old dynamic subscriptions behind, you can use `unsubscribeByPrefix`:
24+
25+
```javascript
26+
import websocket from '@/websocket';
27+
28+
websocket.unsubscribeByPrefix('/topic-name/');
29+
```
30+
31+
This will unsubscribe from all topics whose name starts with the prefix.
32+
33+
It is useful for dynamic topics like `/topic-name/<resourceId>/<recordId>` where a stale subscription can update the wrong page if component unmount does not happen exactly when you expect.
34+
2335
Let's consider a real-world example.
2436

2537
## Usage example

adminforth/documentation/docs/tutorial/05-Adapters/05-ai-completion-adapters.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,25 @@ new CompletionAdapterOpenAIResponses({
134134
openAiApiKey: process.env.OVH_AI_ENDPOINTS_ACCESS_TOKEN as string,
135135
baseUrl: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1',
136136
model: 'gpt-oss-20b',
137+
useComplitionApi: true,
137138
extraRequestBodyParameters: {
138139
store: false,
139140
},
140141
}),
141142
```
142143

144+
For LangChain agent mode, `useComplitionApi` controls which provider API is used:
145+
146+
- `false` uses the OpenAI `responses` API
147+
- `true` uses the Chat Completions API
148+
149+
If `useComplitionApi` is omitted, the adapter keeps the current default behavior:
150+
151+
- official OpenAI uses the `responses` API
152+
- custom `baseUrl` providers use the Chat Completions API
153+
154+
OVH AI Endpoints still does not fully support the `responses` API, so `useComplitionApi: false` may work unstably there.
155+
143156

144157

145158
## Google Gemini Completion Adapter
@@ -179,4 +192,4 @@ new CompletionAdapterGoogleGemini({
179192
responseMimeType: 'application/json',
180193
},
181194
}),
182-
```
195+
```

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

Lines changed: 197 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
title: Agent
33
slug: /tutorial/Plugins/agent
4+
sidebar_position: 0
45
---
56

67
> TODO: this plugin tutorial is in progress, some information might be missing, we are actively working on it now. If you have any questions regarding this plugin, please reach out to us in GitHub issues
@@ -282,6 +283,202 @@ Each item in `modes` defines a user-selectable preset in the chat UI. The select
282283

283284
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.
284285

286+
# Using with self-hosted models
287+
288+
`CompletionAdapterOpenAIResponses` when works with agent plugin, under the hood uses the LangChain internal proxy called `OpenAIChat` (in LangChain they call it "provider"). This proxy is capable with a fresh versions of OpenAI-compatible Responses APIs, for example [self-hosted latest versions of vLLM installations](https://devforth.io/insights/self-hosted-gpt-real-response-time-token-throughput-and-cost-on-l4-l40s-and-h100-for-gpt-oss-20b/)
289+
To use them, just point the adapter to your local vLLM server:
290+
291+
292+
```ts
293+
completionAdapter: new CompletionAdapterOpenAIResponses({
294+
openAiApiKey: process.env.MY_API_KEY as string, // if you use authorization
295+
baseUrl: 'http://my_local_vllm_server:8000/v1',
296+
model: 'gpt-oss-120b',
297+
})
298+
```
299+
300+
However some of 3rd party providers might serve outdated vLLM and still don't fully support the Responses API needed for langchain internal implmentation, for example [OVH AI Endpoints](https://www.ovhcloud.com/en/public-cloud/ai-endpoints/) in Responses mode still don't play well with langchain proxy (25 Apr 2026)
301+
302+
In that case you can try to use the OpenAI Complition API mode of the plugin, which is less efficient but more compatible with older APIs, you can force Chat Completions API mode with `useComplitionApi: true`:
303+
304+
```ts
305+
completionAdapter: new CompletionAdapterOpenAIResponses({
306+
openAiApiKey: process.env.OVH_AI_ENDPOINTS_ACCESS_TOKEN as string,
307+
baseUrl: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1',
308+
model: 'gpt-oss-120b',
309+
useComplitionApi: true,
310+
})
311+
```
312+
313+
OVH AI Endpoints still does not fully support the OpenAI `responses` API, so `useComplitionApi: false` may work unstably there.
314+
315+
316+
317+
318+
## Writing own skills
319+
320+
You can write own skills by following [SKILL.md Agennt Skills](https://agentskills.io/)
321+
322+
For example we will write a skill which can summarizes backoffice data stats like record counts by resources and send it to user email.
323+
324+
It may handle prompts like
325+
326+
```
327+
Please send record counts for all resources to my email
328+
```
329+
330+
Or:
331+
332+
```
333+
Please send record counts to all admin users
334+
```
335+
336+
### Writing custom tools
337+
338+
To define a custom tool you should simply define an express API route in your app using `admin.express.withSchema` wrapper which makes the route available for the agent (clear and predictable schema is a crucial part of making the tool work well).
339+
340+
If you are using `admin.express.authorize` wrapper for authorization, adminuser will be injected atomatically from user which sits on the surface and controls the agent. In other words all permissions and access rights of the agent are defined by the admin user which is controlling this agent. At the same time all actions done by agent are automatically attributed in the audit log to the admin user which is controlling the agent.
341+
342+
This example uses the same email adapter pattern shown in the Email Invite and Email Password Reset plugins. The transport below uses Mailgun only to keep the snippet short; you can replace it with SES or any other adapter from [List of adapters](/docs/tutorial/ListOfAdapters/).
343+
344+
```ts title="./api.ts"
345+
import type { Express, Response } from 'express';
346+
import { Filters, type IAdminForth, type IAdminUserExpressRequest } from 'adminforth';
347+
import * as z from 'zod';
348+
import EmailAdapterMailgun from '@adminforth/email-adapter-mailgun';
349+
350+
const agentEmailAdapter = new EmailAdapterMailgun({
351+
apiKey: process.env.MAILGUN_API_KEY as string,
352+
domain: process.env.MAILGUN_DOMAIN as string,
353+
baseUrl: process.env.MAILGUN_REGION_URL || 'api.mailgun.net',
354+
});
355+
const agentSendFrom = process.env.AGENT_SEND_FROM_EMAIL as string;
356+
357+
function escapeHtml(value: string) {
358+
return value
359+
.replace(/&/g, '&amp;')
360+
.replace(/</g, '&lt;')
361+
.replace(/>/g, '&gt;');
362+
}
363+
364+
function renderEmailHtml(body: string) {
365+
return `<html><body><pre style="font-family: sans-serif; white-space: pre-wrap;">${escapeHtml(body)}</pre></body></html>`;
366+
}
367+
368+
export function initApi(app: Express, admin: IAdminForth) {
369+
app.post('/send_email_to_user',
370+
admin.express.withSchema(
371+
{
372+
description: 'Send an email to one AdminForth user by id. Use this after the user row is resolved.',
373+
request: z.object({
374+
userId: z.string(),
375+
subject: z.string().min(1),
376+
body: z.string().min(1),
377+
}),
378+
response: z.object({
379+
ok: z.boolean(),
380+
email: z.string().email().optional(),
381+
error: z.string().optional(),
382+
}),
383+
},
384+
admin.express.authorize(
385+
async (req: IAdminUserExpressRequest, res: Response) => {
386+
const { userId, subject, body } = req.body as {
387+
userId: string;
388+
subject: string;
389+
body: string;
390+
};
391+
392+
await agentEmailAdapter.validate();
393+
394+
const user = await admin.resource('adminuser').get(
395+
Filters.EQ('id', userId),
396+
);
397+
398+
if (!user || typeof user.email !== 'string' || !user.email) {
399+
res.status(404).json({ ok: false, error: 'User not found' });
400+
return;
401+
}
402+
403+
const result = await agentEmailAdapter.sendEmail(
404+
agentSendFrom,
405+
user.email,
406+
body,
407+
renderEmailHtml(body),
408+
subject,
409+
);
410+
411+
if (!result.ok) {
412+
res.status(500).json({ ok: false, error: result.error ?? 'Failed to send email' });
413+
return;
414+
}
415+
416+
res.json({ ok: true, email: user.email });
417+
}
418+
)
419+
)
420+
);
421+
}
422+
```
423+
424+
## Define skills instructions
425+
426+
Custom skills live in your app's custom directory. The agent loads project skills from:
427+
428+
- `custom/skills/<skill_name>/SKILL.md`
429+
430+
Also it can load skills from plugins, it allows other plugins to expose their own skills and tools:
431+
432+
- `custom/plugins/adminforth-agent/skills/<skill_name>/SKILL.md`
433+
434+
Each skill needs YAML frontmatter with `name` and `description`. The `description` is the discovery surface, so include the phrases the admin is likely to type in chat. Skills are not loaded automcatically, agent loads them
435+
on demand once understands that `description` of the skill matches the user intent, so keep the description clear and concise.
436+
437+
Tools are also not loaded automatically, the agent loads them only if they are mentioned in the skill instructions. Then agent calls `fetch_tool_schema` meta tool to load actual schema of your custom tool.
438+
439+
Tool names are derived from route paths. If you want the tool name to be exactly `send_email_to_user`, register the route as `/send_email_to_user`.
440+
441+
The example below creates a minimal user skill which:
442+
443+
- resolves a user row
444+
- reads the `total` count for each resource with `get_resource_data`
445+
- sends the final report by email with `send_email_to_user`
446+
447+
Create the skill in your app custom folder:
448+
449+
```md title="./custom/skills/email_resource_counts/SKILL.md"
450+
---
451+
name: email_resource_counts
452+
description: Email record counts for each AdminForth resource to a user. Use when the user asks to send resource counts or all-record statistics by email.
453+
---
454+
455+
# Involved tools
456+
457+
Use `send_email_to_user` to send the final report after you have one exact target user row.
458+
459+
# Instructions
460+
461+
- For each resource in system use fetch data default skill to collect total count of records in each resource.
462+
- Create html report in format `Resource Label (resourceId): count` for each resource on a new line, sort resources by count in descending order.
463+
- Use modern, stylish but compatible html formatting in the email.
464+
- Call `send_email_to_user` with the resolved user primary key, the final subject, and the final plain text body.
465+
- After the tool succeeds, tell the user the email was sent and include a short summary in chat.
466+
```
467+
468+
This way allows to extend your agent with literally any custom instructions and tools and make it do complex tasks related to your backoffice data and operations.
469+
470+
471+
## Standard skills
472+
473+
The plugin ships with bundled skills from `plugins/adminforth-agent/custom/skills/`. These are available out of the box and can be combined with your own custom skills.
474+
475+
| Folder | Skill name | Description |
476+
| --- | --- | --- |
477+
| `analyze_data` | `analyze_data` | Analyze AdminForth resource data, summarize trends, and create charts from fetched rows. Prefer server-side aggregation when possible and return Vega-Lite specs for charts. |
478+
| `fetch_data` | `fetch_data` | Fetch one or more exact records with filters after inspecting the resource schema. This skill is for finding rows, not for aggregations. |
479+
| `mutate_data` | `mutate_data` | Create, update, delete, or run actions on records. Before any mutation it must show the exact target row and ask the user for confirmation. |
480+
481+
285482
## Persistent checkpointer
286483

287484
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.
@@ -545,17 +742,3 @@ services:
545742
![alt text](image-6.png)
546743

547744

548-
## Custom skills and tools
549-
550-
551-
Place you skills in `custom/skills/<skill_name>/SKILL.md` file. The plugin will pick them up automatically and make available in agent's toolbox.
552-
553-
554-
To define custom tools, create api endpoints, prefer `admin.express.withSchema(...)` from [Custom Pages / API docs](/docs/tutorial/Customization/customPages/). That exposes machine-readable request and response schemas the agent can use.
555-
556-
In skills markdown file, merge which tool exactlu agent should load.
557-
558-
559-
Skill example:
560-
561-
// TODO

adminforth/documentation/docs/tutorial/08-Plugins/01-AuditLog.md renamed to adminforth/documentation/docs/tutorial/08-Plugins/04-AuditLog.md

File renamed without changes.

adminforth/documentation/docs/tutorial/08-Plugins/14-markdown.md renamed to adminforth/documentation/docs/tutorial/08-Plugins/06-markdown.md

File renamed without changes.

adminforth/documentation/docs/tutorial/08-Plugins/06-text-complete.md renamed to adminforth/documentation/docs/tutorial/08-Plugins/14-text-complete.md

File renamed without changes.

adminforth/documentation/docs/tutorial/08-Plugins/04-RichEditor.md renamed to adminforth/documentation/docs/tutorial/08-Plugins/26-RichEditor.md

File renamed without changes.

adminforth/modules/operationalResource.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ export default class OperationalResource implements IOperationalResource {
9191
}
9292

9393
async update(primaryKey: any, record: any): Promise<any> {
94+
if (Object.keys(record).length === 0) {
95+
return { ok: true };
96+
}
97+
9498
return await this.dataConnector.updateRecord({
9599
resource: this.resourceConfig,
96100
recordId: primaryKey,

adminforth/package.json

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
11
{
22
"name": "adminforth",
33
"version": "1.6.2",
4-
"description": "OpenSource Vue3 powered forth-generation admin panel",
4+
"description": "OpenSource Agent-Native forth-generation admin panel",
5+
"keywords": [
6+
"adminforth",
7+
"agent",
8+
"ai-agent",
9+
"skill.md",
10+
"tool-calling",
11+
"agent-native",
12+
"ai-native",
13+
"ai-first",
14+
"admin-panel",
15+
"backoffice",
16+
"crud",
17+
"nodejs",
18+
"typescript",
19+
"vue",
20+
"tailwind"
21+
],
522
"main": "dist/index.js",
623
"module": "dist/index.js",
724
"types": "dist/index.d.ts",
@@ -63,7 +80,7 @@
6380
"require": "./dist/index.js"
6481
}
6582
},
66-
"author": "devforth.io",
83+
"author": "DevForth (https://devforth.io)",
6784
"license": "MIT",
6885
"type": "module",
6986
"dependencies": {

0 commit comments

Comments
 (0)