adminizer 5.0.0-build.10 → 5.0.0-build.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/assets/add-BhTWhSNx.js +1 -0
  2. package/assets/add-group-BEOEkMGK.js +1 -0
  3. package/assets/add-user-MW-fSILU.js +1 -0
  4. package/assets/ai-assistant/agent.es.js +27121 -0
  5. package/assets/app-DcWwG3m4.css +1 -0
  6. package/assets/app.js +2 -2
  7. package/assets/{catalog-BGkKJyhR.js → catalog-D-lFVV9U.js} +2 -2
  8. package/assets/{dashboard-CvSStqLV.js → dashboard-Cmj4Df_C.js} +2 -2
  9. package/assets/history-BXfeZHTD.js +1 -0
  10. package/assets/{list-cNVyzYi8.js → list-Dp8F_hor.js} +2 -2
  11. package/assets/manifest.json +31 -31
  12. package/assets/module-Q-1AzZH9.js +1 -0
  13. package/assets/notification-CSeMEsPn.js +20 -0
  14. package/assets/user-filters-list-Do6Dis5D.js +1 -0
  15. package/assets/welcome-D3dg746Y.js +1 -0
  16. package/assets/with-app-layout-Bjo8B9zO.js +1 -0
  17. package/controllers/ai/AiAgentController.d.ts +66 -0
  18. package/controllers/ai/AiAgentController.js +435 -0
  19. package/controllers/ai/AiAssistantController.d.ts +0 -4
  20. package/controllers/ai/AiAssistantController.js +0 -75
  21. package/helpers/inertiaMenuHelper.d.ts +0 -1
  22. package/helpers/inertiaMenuHelper.js +3 -26
  23. package/helpers/modelResourceHelper.d.ts +9 -0
  24. package/helpers/modelResourceHelper.js +39 -0
  25. package/helpers/navigationAccessHelper.d.ts +13 -0
  26. package/helpers/navigationAccessHelper.js +40 -0
  27. package/index.d.ts +8 -0
  28. package/index.js +8 -0
  29. package/interfaces/types.d.ts +151 -0
  30. package/lib/Adminizer.d.ts +6 -0
  31. package/lib/Adminizer.js +9 -0
  32. package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
  33. package/lib/admin-links/AdminLinkHandler.js +175 -0
  34. package/lib/ai-assistant/AbstractAiConversationHistoryService.d.ts +39 -0
  35. package/lib/ai-assistant/AbstractAiConversationHistoryService.js +98 -0
  36. package/lib/ai-assistant/AbstractAiModelService.d.ts +139 -2
  37. package/lib/ai-assistant/AbstractAiModelService.js +154 -1
  38. package/lib/ai-assistant/AiAgentRunManager.d.ts +35 -0
  39. package/lib/ai-assistant/AiAgentRunManager.js +109 -0
  40. package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +58 -0
  41. package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +87 -0
  42. package/lib/ai-assistant/AiAssistantHandler.d.ts +1 -0
  43. package/lib/ai-assistant/AiAssistantHandler.js +4 -0
  44. package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
  45. package/lib/ai-assistant/AiAssistantUiMethodHandler.js +138 -0
  46. package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
  47. package/lib/ai-assistant/builtinAgentSkills.js +256 -0
  48. package/lib/ai-assistant/jsonSafe.d.ts +13 -0
  49. package/lib/ai-assistant/jsonSafe.js +81 -0
  50. package/lib/app-manager/AdminizerApp.d.ts +24 -0
  51. package/lib/app-manager/AppManager.js +39 -42
  52. package/package.json +27 -20
  53. package/system/Router.js +18 -3
  54. package/assets/add-BtctcXXV.js +0 -1
  55. package/assets/add-group-C0LgrR0Z.js +0 -1
  56. package/assets/add-user-CALUedTL.js +0 -1
  57. package/assets/app-DNXiApCk.css +0 -1
  58. package/assets/app-layout-CTavO9pF.js +0 -1
  59. package/assets/history-RlFpGr4e.js +0 -1
  60. package/assets/module-B_i1LQ3L.js +0 -1
  61. package/assets/notification-DXP7ACui.js +0 -20
  62. package/assets/user-filters-list-DWE5MBBP.js +0 -1
  63. package/assets/welcome-B6-atbAF.js +0 -1
