Skip to content

Commit 22d1f65

Browse files
committed
feat: add global plugins
1 parent 5c26211 commit 22d1f65

6 files changed

Lines changed: 138 additions & 8 deletions

File tree

adminforth/basePlugin.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
2020
resourceConfig: AdminForthResource;
2121
className: string;
2222
activationOrder: number = 0;
23+
pluginsScope: 'resource' | 'global' = 'resource';
2324
shouldHaveSingleInstancePerWholeApp?: () => boolean;
2425

2526
constructor(pluginOptions: any, metaUrl: string) {
@@ -41,14 +42,27 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
4142
}
4243

4344

45+
initializePluginInstanceId = (resourceConfig?: AdminForthResource) => {
46+
const uniqueness = this.instanceUniqueRepresentation(this.pluginOptions);
47+
let seed = '';
48+
if (resourceConfig) {
49+
seed = `af_pl_${this.constructor.name}_${resourceConfig?.resourceId || '_'}_${uniqueness}`;
50+
} else {
51+
seed = `af_pl_${this.constructor.name}_global_${uniqueness}`;
52+
}
53+
this.pluginInstanceId = md5hash(seed);
54+
afLogger.trace({seed, pluginInstanceId: this.pluginInstanceId}, `🪲 AdminForthPlugin.initializePluginInstanceId`);
55+
}
56+
4457
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource, allPluginInstances?: {pi: AdminForthPlugin, resource: AdminForthResource}[]) {
4558
this.resourceConfig = resourceConfig;
46-
const uniqueness = this.instanceUniqueRepresentation(this.pluginOptions);
59+
this.initializePluginInstanceId(resourceConfig);
60+
this.adminforth = adminforth;
61+
}
4762

48-
const seed = `af_pl_${this.constructor.name}_${resourceConfig.resourceId}_${uniqueness}`;
49-
this.pluginInstanceId = md5hash(seed);
50-
afLogger.trace({seed, pluginInstanceId: this.pluginInstanceId}, `🪲 AdminForthPlugin.modifyResourceConfig`);
63+
modifyGlobalConfig(adminforth: IAdminForth) {
5164
this.adminforth = adminforth;
65+
this.initializePluginInstanceId();
5266
}
5367

5468
/**

adminforth/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import CodeInjector from './modules/codeInjector.js';
77
import ExpressServer from './servers/express.js';
88
import OpenApiRegistry from './servers/openapi.js';
99
// import FastifyServer from './servers/fastify.js';
10-
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn, convertPeriodToSeconds, hookResponseError } from './modules/utils.js';
10+
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn, convertPeriodToSeconds, hookResponseError, formatHugePluginError } from './modules/utils.js';
1111
import {
1212
type AdminForthConfig,
1313
type IAdminForth,
@@ -237,13 +237,18 @@ class AdminForth implements IAdminForth {
237237
activatePlugins() {
238238
afLogger.trace('🔌🔌🔌 Activating plugins');
239239
const allPluginInstances = [];
240+
const globalPlugins = this.config.globalPlugins || [];
240241
for (let resource of this.config.resources) {
241242
afLogger.trace(`🔌 Checking plugins for resource: ${resource.resourceId}`);
242243
for (let pluginInstance of resource.plugins || []) {
243244
afLogger.trace(`🔌 Found plugin: ${pluginInstance.constructor.name} for resource ${resource.resourceId}`);
245+
if (pluginInstance.pluginsScope === 'global') {
246+
throw new Error(formatHugePluginError(`Move plugin ${pluginInstance.constructor.name} to index.ts config.globalPlugins array`));
247+
}
244248
allPluginInstances.push({pi: pluginInstance, resource});
245249
}
246250
}
251+
allPluginInstances.push(...globalPlugins.map((pluginInstance) => ({pi: pluginInstance, resource: null})));
247252
afLogger.trace(`🔌 Total plugins to activate: ${allPluginInstances.length}`);
248253

249254
let activationLoopCounter = 0;
@@ -272,8 +277,12 @@ class AdminForth implements IAdminForth {
272277
unactivatedPlugins.forEach(
273278
({pi: pluginInstance, resource}, index) => {
274279
afLogger.trace(`Activating plugin: ${pluginInstance.constructor.name}`);
275-
afLogger.trace(`🔌 Activating plugin ${index + 1}/${unactivatedPlugins.length}: ${pluginInstance.constructor.name} for resource ${resource.resourceId}`);
276-
pluginInstance.modifyResourceConfig(this, resource, allPluginInstances);
280+
afLogger.trace(`🔌 Activating plugin ${index + 1}/${unactivatedPlugins.length}: ${pluginInstance.constructor.name} for resource ${resource ? resource.resourceId : 'global'}`);
281+
if (pluginInstance.pluginsScope === 'global'){
282+
pluginInstance.modifyGlobalConfig(this);
283+
} else {
284+
pluginInstance.modifyResourceConfig(this, resource, allPluginInstances);
285+
}
277286
afLogger.trace(`🔌 Plugin ${pluginInstance.constructor.name} modifyResourceConfig completed`);
278287

279288
const plugin = this.activatedPlugins.find((p) => p.pluginInstanceId === pluginInstance.pluginInstanceId);

adminforth/modules/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,4 +535,26 @@ export async function cascadeChildrenDelete(resource: AdminForthResource, primar
535535
return { error: hookResponse.error };
536536
}
537537
return null;
538+
}
539+
540+
export function formatHugePluginError(message: string) {
541+
const RED = '\x1b[31m';
542+
const BG = '\x1b[41m';
543+
const WHITE = '\x1b[97m';
544+
const BOLD = '\x1b[1m';
545+
const RESET = '\x1b[0m';
546+
547+
const horizontal = '═'.repeat(100);
548+
549+
return `
550+
${BG}${WHITE}${BOLD}
551+
${horizontal}
552+
${' '.repeat(100)}
553+
║ 🚨 PLUGIN CONFIGURATION ERROR${' '.repeat(69)}
554+
${' '.repeat(100)}
555+
${message.padEnd(98)}
556+
${' '.repeat(100)}
557+
${horizontal}
558+
${RESET}
559+
`;
538560
}

adminforth/types/Back.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,7 @@ export interface IAdminForthPlugin {
574574
pluginOptions: any;
575575
resourceConfig: AdminForthResource;
576576
className: string;
577+
pluginsScope: 'resource' | 'global';
577578

578579
/**
579580
* Before activating all plugins are sorted by this number and then activated in order.
@@ -591,7 +592,15 @@ export interface IAdminForthPlugin {
591592
* @param adminforth Instance of IAdminForth
592593
* @param resourceConfig Resource configuration object which will be modified by plugin
593594
*/
594-
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource, allPluginInstances?: {pi: IAdminForthPlugin, resource: AdminForthResource}[]): void;
595+
modifyResourceConfig?(adminforth: IAdminForth, resourceConfig: AdminForthResource, allPluginInstances?: {pi: IAdminForthPlugin, resource: AdminForthResource}[]): void;
596+
597+
598+
/**
599+
* This method is used for plugins, applied in global scope (pluginsScope = 'global')
600+
* @param adminforth Instance of IAdminForth
601+
*/
602+
modifyGlobalConfig?(adminforth: IAdminForth): void;
603+
595604
componentPath(componentFile: string): string;
596605