package/index.d.ts CHANGED
@@ -43,7 +43,15 @@ export * from "./lib/helper/jwt.js";
43
43
  export * from "./lib/notifications/AbstractNotificationService.js";
44
44
  export * from "./lib/POWCaptcha.js";
45
45
  export * from "./lib/ai-assistant/AbstractAiModelService.js";
46
+ export * from "./lib/ai-assistant/AbstractAiConversationHistoryService.js";
46
47
  export * from "./lib/ai-assistant/AiAssistantHandler.js";
48
+ export * from "./lib/ai-assistant/AiAgentRunManager.js";
49
+ export * from "./lib/ai-assistant/jsonSafe.js";
50
+ export * from "./lib/ai-assistant/AiAssistantUiMethodHandler.js";
51
+ export * from "./lib/ai-assistant/AiAssistantAgentSkillHandler.js";
52
+ export * from "./lib/admin-links/AdminLinkHandler.js";
53
+ export * from "./helpers/modelResourceHelper.js";
54
+ export * from "./helpers/navigationAccessHelper.js";
47
55
  export * from "./lib/DataAccessor.js";
48
56
  export * from "./lib/history-actions/AbstractHistoryAdapter.js";
49
57
  export * from "./lib/filters/CustomFilterHandler.js";
package/index.js CHANGED
@@ -43,7 +43,15 @@ export * from "./lib/helper/jwt.js";
43
43
  export * from "./lib/notifications/AbstractNotificationService.js";
44
44
  export * from "./lib/POWCaptcha.js";
45
45
  export * from "./lib/ai-assistant/AbstractAiModelService.js";
46
+ export * from "./lib/ai-assistant/AbstractAiConversationHistoryService.js";
46
47
  export * from "./lib/ai-assistant/AiAssistantHandler.js";
48
+ export * from "./lib/ai-assistant/AiAgentRunManager.js";
49
+ export * from "./lib/ai-assistant/jsonSafe.js";
50
+ export * from "./lib/ai-assistant/AiAssistantUiMethodHandler.js";
51
+ export * from "./lib/ai-assistant/AiAssistantAgentSkillHandler.js";
52
+ export * from "./lib/admin-links/AdminLinkHandler.js";
53
+ export * from "./helpers/modelResourceHelper.js";
54
+ export * from "./helpers/navigationAccessHelper.js";
47
55
  export * from "./lib/DataAccessor.js";
48
56
  export * from "./lib/history-actions/AbstractHistoryAdapter.js";
49
57
  export * from "./lib/filters/CustomFilterHandler.js";
@@ -70,3 +70,154 @@ export interface AiAssistantModelInfo {
70
70
  name: string;
71
71
  description?: string;
72
72
  }