597606
/**
@@ -1745,6 +1754,10 @@ export interface AdminForthInputConfig {
17451754
*/
17461755
componentsToExplicitRegister?: AdminForthComponentDeclarationFull[]
17471756

1757+
/**
1758+
* List of plugins that should be applied in global scope.
1759+
*/
1760+
globalPlugins?: Array<IAdminForthPlugin>,
17481761
}
17491762

17501763

dev-demo/globalPlugins.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import CompletionAdapterOpenAIResponses from '../adapters/adminforth-completion-adapter-openai-responses/index.js';
2+
import AdminForthAgent from '../plugins/adminforth-agent/index.js';
3+
import AdminForthPlugin from '../adminforth/basePlugin.js';
4+
5+
const OVH_AI_ENDPOINTS_BASE_URL = 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1';
6+
const ovhAiEndpointsAccessToken = process.env.OVH_AI_ENDPOINTS_ACCESS_TOKEN;
7+
const openAiResponsesApiKey = ovhAiEndpointsAccessToken || process.env.OPENAI_API_KEY;
8+
const usesOvhAiEndpoints = Boolean(ovhAiEndpointsAccessToken);
9+
10+
function createAgentCompletionAdapter(
11+
model: string,
12+
effort: 'low' | 'medium' | 'xhigh',
13+
) {
14+
return new CompletionAdapterOpenAIResponses({
15+
openAiApiKey: openAiResponsesApiKey as string,
16+
baseUrl: usesOvhAiEndpoints ? OVH_AI_ENDPOINTS_BASE_URL : undefined,
17+
model: usesOvhAiEndpoints ? 'gpt-oss-120b' : model,
18+
extraRequestBodyParameters: {
19+
...(usesOvhAiEndpoints ? { store: false } : {}),
20+
reasoning: {
21+
effort,
22+
},
23+
},
24+
});
25+
}
26+
27+
export const globalPlugins = [
28+
new AdminForthAgent({
29+
placeholderMessages: async ({ adminUser, httpExtra }) => {
30+
return [
31+
"What is a cars count in SQLite",
32+
"Build average car price by days chart in SQLite",
33+
]
34+
},
35+
modes: [
36+
{
37+
name: 'Balanced',
38+
completionAdapter: createAgentCompletionAdapter('gpt-5.4-mini', 'medium'),
39+
},
40+
{
41+
name: 'Fast',
42+
completionAdapter: createAgentCompletionAdapter('gpt-5.4-mini', 'low'),
43+
},
44+
{
45+
name: 'Smart Thinking',
46+
completionAdapter: createAgentCompletionAdapter('gpt-5.4', 'xhigh'),
47+
},
48+
],
49+
maxTokens: 10000,
50+
reasoning: 'none',
51+
sessionResource: {
52+
resourceId: 'sessions',
53+
idField: 'id',
54+
titleField: 'title',
55+
turnsField: 'turns',
56+
askerIdField: 'asker_id',
57+
createdAtField: 'created_at',
58+
},
59+
turnResource: {
60+
resourceId: 'turns',
61+
idField: 'id',
62+
sessionIdField: 'session_id',
63+
createdAtField: 'created_at',
64+
promptField: 'prompt',
65+
responseField: 'response',
66+
debugField: 'dubbug',
67+
},
68+
}),
69+
];

dev-demo/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import carsDescriptionImage from './resources/cars_description_image.js';
3131
import translations from "./resources/translations.js";
3232
import { logger } from '../adminforth/modules/logger.js';
3333

34+
import { globalPlugins } from './globalPlugins.js';
35+
3436
const ADMIN_BASE_URL = '';
3537

3638
export const admin = new AdminForth({
@@ -229,6 +231,7 @@ export const admin = new AdminForth({
229231
resourceId: 'turns',
230232
}
231233
],
234+
globalPlugins: globalPlugins,
232235
});
233236

234237
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {

0 commit comments

Comments
 (0)