73
+ /**
74
+ * Streaming agent contract.
75
+ *
76
+ * A model service that only implements `generateReply` stays a plain chat
77
+ * model; services that also implement the optional agent members below are
78
+ * driven by the streaming UI (tool calls, reasoning, attachments, per-user
79
+ * server-side session). See AbstractAiModelService.
80
+ */
81
+ /** One SSE frame of a run. `type` drives how the UI folds it into the thread. */
82
+ export type AiAgentStreamEvent = {
83
+ type: 'text.delta' | 'text.done' | 'reasoning.delta' | 'reasoning.done' | 'tool.start' | 'tool.done' | 'tool.error' | 'step.done' | 'turn.done' | 'done' | 'error' | string;
84
+ [key: string]: unknown;
85
+ };
86
+ export type AiAgentPublish = (event: AiAgentStreamEvent) => void;
87
+ export interface AiAgentTokenUsage {
88
+ inputTokens?: number;
89
+ outputTokens?: number;
90
+ totalTokens?: number;
91
+ }
92
+ /** One selectable LLM inside an agent service. */
93
+ export interface AiAgentModelOption {
94
+ id: string;
95
+ contextWindow: number | null;
96
+ maxOutputTokens: number | null;
97
+ vision: boolean;
98
+ /** Relative spend multiplier (×1 = median-priced model); null if unknown. */
99
+ costCoefficient: number | null;
100
+ [key: string]: unknown;
101
+ }
102
+ /** State of the user's server-side agent session. */
103
+ export interface AiAgentSessionMeta {
104
+ model: string;
105
+ contextWindow: number;
106
+ vision: boolean;
107
+ turns: number;
108
+ contextTokens: number;
109
+ totalUsage: AiAgentTokenUsage;
110
+ [key: string]: unknown;
111
+ }
112
+ /**
113
+ * Whether the service can talk to its LLM yet. Services that need no
114
+ * provisioning simply omit `getConnectionStatus()` and are treated as ready.
115
+ */
116
+ export interface AiAgentConnectionStatus {
117
+ state: 'ready' | 'registering' | 'waiting_retry' | 'setup_required' | 'error';
118
+ provider?: string;
119
+ baseUrl?: string;
120
+ source?: string | null;
121
+ nextAttemptAt?: string | null;
122
+ lastError?: string | null;
123
+ expiresAt?: string | null;
124
+ [key: string]: unknown;
125
+ }
126
+ /** States for which an agent can describe its connection screen. */
127
+ export type AiAgentConnectionState = Exclude<AiAgentConnectionStatus['state'], 'ready'>;
128
+ /**
129
+ * Declarative connection screen supplied by an agent service. The shared panel
130
+ * owns rendering, while the agent owns its copy, CTA and which status details
131
+ * are useful to expose.
132
+ */
133
+ export interface AiAgentConnectionScreen {
134
+ title?: string;
135
+ description?: string;
136
+ icon?: 'spinner' | 'bot' | 'error';
137
+ action?: {
138
+ label: string;
139
+ href: string;
140
+ };
141
+ /** Status values the agent wants to render on this screen. */
142
+ details?: Array<{
143
+ field: string;
144
+ label?: string;
145
+ format?: 'text' | 'local-time';
146
+ tone?: 'muted' | 'error';
147
+ }>;
148
+ }
149
+ /** Normalized budget / rate limits of the provider key, when it exposes them. */
150
+ export interface AiAgentLimits {
151
+ provider: string;
152
+ supported: boolean;
153
+ overBudget?: boolean;
154
+ message?: string;
155
+ maxBudgetUsd?: number | null;
156
+ spentUsd?: number | null;
157
+ remainingBudgetUsd?: number | null;
158
+ budgetResetAt?: string | null;
159
+ burstMaxBudgetUsd?: number | null;
160
+ burstSpentUsd?: number | null;
161
+ burstRemainingBudgetUsd?: number | null;
162
+ burstResetAt?: string | null;
163
+ burstDurationLabel?: string | null;
164
+ rpmLimit?: number | null;
165
+ tpmLimit?: number | null;
166
+ fetchedAt?: string;
167
+ [key: string]: unknown;
168
+ }
169
+ /**
170
+ * Copy the panel shows instead of its neutral defaults. `getUiHints(locale)`
171
+ * receives the admin user's locale, so agents can return localized copy.
172
+ */
173
+ export interface AiAgentUiHints {
174
+ title?: string;
175
+ welcomeHint?: string;
176
+ composerPlaceholder?: string;
177
+ suggestions?: string[];
178
+ /** Setting an operator must fill in before the service can connect. */
179
+ setupSetting?: string;
180
+ /** Where to fill it in; rendered as a button on the connection loader. */
181
+ setupUrl?: string;
182
+ /**
183
+ * Per-state pages rendered while this agent is not ready. `default` is
184
+ * used for any state without its own screen.
185
+ */
186
+ connectionScreens?: Partial<Record<AiAgentConnectionState | 'default', AiAgentConnectionScreen>>;
187
+ [key: string]: unknown;
188
+ }
189
+ /** A browser-side capability which an agent may call through `ui.method`. */
190
+ export interface AiAgentUiMethod {
191
+ id: string;
192
+ title: string;
193
+ description: string;
194
+ inputSchema: Record<string, unknown>;
195
+ action: string;
196
+ accessRightsToken?: string;
197
+ }
198
+ /** Declarative UI contract consumed by the shared assistant panel. */
199
+ export interface AiAgentUiSchema extends AiAgentUiHints {
200
+ commands: Array<{
201
+ id: string;
202
+ description?: string;
203
+ }>;
204
+ uiMethods?: AiAgentUiMethod[];
205
+ panels: {
206
+ history: boolean;
207
+ models: boolean;
208
+ limits: boolean;
209
+ };
210
+ }
211
+ /** An uploaded file as handed to the agent service (multer memory storage). */
212
+ export interface AiAgentUploadedFile {
213
+ originalname: string;
214
+ mimetype: string;
215
+ size: number;
216
+ buffer: Buffer;
217
+ }
218
+ /** What the service reports back about a stored upload, if it stores them. */
219
+ export interface AiAgentSavedFile {
220
+ id?: string;
221
+ name?: string;
222
+ [key: string]: unknown;
223
+ }
@@ -27,6 +27,9 @@ import { ControllerHandler } from "./app-manager/ControllerHandler.js";
27
27
  import { AssetHandler } from "./app-manager/AssetHandler.js";
28
28
  import { ConfigLayerHandler } from "./app-manager/ConfigLayerHandler.js";
29
29
  import { AccessRightsHandler } from "./access-rights/AccessRightsHandler.js";
30
+ import { AiAssistantUiMethodHandler } from './ai-assistant/AiAssistantUiMethodHandler.js';
31
+ import { AdminLinkHandler } from './admin-links/AdminLinkHandler.js';
32
+ import { AiAssistantAgentSkillHandler } from './ai-assistant/AiAssistantAgentSkillHandler.js';
30
33
  export declare function createAdminizerLogger(): winston.Logger;
31
34
  export interface EmitAsyncOptions {
32
35
  /**
@@ -54,6 +57,9 @@ export declare class Adminizer {
54
57
  notificationHandler: NotificationHandler;
55
58
  historyHandler: HistoryHandler;
56
59
  private _aiAssistantHandler?;
60
+ aiAssistantUiMethodHandler: AiAssistantUiMethodHandler;
61
+ adminLinkHandler: AdminLinkHandler;
62
+ aiAssistantAgentSkillHandler: AiAssistantAgentSkillHandler;
57
63
  modelHandler: ModelHandler;
58
64
  widgetHandler: WidgetHandler;
59
65
  customFilterHandler: CustomFilterHandler;
package/lib/Adminizer.js CHANGED
@@ -43,6 +43,9 @@ import { ControllerHandler } from "./app-manager/ControllerHandler.js";
43
43
  import { AssetHandler } from "./app-manager/AssetHandler.js";
44
44
  import { ConfigLayerHandler } from "./app-manager/ConfigLayerHandler.js";
45
45
  import { AccessRightsHandler } from "./access-rights/AccessRightsHandler.js";
46
+ import { AiAssistantUiMethodHandler } from './ai-assistant/AiAssistantUiMethodHandler.js';
47
+ import { AdminLinkHandler } from './admin-links/AdminLinkHandler.js';
48
+ import { AiAssistantAgentSkillHandler } from './ai-assistant/AiAssistantAgentSkillHandler.js';
46
49
  const logFilePath = "logs/app.log";
47
50
  function ensureLogDirectory(filePath) {
48
51
  const logDirectory = path.dirname(filePath);
@@ -109,6 +112,9 @@ export class Adminizer {
109
112
  notificationHandler;
110
113
  historyHandler;
111
114
  _aiAssistantHandler;
115
+ aiAssistantUiMethodHandler;
116
+ adminLinkHandler;
117
+ aiAssistantAgentSkillHandler;
112
118
  modelHandler;
113
119
  widgetHandler;
114
120
  customFilterHandler;
@@ -141,6 +147,9 @@ export class Adminizer {
141
147
  this.catalogTemplateComponentHandler = new CatalogTemplateComponentHandler();
142
148
  this.mediaManagerHandler = new MediaManagerHandler();
143
149
  this.feedbackHandler = new FeedbackHandler(this);
150
+ this.aiAssistantUiMethodHandler = new AiAssistantUiMethodHandler(this);
151
+ this.adminLinkHandler = new AdminLinkHandler(this);
152
+ this.aiAssistantAgentSkillHandler = new AiAssistantAgentSkillHandler(this);
144
153
  this.controllerHandler = new ControllerHandler(this);
145
154
  this.assetHandler = new AssetHandler(this);
146
155
  this.configLayerHandler = new ConfigLayerHandler(this);
@@ -0,0 +1,72 @@
1
+ import type { User } from '../../models/User.js';
2
+ import type { Adminizer } from '../Adminizer.js';
3
+ export interface AdminLink {
4
+ id: string;
5
+ type: string;
6
+ name: string;
7
+ link: string;
8
+ title?: string;
9
+ section?: string;
10
+ accessRightsToken?: string;
11
+ }
12
+ export interface ResolvedAdminLink extends AdminLink {
13
+ owner: string;
14
+ title: string;
15
+ }
16
+ /** A placeholder of an admin link template, e.g. `id` in `/model/Test/edit/:id`. */
17
+ export interface AdminLinkTemplateParam {
18
+ name: string;
19
+ description?: string;
20
+ }
21
+ /**
22
+ * A parametrized admin page, e.g. `/admin/model/Test/edit/:id`. Templates are
23
+ * what makes a record page reachable: the concrete URL only exists once the
24
+ * caller knows the record id.
25
+ */
26
+ export interface AdminLinkTemplate {
27
+ id: string;
28
+ title: string;
29
+ /** Absolute admin path with `:param` placeholders. */
30
+ template: string;
31
+ description?: string;
32
+ section?: string;
33
+ accessRightsToken?: string;
34
+ /** Placeholder descriptions; placeholders absent here are still required. */
35
+ params?: AdminLinkTemplateParam[];
36
+ }
37
+ export interface ResolvedAdminLinkTemplate extends AdminLinkTemplate {
38
+ owner: string;
39
+ params: AdminLinkTemplateParam[];
40
+ }
41
+ /** Server-side registry of standalone admin pages contributed by applications. */
42
+ export declare class AdminLinkHandler {
43
+ private readonly adminizer;
44
+ private readonly links;
45
+ private readonly templates;
46
+ constructor(adminizer: Adminizer);
47
+ add(link: AdminLink, owner?: string): string;
48
+ remove(id: string, owner?: string): boolean;
49
+ list(user: User, type?: string): ResolvedAdminLink[];
50
+ resolve(user: User, type: string, name: string): ResolvedAdminLink | undefined;
51
+ addTemplate(template: AdminLinkTemplate, owner?: string): string;
52
+ removeTemplate(id: string, owner?: string): boolean;
53
+ /**
54
+ * Every parametrized page this user may open: model record pages, catalog
55
+ * items, templates registered by apps and any navigation link that carries
56
+ * placeholders of its own.
57
+ */
58
+ listTemplates(user: User): ResolvedAdminLinkTemplate[];
59
+ /**
60
+ * Turns a template id (or its title) plus placeholder values into a real
61
+ * admin URL, rejecting templates this user may not open.
62
+ */
63
+ resolveTemplate(user: User, id: string, params?: Record<string, unknown>): string;
64
+ /** Fills the placeholders of a template with url-encoded values. */
65
+ buildHref(template: ResolvedAdminLinkTemplate, params?: Record<string, unknown>): string;
66
+ /** True for paths that change data and therefore must never be navigated to. */
67
+ isDestructivePath(path: string): boolean;
68
+ /** Templates derived from the current configuration, catalogs and navigation. */
69
+ private builtinTemplates;
70
+ private describeParams;
71
+ private slug;
72
+ }
@@ -0,0 +1,175 @@
1
+ /** Segments no template may point at: opening one mutates data. */
2
+ const DESTRUCTIVE_PATH = /\/model\/[^/]+\/remove(\/|$)/i;
3
+ /** Server-side registry of standalone admin pages contributed by applications. */
4
+ export class AdminLinkHandler {
5
+ adminizer;
6
+ links = new Map();
7
+ templates = new Map();
8
+ constructor(adminizer) {
9
+ this.adminizer = adminizer;
10
+ }
11
+ add(link, owner = 'host') {
12
+ const type = link.type.trim();
13
+ const name = link.name.trim();
14
+ const href = link.link.trim();
15
+ if (!type || !name || !href)
16
+ throw new Error('Admin link requires type, name and link');
17
+ if (!href.startsWith('/'))
18
+ throw new Error(`Admin link "${name}" must use an absolute admin path`);
19
+ const id = `${this.slug(type)}:${this.slug(name)}`;
20
+ if (this.links.has(id))
21
+ throw new Error(`Admin link "${id}" is already registered`);
22
+ this.links.set(id, {
23
+ ...link, id, type, name, link: href, owner,
24
+ title: link.title?.trim() || name,
25
+ section: link.section?.trim() || undefined,
26
+ });
27
+ return id;
28
+ }
29
+ remove(id, owner) {
30
+ const link = this.links.get(id);
31
+ if (!link || (owner && link.owner !== owner))
32
+ return false;
33
+ return this.links.delete(id);
34
+ }
35
+ list(user, type) {
36
+ const wanted = type ? this.slug(type) : null;
37
+ return [...this.links.values()].filter((link) => (!wanted || this.slug(link.type) === wanted)
38
+ && (!link.accessRightsToken || this.adminizer.accessRightsHelper.hasPermission(link.accessRightsToken, user)));
39
+ }
40
+ resolve(user, type, name) {
41
+ const wanted = this.slug(name);
42
+ return this.list(user, type).find((link) => this.slug(link.name) === wanted || this.slug(link.title) === wanted);
43
+ }
44
+ addTemplate(template, owner = 'host') {
45
+ const id = template.id.trim();
46
+ const title = template.title.trim();
47
+ const path = template.template.trim();
48
+ if (!id || !title || !path)
49
+ throw new Error('Admin link template requires id, title and template');
50
+ if (!path.startsWith('/'))
51
+ throw new Error(`Admin link template "${id}" must use an absolute admin path`);
52
+ if (this.templates.has(id))
53
+ throw new Error(`Admin link template "${id}" is already registered`);
54
+ this.templates.set(id, {
55
+ ...template, id, title, template: path, owner,
56
+ description: template.description?.trim() || undefined,
57
+ section: template.section?.trim() || undefined,
58
+ params: this.describeParams(path, template.params),
59
+ });
60
+ return id;
61
+ }
62
+ removeTemplate(id, owner) {
63
+ const template = this.templates.get(id);
64
+ if (!template || (owner && template.owner !== owner))
65
+ return false;
66
+ return this.templates.delete(id);
67
+ }
68
+ /**
69
+ * Every parametrized page this user may open: model record pages, catalog
70
+ * items, templates registered by apps and any navigation link that carries
71
+ * placeholders of its own.
72
+ */
73
+ listTemplates(user) {
74
+ const templates = [];
75
+ const push = (template) => {
76
+ if (DESTRUCTIVE_PATH.test(template.template))
77
+ return;
78
+ if (template.accessRightsToken && !this.adminizer.accessRightsHelper.hasPermission(template.accessRightsToken, user))
79
+ return;
80
+ if (templates.some((existing) => existing.template === template.template || existing.id === template.id))
81
+ return;
82
+ templates.push(template);
83
+ };
84
+ for (const template of this.builtinTemplates(user))
85
+ push(template);
86
+ for (const template of this.templates.values())
87
+ push(template);
88
+ return templates;
89
+ }
90
+ /**
91
+ * Turns a template id (or its title) plus placeholder values into a real
92
+ * admin URL, rejecting templates this user may not open.
93
+ */
94
+ resolveTemplate(user, id, params = {}) {
95
+ const wanted = this.slug(id);
96
+ const available = this.listTemplates(user);
97
+ const template = available.find((candidate) => this.slug(candidate.id) === wanted)
98
+ ?? available.find((candidate) => this.slug(candidate.title) === wanted || this.slug(candidate.template) === wanted);
99
+ if (!template)
100
+ throw new Error(`Admin link template "${id}" is not available`);
101
+ return this.buildHref(template, params);
102
+ }
103
+ /** Fills the placeholders of a template with url-encoded values. */
104
+ buildHref(template, params = {}) {
105
+ return template.template.replace(/:([A-Za-z0-9_]+)/g, (_match, name) => {
106
+ const value = params[name];
107
+ if (value === undefined || value === null || value === '') {
108
+ throw new Error(`Admin link template "${template.id}" requires the "${name}" parameter`);
109
+ }
110
+ return encodeURIComponent(String(value));
111
+ });
112
+ }
113
+ /** True for paths that change data and therefore must never be navigated to. */
114
+ isDestructivePath(path) {
115
+ return DESTRUCTIVE_PATH.test(path);
116
+ }
117
+ /** Templates derived from the current configuration, catalogs and navigation. */
118
+ builtinTemplates(user) {
119
+ const prefix = (this.adminizer.config.routePrefix || '').replace(/\/+$/, '');
120
+ const templates = [];
121
+ const builtin = (id, title, path, accessRightsToken, description, section, params = []) => {
122
+ templates.push({
123
+ id, title, template: path, accessRightsToken, description, section, owner: 'host',
124
+ params: this.describeParams(path, params),
125
+ });
126
+ };
127
+ const recordId = [
128
+ { name: 'id', description: 'Identifier of the record, as returned when reading its data.' },
129
+ ];
130
+ for (const [model, config] of Object.entries(this.adminizer.config.models ?? {})) {
131
+ const title = typeof config === 'object' && config?.title ? config.title : model;
132
+ const section = typeof config === 'object' ? config?.navbar?.section : undefined;
133
+ builtin(`model-${model}-edit`, `${title}: open record`, `${prefix}/model/${model}/edit/:id`, `update-${model}-model`, `Open the edit page of a single ${title} record by its id.`, section, recordId);
134
+ builtin(`model-${model}-add`, `${title}: create record`, `${prefix}/model/${model}/add`, `create-${model}-model`, `Open the create form of ${title}.`, section);
135
+ }
136
+ for (const catalog of this.adminizer.catalogHandler.getAll()) {
137
+ if (!catalog?.slug)
138
+ continue;
139
+ builtin(`catalog-${catalog.slug}-item`, `${catalog.name || catalog.slug}: open item`, `${prefix}/catalog/${catalog.slug}/:id`, `catalog-${catalog.slug}`, `Open a single item of the ${catalog.name || catalog.slug} catalog by its id.`, undefined, recordId);
140
+ }
141
+ // Navigation links may carry placeholders themselves (app pages, custom
142
+ // tools). Such a link cannot be opened as-is, so it is a template.
143
+ const fromNavigation = (item, section) => {
144
+ const path = typeof item?.link === 'string' ? item.link : '';
145
+ if (path.startsWith('/') && /:[A-Za-z0-9_]+/.test(path) && item.title) {
146
+ templates.push({
147
+ id: `link-${this.slug(String(item.id || item.title))}`,
148
+ title: String(item.title),
149
+ template: path,
150
+ section: item.section || section,
151
+ accessRightsToken: item.accessRightsToken || undefined,
152
+ owner: 'host',
153
+ params: this.describeParams(path),
154
+ });
155
+ }
156
+ for (const child of item?.actions ?? item?.subItems ?? [])
157
+ fromNavigation(child, item?.section || section);
158
+ };
159
+ for (const item of this.adminizer.menuHelper.getMenuItems(user))
160
+ fromNavigation(item, item.section);
161
+ for (const link of this.links.values())
162
+ fromNavigation(link, link.section);
163
+ return templates;
164
+ }
165
+ describeParams(path, described = []) {
166
+ const names = [...path.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1]);
167
+ return [...new Set(names)].map((name) => ({
168
+ name,
169
+ description: described.find((param) => param.name === name)?.description,
170
+ }));
171
+ }
172
+ slug(value) {
173
+ return String(value).trim().toLowerCase().replace(/[\s_-]+/g, '-');
174
+ }
175
+ }
@@ -0,0 +1,39 @@
1
+ import type { Adminizer } from '../Adminizer.js';
2
+ import type { AbstractAiModelService } from './AbstractAiModelService.js';
3
+ import type { User } from '../../models/User.js';
4
+ export interface AiConversation {
5
+ id: string;
6
+ title: string;
7
+ createdAt: string;
8
+ updatedAt: string;
9
+ messages: Array<Record<string, unknown>>;
10
+ }
11
+ /**
12
+ * Storage contract for an agent's dialogs. Applications can extend this class
13
+ * to persist conversations in their own database instead of process memory.
14
+ */
15
+ export declare abstract class AbstractAiConversationHistoryService {
16
+ protected agent: AbstractAiModelService;
17
+ protected adminizer: Adminizer;
18
+ initialize(agent: AbstractAiModelService, adminizer: Adminizer): void;
19
+ abstract list(user: User): AiConversation[];
20
+ abstract getActive(user: User): AiConversation;
21
+ abstract create(user: User, title?: string): AiConversation;
22
+ abstract select(user: User, conversationId: string): AiConversation | undefined;
23
+ abstract remove(user: User, conversationId: string): boolean;
24
+ abstract saveActive(user: User, messages: Array<Record<string, unknown>>, title?: string): AiConversation;
25
+ }
26
+ /** Default storage used when an agent does not provide a database-backed one. */
27
+ export declare class InMemoryAiConversationHistoryService extends AbstractAiConversationHistoryService {
28
+ private readonly conversations;
29
+ private readonly activeConversationIds;
30
+ list(user: User): AiConversation[];
31
+ getActive(user: User): AiConversation;
32
+ create(user: User, title?: string): AiConversation;
33
+ select(user: User, conversationId: string): AiConversation | undefined;
34
+ remove(user: User, conversationId: string): boolean;
35
+ saveActive(user: User, messages: Array<Record<string, unknown>>, title?: string): AiConversation;
36
+ private getConversations;
37
+ private getKey;
38
+ private findFirstUserText;
39
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Storage contract for an agent's dialogs. Applications can extend this class
3
+ * to persist conversations in their own database instead of process memory.
4
+ */
5
+ export class AbstractAiConversationHistoryService {
6
+ agent;
7
+ adminizer;
8
+ initialize(agent, adminizer) {
9
+ this.agent = agent;
10
+ this.adminizer = adminizer;
11
+ }
12
+ }
13
+ /** Default storage used when an agent does not provide a database-backed one. */
14
+ export class InMemoryAiConversationHistoryService extends AbstractAiConversationHistoryService {
15
+ conversations = new Map();
16
+ activeConversationIds = new Map();
17
+ list(user) {
18
+ return [...this.getConversations(user)]
19
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
20
+ }
21
+ getActive(user) {
22
+ const key = this.getKey(user);
23
+ const conversations = this.getConversations(user);
24
+ const activeId = this.activeConversationIds.get(key);
25
+ const active = conversations.find((conversation) => conversation.id === activeId);
26
+ if (active)
27
+ return active;
28
+ const conversation = this.create(user);
29
+ this.activeConversationIds.set(key, conversation.id);
30
+ return conversation;
31
+ }
32
+ create(user, title = 'New conversation') {
33
+ const now = new Date().toISOString();
34
+ const conversation = {
35
+ id: `conversation-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
36
+ title,
37
+ createdAt: now,
38
+ updatedAt: now,
39
+ messages: [],
40
+ };
41
+ this.getConversations(user).push(conversation);
42
+ this.activeConversationIds.set(this.getKey(user), conversation.id);
43
+ return conversation;
44
+ }
45
+ select(user, conversationId) {
46
+ const conversation = this.getConversations(user).find((item) => item.id === conversationId);
47
+ if (conversation)
48
+ this.activeConversationIds.set(this.getKey(user), conversation.id);
49
+ return conversation;
50
+ }
51
+ remove(user, conversationId) {
52
+ const conversations = this.getConversations(user);
53
+ const index = conversations.findIndex((conversation) => conversation.id === conversationId);
54
+ if (index < 0)
55
+ return false;
56
+ conversations.splice(index, 1);
57
+ if (this.activeConversationIds.get(this.getKey(user)) === conversationId) {
58
+ this.activeConversationIds.delete(this.getKey(user));
59
+ }
60
+ return true;
61
+ }
62
+ saveActive(user, messages, title) {
63
+ const conversation = this.getActive(user);
64
+ conversation.messages = messages;
65
+ conversation.updatedAt = new Date().toISOString();
66
+ if (title)
67
+ conversation.title = title;
68
+ else if (conversation.title === 'New conversation') {
69
+ const firstText = this.findFirstUserText(messages);
70
+ if (firstText)
71
+ conversation.title = firstText.slice(0, 80);
72
+ }
73
+ return conversation;
74
+ }
75
+ getConversations(user) {
76
+ const key = this.getKey(user);
77
+ let conversations = this.conversations.get(key);
78
+ if (!conversations) {
79
+ conversations = [];
80
+ this.conversations.set(key, conversations);
81
+ }
82
+ return conversations;
83
+ }
84
+ getKey(user) {
85
+ return `${user.id}:${this.agent.id}`;
86
+ }
87
+ findFirstUserText(messages) {
88
+ const message = messages.find((item) => item.role === 'user');
89
+ const content = message?.content;
90
+ if (typeof content === 'string')
91
+ return content.trim();
92
+ if (Array.isArray(content)) {
93
+ const text = content.find((part) => part && typeof part === 'object' && part.type === 'text');
94
+ return typeof text?.text === 'string' ? text.text.trim() : undefined;
95
+ }
96
+ return undefined;
97
+ }
98
+ }