adminizer 5.0.0-build.2 → 5.0.0-build.21
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.
- package/assets/abnfDiagram-VCTEODGH-CkmsvdDi.js +1 -0
- package/assets/add-CESPH_aI.js +1 -0
- package/assets/add-group-CQ4Xm8Ru.js +1 -0
- package/assets/add-user-B-QNjR4B.js +1 -0
- package/assets/ai-assistant/agent.es.js +28362 -0
- package/assets/app-C5Ju5Zra.css +1 -0
- package/assets/app-Ca5yUSBv.js +138 -0
- package/assets/arc-B-Dt8YNl.js +1 -0
- package/assets/architectureDiagram-5GKGNRK7-Ca58kq-l.js +36 -0
- package/assets/asmcrypto-DZeQH0LC.js +1 -0
- package/assets/blockDiagram-NRAW4CY4-CuqE52IG.js +129 -0
- package/assets/c4Diagram-UCG6FXSJ-Cx8z-0Bw.js +38 -0
- package/assets/catalog-CkSbxvx0.js +6 -0
- package/assets/channel-C9z6Pi_j.js +1 -0
- package/assets/chunk-2Q5K7J3B-CXISVKTC.js +1 -0
- package/assets/chunk-5VM5RSS4-DYGeeVYe.js +15 -0
- package/assets/chunk-F27PBJKO-BeY-dbKj.js +1 -0
- package/assets/chunk-G27WJ6UU-DeX_eH7C.js +231 -0
- package/assets/chunk-JWPE2WC7-DaxI9q0u.js +1 -0
- package/assets/chunk-LCL6LL3I-B7PkaEz_.js +206 -0
- package/assets/chunk-POPQ4Y6H-lwu6ZpsN.js +1 -0
- package/assets/chunk-SVP7TREG-CVFllgl1.js +88 -0
- package/assets/chunk-XXDRQBXY-DDLN0rcg.js +1 -0
- package/assets/classDiagram-DTDB5LWJ-BKtCIC3B.js +1 -0
- package/assets/classDiagram-v2-JRS7N3AN-BKtCIC3B.js +1 -0
- package/assets/controls/ckeditor.es.js +9370 -9306
- package/assets/controls/handsontable.es.js +11971 -11938
- package/assets/controls/jsoneditor.es.js +13104 -13008
- package/assets/controls/leaflet.es.js +1023 -1004
- package/assets/controls/toast-ui.es.js +1843 -1834
- package/assets/cose-bilkent-JH36ORCC-CWRyUkhQ.js +1 -0
- package/assets/cynefin-OW5HDTMX-B1iLDXCC.js +166 -0
- package/assets/cynefinDiagram-5FMLGOSQ-DaXQHr3l.js +62 -0
- package/assets/cytoscape.esm-yzknjiTM.js +321 -0
- package/assets/dagre-3AP2YEHR-DFORa4Hz.js +4 -0
- package/assets/dashboard-Ssa1_Of3.js +3 -0
- package/assets/debounce-B3mXI_sx.js +1 -0
- package/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/assets/diagram-S7CK7UJ4-CgP6KBo0.js +30 -0
- package/assets/diagram-UQ7AKVKN-oYfQe6w0.js +41 -0
- package/assets/diagram-VSXAHHWV-DfCU5wgt.js +3 -0
- package/assets/diagram-VX7I27RA-BITJVrNY.js +24 -0
- package/assets/diagram-Z3DM3KII-D8J9Ssjy.js +24 -0
- package/assets/docs-DHMt_Rq1.js +1 -0
- package/assets/ebnfDiagram-PWID7BFC-qpNmoYgx.js +1 -0
- package/assets/erDiagram-SSCWMZ5O-DDF-Y0zs.js +99 -0
- package/assets/flowDiagram-A5DVABFB-CjKVkR9T.js +168 -0
- package/assets/ganttDiagram-EL5Y4UJY-BXGQ53w6.js +292 -0
- package/assets/gitGraphDiagram-WWUBYQGX-BLMevdLh.js +106 -0
- package/assets/history-tBOo2gd5.js +1 -0
- package/assets/infoDiagram-RXCK75RN-BAnPdnyw.js +2 -0
- package/assets/init-Gi6I4Gst.js +1 -0
- package/assets/init-user-DrNUHa6r.js +1 -0
- package/assets/isObject-DL-BuaBl.js +1 -0
- package/assets/ishikawaDiagram-5VMMS53U-PnL0Z4Li.js +70 -0
- package/assets/journeyDiagram-EYS64GPL-CLcsCSwF.js +139 -0
- package/assets/kanban-definition-3QL26DDD-C2eU3uBE.js +89 -0
- package/assets/katex-HP8lGamR.js +257 -0
- package/assets/layout-BXoYdHdx.js +1 -0
- package/assets/linear-BSzrP0oP.js +1 -0
- package/assets/list-CLg6xeIK.js +4 -0
- package/assets/login-Ce7o3-o_.js +1 -0
- package/assets/manifest.json +834 -58
- package/assets/{media-manager-BDbpSSjH.js → media-manager-D7G1P3fz.js} +3 -3
- package/assets/mermaid.core-aKMJHeEk.js +310 -0
- package/assets/mindmap-definition-FBJOCRG2-Dzbz5ghA.js +96 -0
- package/assets/module-BsXph_pA.js +1 -0
- package/assets/notification-D-Zz1gF_.js +20 -0
- package/assets/ordinal-Cboi1Yqb.js +1 -0
- package/assets/pegDiagram-XKGWAZYB-BTdWkU0Y.js +1 -0
- package/assets/pieDiagram-E7YTZNPT-Dnw8-TB-.js +39 -0
- package/assets/quadrantDiagram-AXDQQJYC-CALzLlg9.js +7 -0
- package/assets/railroadDiagram-O6MQD6OU-CTA1vzcv.js +1 -0
- package/assets/register-CNnBkzej.js +1 -0
- package/assets/requirementDiagram-EFPCY7ZU-UX2CF5HA.js +84 -0
- package/assets/ru-CoNgAwFK.js +1 -0
- package/assets/sankeyDiagram-P5KCCOFB-4aPp3ATV.js +40 -0
- package/assets/sequenceDiagram-WJ2MYXX4--kltkwJw.js +162 -0
- package/assets/sizeCapture-X5ZJPWSS-Ca9-g5QY.js +1 -0
- package/assets/stateDiagram-HBIQ2CUA-Diaf5Q7I.js +1 -0
- package/assets/stateDiagram-v2-4QOOHH4V-C6FlRp7u.js +1 -0
- package/assets/swimlanes-XN3QIQJK-DhqX_myE.js +1 -0
- package/assets/swimlanesDiagram-VK2B7HYN-i_3LTEBk.js +8 -0
- package/assets/timeline-definition-24CTP7MA-CwkJQbhq.js +120 -0
- package/assets/toNumber-DZb-r9sy.js +1 -0
- package/assets/use-appearance-DC-DESKB.js +1 -0
- package/assets/user-filters-list-W47DTBp2.js +1 -0
- package/assets/vennDiagram-4TSXK5OY-BNM3_cVS.js +34 -0
- package/assets/wardleyDiagram-VM6X3IG4-BODb1NP8.js +78 -0
- package/assets/welcome-BeBm6NIi.js +1 -0
- package/assets/with-app-layout-B90DtzYC.js +31 -0
- package/assets/xychartDiagram-S5SC5T6Z-DthqEh3g.js +7 -0
- package/controllers/access-rights/permissionOptions.d.ts +2 -0
- package/controllers/access-rights/permissionOptions.js +23 -0
- package/controllers/add.js +1 -1
- package/controllers/addGroup.js +15 -6
- package/controllers/ai/AiAgentController.d.ts +66 -0
- package/controllers/ai/AiAgentController.js +435 -0
- package/controllers/ai/AiAssistantController.d.ts +0 -4
- package/controllers/ai/AiAssistantController.js +4 -78
- package/controllers/catalog/FrontendCatalogAdapter.d.ts +1 -1
- package/controllers/catalog/FrontentCatalogAdapter.d.ts +1 -1
- package/controllers/docs/index.d.ts +22 -0
- package/controllers/docs/index.js +126 -0
- package/controllers/edit.js +1 -1
- package/controllers/editGroup.js +15 -6
- package/controllers/feedback/feedbackController.d.ts +1 -1
- package/controllers/filter-fields/savedFilters.js +1 -1
- package/controllers/history-actions/HistoryController.d.ts +1 -1
- package/controllers/history-actions/HistoryController.js +6 -6
- package/controllers/list.js +1 -1
- package/controllers/login.js +1 -1
- package/controllers/media-manager/mediaManagerAdapter.d.ts +1 -1
- package/controllers/notifications/NotificationController.js +14 -8
- package/controllers/view.js +2 -2
- package/controllers/widgets/filterInfo.js +1 -1
- package/helpers/accessRightsHelper.d.ts +11 -4
- package/helpers/accessRightsHelper.js +82 -12
- package/helpers/assetVersionHelper.d.ts +14 -0
- package/helpers/assetVersionHelper.js +42 -0
- package/helpers/configHelper.d.ts +3 -3
- package/helpers/configHelper.js +20 -18
- package/helpers/controllerHelper.d.ts +3 -2
- package/helpers/controllerHelper.js +25 -15
- package/helpers/fieldsHelper.d.ts +2 -2
- package/helpers/fieldsHelper.js +3 -3
- package/helpers/filterDatetimeHelper.d.ts +1 -1
- package/helpers/inertiaActionsHelper.d.ts +3 -3
- package/helpers/inertiaActionsHelper.js +4 -5
- package/helpers/inertiaAddHelper.d.ts +5 -5
- package/helpers/inertiaAddHelper.js +7 -6
- package/helpers/inertiaGroupHelper.d.ts +3 -3
- package/helpers/inertiaGroupHelper.js +10 -2
- package/helpers/inertiaListHelper.d.ts +4 -4
- package/helpers/inertiaListHelper.js +7 -8
- package/helpers/inertiaMenuHelper.d.ts +2 -3
- package/helpers/inertiaMenuHelper.js +3 -26
- package/helpers/inertiaUserHelper.d.ts +3 -3
- package/helpers/menuHelper.d.ts +2 -2
- package/helpers/modelResourceHelper.d.ts +9 -0
- package/helpers/modelResourceHelper.js +43 -0
- package/helpers/navigationAccessHelper.d.ts +13 -0
- package/helpers/navigationAccessHelper.js +40 -0
- package/helpers/translationHelper.d.ts +2 -2
- package/index.d.ts +69 -58
- package/index.js +11 -0
- package/interfaces/adminpanelConfig.d.ts +18 -5
- package/interfaces/global.d.ts +6 -6
- package/interfaces/internalModelAccess.d.ts +1 -1
- package/interfaces/listQueryBuilder.d.ts +1 -1
- package/interfaces/types.d.ts +171 -2
- package/lib/Adminizer.d.ts +46 -26
- package/lib/Adminizer.js +37 -1
- package/lib/DataAccessor.d.ts +7 -6
- package/lib/DataAccessor.js +50 -48
- package/lib/MiddlewareManager.d.ts +1 -1
- package/lib/access-rights/AccessRightsHandler.d.ts +2 -2
- package/lib/admin-links/AdminLinkHandler.d.ts +72 -0
- package/lib/admin-links/AdminLinkHandler.js +175 -0
- package/lib/ai-assistant/AbstractAiConversationHistoryService.d.ts +39 -0
- package/lib/ai-assistant/AbstractAiConversationHistoryService.js +98 -0
- package/lib/ai-assistant/AbstractAiModelService.d.ts +140 -3
- package/lib/ai-assistant/AbstractAiModelService.js +154 -1
- package/lib/ai-assistant/AiAgentRunManager.d.ts +35 -0
- package/lib/ai-assistant/AiAgentRunManager.js +109 -0
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.d.ts +58 -0
- package/lib/ai-assistant/AiAssistantAgentSkillHandler.js +87 -0
- package/lib/ai-assistant/AiAssistantHandler.d.ts +16 -5
- package/lib/ai-assistant/AiAssistantHandler.js +35 -1
- package/lib/ai-assistant/AiAssistantUiMethodHandler.d.ts +60 -0
- package/lib/ai-assistant/AiAssistantUiMethodHandler.js +138 -0
- package/lib/ai-assistant/builtinAgentSkills.d.ts +9 -0
- package/lib/ai-assistant/builtinAgentSkills.js +256 -0
- package/lib/ai-assistant/jsonSafe.d.ts +13 -0
- package/lib/ai-assistant/jsonSafe.js +81 -0
- package/lib/app-manager/AdminizerApp.d.ts +40 -11
- package/lib/app-manager/AppManager.d.ts +2 -2
- package/lib/app-manager/AppManager.js +54 -50
- package/lib/app-manager/AssetHandler.d.ts +2 -2
- package/lib/app-manager/ConfigLayerHandler.d.ts +2 -2
- package/lib/app-manager/ControllerHandler.d.ts +2 -2
- package/lib/catalog/AbstractCatalog.d.ts +1 -1
- package/lib/catalog/CatalogHandler.d.ts +1 -1
- package/lib/controls/ControlsHandler.d.ts +1 -1
- package/lib/controls/codeEditor/MonacoEditor.d.ts +1 -1
- package/lib/controls/geojsoneditor/GeoEditor.d.ts +1 -1
- package/lib/controls/jsoneditor/JsonEditor.d.ts +1 -1
- package/lib/controls/markdown/ToastUiEditor.d.ts +1 -1
- package/lib/controls/table/Handsontable.d.ts +1 -1
- package/lib/controls/wysiwyg/CKeditor.d.ts +1 -1
- package/lib/docs/AbstractDocumentation.d.ts +100 -0
- package/lib/docs/AbstractDocumentation.js +108 -0
- package/lib/docs/DocumentationHandler.d.ts +100 -0
- package/lib/docs/DocumentationHandler.js +198 -0
- package/lib/docs/FileDocumentation.d.ts +47 -0
- package/lib/docs/FileDocumentation.js +189 -0
- package/lib/docs/docsAgentSkills.d.ts +10 -0
- package/lib/docs/docsAgentSkills.js +93 -0
- package/lib/feedback/FeedbackHandler.d.ts +2 -2
- package/lib/filters/ConditionValidator.d.ts +1 -1
- package/lib/filters/FilterCustomFieldHandler.d.ts +2 -2
- package/lib/filters/FilterService.d.ts +4 -4
- package/lib/filters/FilterService.js +4 -3
- package/lib/filters/index.d.ts +6 -6
- package/lib/history-actions/AbstractHistoryAdapter.d.ts +6 -6
- package/lib/history-actions/AbstractHistoryAdapter.js +11 -9
- package/lib/history-actions/DefaultHistoryAdapter.d.ts +4 -4
- package/lib/history-actions/HistoryHandler.d.ts +1 -1
- package/lib/list-query-builder/ListQueryBuilder.d.ts +7 -7
- package/lib/media-manager/AbstractMediaManager.d.ts +2 -2
- package/lib/media-manager/MediaManagerHandler.d.ts +1 -1
- package/lib/media-manager/Thumb.d.ts +1 -1
- package/lib/media-manager/helpers/MediaManagerHelper.d.ts +5 -5
- package/lib/model/AbstractModel.d.ts +11 -4
- package/lib/model/InternalModelAccessFactory.d.ts +2 -2
- package/lib/model/ModelHandler.d.ts +50 -5
- package/lib/model/ModelHandler.js +107 -8
- package/lib/model/adapter/sequelize.d.ts +7 -5
- package/lib/model/adapter/sequelize.js +79 -46
- package/lib/model/adapter/typeorm.d.ts +2 -2
- package/lib/model/adapter/typeorm.js +1 -1
- package/lib/model/internalModelRepository.d.ts +2 -2
- package/lib/notifications/AbstractNotificationService.d.ts +4 -4
- package/lib/notifications/GeneralNotificationService.d.ts +2 -2
- package/lib/notifications/GeneralNotificationService.js +6 -5
- package/lib/notifications/NotificationHandler.d.ts +2 -2
- package/lib/notifications/SystemNotificationService.d.ts +2 -2
- package/lib/notifications/SystemNotificationService.js +1 -1
- package/lib/requestProcessor.d.ts +1 -1
- package/lib/ui-i18n/uiTranslationKeys.d.ts +2 -1
- package/lib/ui-i18n/uiTranslationKeys.js +42 -1
- package/lib/widgets/abstractAction.d.ts +1 -1
- package/lib/widgets/abstractCustom.d.ts +2 -2
- package/lib/widgets/abstractInfo.d.ts +1 -1
- package/lib/widgets/abstractLink.d.ts +2 -2
- package/lib/widgets/abstractSwitch.d.ts +1 -1
- package/lib/widgets/abstractWidgetBase.d.ts +1 -1
- package/lib/widgets/widgetHandler.d.ts +9 -9
- package/lib/widgets/widgetHandler.js +5 -1
- package/models/Filter.d.ts +1 -1
- package/models/FilterColumn.d.ts +1 -1
- package/models/Group.d.ts +3 -2
- package/models/HistoryActions.d.ts +1 -1
- package/models/MediaManagerAP.d.ts +2 -2
- package/models/MediaManagerAssociationsAP.d.ts +1 -1
- package/models/MediaManagerMetaAP.d.ts +1 -1
- package/models/User.d.ts +2 -2
- package/models/UserNotification.d.ts +1 -1
- package/package.json +29 -20
- package/policies/authPolicies.js +6 -6
- package/policies/permissionResolvers.d.ts +1 -0
- package/policies/permissionResolvers.js +3 -0
- package/services/FeedService.d.ts +3 -3
- package/system/Router.d.ts +1 -1
- package/system/Router.js +27 -1
- package/system/bindAccessRights.d.ts +1 -1
- package/system/bindAccessRights.js +2 -3
- package/system/bindAuthorization.d.ts +1 -1
- package/system/bindControls.d.ts +1 -1
- package/system/bindCors.d.ts +1 -1
- package/system/bindCustomFilterHandlers.d.ts +1 -1
- package/system/bindDashboardWidgets.d.ts +1 -1
- package/system/bindDev.d.ts +1 -1
- package/system/bindDocs.d.ts +13 -0
- package/system/bindDocs.js +77 -0
- package/system/bindHistory.d.ts +1 -1
- package/system/bindInertia.d.ts +1 -1
- package/system/bindInertia.js +23 -1
- package/system/bindMediaManager.d.ts +1 -1
- package/system/bindNotifications.d.ts +1 -1
- package/system/bindReqFunctions.d.ts +1 -1
- package/system/bindTranslations.d.ts +1 -1
- package/system/buildInternalModelAccess.d.ts +3 -3
- package/system/buildInternalModelAccess.js +5 -2
- package/system/defaults.d.ts +1 -1
- package/system/routePolicies.d.ts +1 -1
- package/system/systemModelContracts.d.ts +1 -1
- package/system/systemModelContracts.js +11 -7
- package/system/validateSystemModels.d.ts +1 -1
- package/system/validateSystemModels.js +29 -9
- package/assets/add-group-BdHFzKng.js +0 -1
- package/assets/add-p6yGCoZ7.js +0 -1
- package/assets/add-user-CDspCLu4.js +0 -1
- package/assets/app-DNXiApCk.css +0 -1
- package/assets/app-layout-DCr_-JY3.js +0 -1
- package/assets/app.js +0 -138
- package/assets/catalog-XN-_p9TB.js +0 -6
- package/assets/dashboard-CHZO2QFY.js +0 -3
- package/assets/debounce-BMmXVQ06.js +0 -1
- package/assets/history-CQd0FojQ.js +0 -1
- package/assets/init-user-8aUohVDq.js +0 -1
- package/assets/list-PHWgTHsg.js +0 -4
- package/assets/login-CO4laf6m.js +0 -1
- package/assets/module-DVyQtIxm.js +0 -1
- package/assets/notification-B87XG8nQ.js +0 -20
- package/assets/register-DBYIMXGQ.js +0 -1
- package/assets/ru-rYKdQbWF.js +0 -1
- package/assets/use-appearance-_EbbjHTh.js +0 -1
- package/assets/user-filters-list-BRzDbPkx.js +0 -1
- package/assets/welcome-DiXmrTgc.js +0 -1
|
@@ -3,15 +3,46 @@ import { Adminizer } from '../Adminizer.js';
|
|
|
3
3
|
export class AiAssistantHandler {
|
|
4
4
|
adminizer;
|
|
5
5
|
models = new Map();
|
|
6
|
+
owners = new Map();
|
|
6
7
|
history = new Map();
|
|
7
8
|
constructor(adminizer) {
|
|
8
9
|
this.adminizer = adminizer;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* @param owner App name when the model belongs to an app. Models without an
|
|
13
|
+
* owner belong to the host application and are never removed automatically.
|
|
14
|
+
*/
|
|
15
|
+
registerModel(service, owner) {
|
|
11
16
|
if (this.models.has(service.id)) {
|
|
12
17
|
Adminizer.log.warn(`AI assistant model with id "${service.id}" is already registered. Overwriting.`);
|
|
13
18
|
}
|
|
19
|
+
service.attachAdminizer(this.adminizer);
|
|
14
20
|
this.models.set(service.id, service);
|
|
21
|
+
if (owner) {
|
|
22
|
+
this.owners.set(service.id, owner);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
this.owners.delete(service.id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* @param owner When passed, the model is kept if it is owned by someone
|
|
30
|
+
* else, so a disabled app never removes another owner's model.
|
|
31
|
+
*/
|
|
32
|
+
unregisterModel(id, owner) {
|
|
33
|
+
if (!this.models.has(id)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (owner && this.owners.get(id) !== owner) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.models.delete(id);
|
|
40
|
+
this.owners.delete(id);
|
|
41
|
+
for (const key of this.history.keys()) {
|
|
42
|
+
if (key.endsWith(`:${id}`)) {
|
|
43
|
+
this.history.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
15
46
|
}
|
|
16
47
|
getModel(id) {
|
|
17
48
|
return this.models.get(id);
|
|
@@ -25,6 +56,9 @@ export class AiAssistantHandler {
|
|
|
25
56
|
resetHistory(userId, modelId) {
|
|
26
57
|
this.history.delete(this.getSessionKey(userId, modelId));
|
|
27
58
|
}
|
|
59
|
+
replaceHistory(userId, modelId, history) {
|
|
60
|
+
this.history.set(this.getSessionKey(userId, modelId), history);
|
|
61
|
+
}
|
|
28
62
|
async sendMessage(user, modelId, prompt) {
|
|
29
63
|
const service = this.getModel(modelId);
|
|
30
64
|
if (!service) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { User } from '../../models/User.js';
|
|
2
|
+
import type { Adminizer } from '../Adminizer.js';
|
|
3
|
+
import type { AdminLinkTemplateParam } from '../admin-links/AdminLinkHandler.js';
|
|
4
|
+
export interface AiAssistantAdminLink {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
/** Concrete page, ready for `navigate`. Absent for templates. */
|
|
8
|
+
link?: string;
|
|
9
|
+
/** Parametrized page: pass `template` plus `params` to `navigate`. */
|
|
10
|
+
template?: string;
|
|
11
|
+
/** Placeholders that must be filled to open a template. */
|
|
12
|
+
params?: AdminLinkTemplateParam[];
|
|
13
|
+
description?: string;
|
|
14
|
+
section?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A browser capability an AI service may expose as a tool. The agent emits
|
|
18
|
+
* `ui.method` with this id; the assistant panel performs the browser action.
|
|
19
|
+
*/
|
|
20
|
+
export interface AiAssistantUiMethod {
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
description: string;
|
|
24
|
+
/** JSON Schema consumed by tool-capable model services. */
|
|
25
|
+
inputSchema: Record<string, unknown>;
|
|
26
|
+
/** Built-in browser executor understood by the shared assistant panel. */
|
|
27
|
+
action: 'navigate' | 'search-admin-links' | string;
|
|
28
|
+
accessRightsToken?: string;
|
|
29
|
+
}
|
|
30
|
+
/** What the agent may ask to open: a concrete link or a template plus values. */
|
|
31
|
+
export interface AiAssistantNavigationTarget {
|
|
32
|
+
href?: string;
|
|
33
|
+
template?: string;
|
|
34
|
+
params?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
export declare class AiAssistantUiMethodHandler {
|
|
37
|
+
private readonly adminizer;
|
|
38
|
+
private readonly methods;
|
|
39
|
+
private readonly owners;
|
|
40
|
+
constructor(adminizer: Adminizer);
|
|
41
|
+
register(method: AiAssistantUiMethod, owner?: string): void;
|
|
42
|
+
unregister(id: string, owner?: string): void;
|
|
43
|
+
getAvailable(user: User): AiAssistantUiMethod[];
|
|
44
|
+
/**
|
|
45
|
+
* Search exactly the navigation a user can open. This intentionally uses
|
|
46
|
+
* MenuHelper rather than client-side markup, therefore a tool result is
|
|
47
|
+
* permission-safe and works when the assistant panel is not visible.
|
|
48
|
+
*
|
|
49
|
+
* Parametrized pages are returned as templates, so a record page is
|
|
50
|
+
* reachable even though its URL only exists once the id is known.
|
|
51
|
+
*/
|
|
52
|
+
searchAdminLinks(user: User, query?: string): AiAssistantAdminLink[];
|
|
53
|
+
/**
|
|
54
|
+
* Validates what an agent asked to open and returns the concrete admin URL.
|
|
55
|
+
* Templates are resolved against this user's own permissions, so the LLM
|
|
56
|
+
* can only reach pages that user could reach by clicking.
|
|
57
|
+
*/
|
|
58
|
+
resolveNavigation(user: User, target: AiAssistantNavigationTarget): string;
|
|
59
|
+
private slug;
|
|
60
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { listAccessibleMenuItems } from '../../helpers/navigationAccessHelper.js';
|
|
2
|
+
export class AiAssistantUiMethodHandler {
|
|
3
|
+
adminizer;
|
|
4
|
+
methods = new Map();
|
|
5
|
+
owners = new Map();
|
|
6
|
+
constructor(adminizer) {
|
|
7
|
+
this.adminizer = adminizer;
|
|
8
|
+
this.register({
|
|
9
|
+
id: 'navigate',
|
|
10
|
+
title: 'Open admin section',
|
|
11
|
+
description: 'Open an Adminizer page in the current browser tab using the Inertia router.'
|
|
12
|
+
+ ' Pass `href` for a concrete link, or `template` plus `params` for a parametrized page'
|
|
13
|
+
+ ' such as a single record. Both come from search-admin-links.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
href: { type: 'string', description: 'Adminizer-relative URL of a concrete page to open.' },
|
|
18
|
+
template: { type: 'string', description: 'Id of a link template returned by search-admin-links.' },
|
|
19
|
+
params: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
description: 'Values for the template placeholders, e.g. {"id": "3860d321-…"}.',
|
|
22
|
+
additionalProperties: { type: 'string' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
},
|
|
27
|
+
action: 'navigate',
|
|
28
|
+
});
|
|
29
|
+
this.register({
|
|
30
|
+
id: 'search-admin-links',
|
|
31
|
+
title: 'Search admin sections and links',
|
|
32
|
+
description: 'Search the current user\'s accessible Adminizer navigation sections, links and'
|
|
33
|
+
+ ' link templates (record pages, catalog items and other parametrized pages).',
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: { query: { type: 'string', description: 'Words to search in section, link and template titles.' } },
|
|
37
|
+
required: ['query'], additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
action: 'search-admin-links',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
register(method, owner) {
|
|
43
|
+
const id = method.id.trim().toLowerCase();
|
|
44
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(id))
|
|
45
|
+
throw new Error(`Invalid AI assistant UI method id: ${method.id}`);
|
|
46
|
+
if (!method.title.trim() || !method.description.trim())
|
|
47
|
+
throw new Error(`AI assistant UI method "${id}" requires title and description`);
|
|
48
|
+
this.methods.set(id, { ...method, id });
|
|
49
|
+
if (owner)
|
|
50
|
+
this.owners.set(id, owner);
|
|
51
|
+
else
|
|
52
|
+
this.owners.delete(id);
|
|
53
|
+
}
|
|
54
|
+
unregister(id, owner) {
|
|
55
|
+
const normalized = id.trim().toLowerCase();
|
|
56
|
+
if (owner && this.owners.get(normalized) !== owner)
|
|
57
|
+
return;
|
|
58
|
+
this.methods.delete(normalized);
|
|
59
|
+
this.owners.delete(normalized);
|
|
60
|
+
}
|
|
61
|
+
getAvailable(user) {
|
|
62
|
+
return [...this.methods.values()]
|
|
63
|
+
.filter((method) => !method.accessRightsToken || this.adminizer.accessRightsHelper.hasPermission(method.accessRightsToken, user))
|
|
64
|
+
.map((method) => ({ ...method, inputSchema: { ...method.inputSchema } }));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Search exactly the navigation a user can open. This intentionally uses
|
|
68
|
+
* MenuHelper rather than client-side markup, therefore a tool result is
|
|
69
|
+
* permission-safe and works when the assistant panel is not visible.
|
|
70
|
+
*
|
|
71
|
+
* Parametrized pages are returned as templates, so a record page is
|
|
72
|
+
* reachable even though its URL only exists once the id is known.
|
|
73
|
+
*/
|
|
74
|
+
searchAdminLinks(user, query = '') {
|
|
75
|
+
const needle = this.slug(query);
|
|
76
|
+
const result = [];
|
|
77
|
+
const matches = (...values) => !needle || this.slug(values.filter(Boolean).join(' ')).includes(needle);
|
|
78
|
+
const add = (item, section) => {
|
|
79
|
+
if (!item?.link || !item?.title)
|
|
80
|
+
return;
|
|
81
|
+
const link = {
|
|
82
|
+
id: String(item.id || item.title), title: String(item.title), link: String(item.link),
|
|
83
|
+
section: item.section || section,
|
|
84
|
+
};
|
|
85
|
+
// A link with placeholders is surfaced as a template instead.
|
|
86
|
+
if (!/:[A-Za-z0-9_]+/.test(link.link) && matches(link.title, link.id, link.section))
|
|
87
|
+
result.push(link);
|
|
88
|
+
for (const child of item.actions ?? item.subItems ?? [])
|
|
89
|
+
add(child, link.section);
|
|
90
|
+
};
|
|
91
|
+
// Exactly the menu the sidebar renders for this user, sub-items included.
|
|
92
|
+
for (const item of listAccessibleMenuItems(this.adminizer, user))
|
|
93
|
+
add(item, item.section);
|
|
94
|
+
for (const template of this.adminizer.adminLinkHandler.listTemplates(user)) {
|
|
95
|
+
if (!matches(template.title, template.id, template.section, template.description, template.template))
|
|
96
|
+
continue;
|
|
97
|
+
result.push({
|
|
98
|
+
id: template.id,
|
|
99
|
+
title: template.title,
|
|
100
|
+
template: template.template,
|
|
101
|
+
params: template.params,
|
|
102
|
+
description: template.description,
|
|
103
|
+
section: template.section,
|
|
104
|
+
// A template without placeholders is already a usable link.
|
|
105
|
+
link: template.params.length ? undefined : template.template,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Validates what an agent asked to open and returns the concrete admin URL.
|
|
112
|
+
* Templates are resolved against this user's own permissions, so the LLM
|
|
113
|
+
* can only reach pages that user could reach by clicking.
|
|
114
|
+
*/
|
|
115
|
+
resolveNavigation(user, target) {
|
|
116
|
+
const template = target.template?.trim();
|
|
117
|
+
if (template) {
|
|
118
|
+
return this.adminizer.adminLinkHandler.resolveTemplate(user, template, target.params ?? {});
|
|
119
|
+
}
|
|
120
|
+
const href = target.href?.trim();
|
|
121
|
+
if (!href)
|
|
122
|
+
throw new Error('Navigation requires either href or template');
|
|
123
|
+
const prefix = (this.adminizer.config.routePrefix || '').replace(/\/+$/, '');
|
|
124
|
+
if (href !== prefix && !href.startsWith(`${prefix}/`)) {
|
|
125
|
+
throw new Error('Only Adminizer-relative links may be opened.');
|
|
126
|
+
}
|
|
127
|
+
if (/:[A-Za-z0-9_]+/.test(href)) {
|
|
128
|
+
throw new Error('This link is a template: pass it as `template` together with `params`.');
|
|
129
|
+
}
|
|
130
|
+
if (this.adminizer.adminLinkHandler.isDestructivePath(href)) {
|
|
131
|
+
throw new Error('This page changes data and cannot be opened by the assistant.');
|
|
132
|
+
}
|
|
133
|
+
return href;
|
|
134
|
+
}
|
|
135
|
+
slug(value) {
|
|
136
|
+
return String(value ?? '').trim().toLowerCase().replace(/[\s_-]+/g, '-');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Adminizer } from '../Adminizer.js';
|
|
2
|
+
import type { AiAssistantAgentSkill } from './AiAssistantAgentSkillHandler.js';
|
|
3
|
+
/**
|
|
4
|
+
* Skills every agent gets for free: who it is talking to, and read/edit access
|
|
5
|
+
* scoped to exactly the data that user may reach in the admin panel. Because
|
|
6
|
+
* every call re-checks the user's permissions, an agent built on them can be
|
|
7
|
+
* offered to non-administrator accounts as well.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildBuiltinAgentSkills(adminizer: Adminizer): AiAssistantAgentSkill[];
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { listModelResources, resolveModelResource } from '../../helpers/modelResourceHelper.js';
|
|
2
|
+
import { listAccessibleMenuItems } from '../../helpers/navigationAccessHelper.js';
|
|
3
|
+
import { DataAccessor } from '../DataAccessor.js';
|
|
4
|
+
import { toJsonSafe } from './jsonSafe.js';
|
|
5
|
+
const MAX_RECORDS = 50;
|
|
6
|
+
const DEFAULT_RECORDS = 10;
|
|
7
|
+
/** What the current user may do with every configured model resource. */
|
|
8
|
+
function listPermittedModels(adminizer, user) {
|
|
9
|
+
const has = (token) => adminizer.accessRightsHelper.hasPermission(token, user);
|
|
10
|
+
return listModelResources(adminizer)
|
|
11
|
+
.map((resource) => ({
|
|
12
|
+
resource,
|
|
13
|
+
canRead: has(`read-${resource.name}-model`),
|
|
14
|
+
canUpdate: has(`update-${resource.name}-model`),
|
|
15
|
+
canCreate: has(`create-${resource.name}-model`),
|
|
16
|
+
}))
|
|
17
|
+
.filter((entry) => entry.canRead || entry.canUpdate);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a model this user may use for `action`. Nothing outside the user's
|
|
21
|
+
* own permissions is reachable, which is what makes these skills safe to hand
|
|
22
|
+
* to non-administrator accounts.
|
|
23
|
+
*/
|
|
24
|
+
function requireModel(adminizer, user, name, action) {
|
|
25
|
+
const resource = resolveModelResource(adminizer, name);
|
|
26
|
+
if (!resource?.model)
|
|
27
|
+
throw new Error(`Model "${name}" is not available.`);
|
|
28
|
+
const token = action === 'read' ? `read-${resource.name}-model` : `update-${resource.name}-model`;
|
|
29
|
+
if (!adminizer.accessRightsHelper.hasPermission(token, user)) {
|
|
30
|
+
throw new Error(`You are not allowed to ${action} the "${resource.name}" model.`);
|
|
31
|
+
}
|
|
32
|
+
return resource;
|
|
33
|
+
}
|
|
34
|
+
function modelNames(adminizer, user, action) {
|
|
35
|
+
return listPermittedModels(adminizer, user)
|
|
36
|
+
.filter((entry) => (action === 'read' ? entry.canRead : entry.canUpdate))
|
|
37
|
+
.map((entry) => entry.resource.name);
|
|
38
|
+
}
|
|
39
|
+
/** Adds an `enum` of permitted models, so the agent cannot invent a model name. */
|
|
40
|
+
function withModelEnum(schema, models) {
|
|
41
|
+
const properties = { ...schema.properties };
|
|
42
|
+
properties.model = { ...properties.model, enum: models };
|
|
43
|
+
return { ...schema, properties };
|
|
44
|
+
}
|
|
45
|
+
function identifierField(adminizer, resource) {
|
|
46
|
+
return resource.config?.identifierField
|
|
47
|
+
?? adminizer.config.identifierField
|
|
48
|
+
?? resource.model?.primaryKey
|
|
49
|
+
?? 'id';
|
|
50
|
+
}
|
|
51
|
+
function parseCriteria(filter) {
|
|
52
|
+
if (filter === undefined || filter === null || filter === '')
|
|
53
|
+
return {};
|
|
54
|
+
if (typeof filter === 'object')
|
|
55
|
+
return filter;
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(String(filter));
|
|
58
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
59
|
+
throw new Error('not an object');
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error('filter must be a JSON object, e.g. {"title": "example"}');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function pickFields(record, fields) {
|
|
67
|
+
return Object.fromEntries(fields.filter((field) => field in record).map((field) => [field, record[field]]));
|
|
68
|
+
}
|
|
69
|
+
function slugify(value) {
|
|
70
|
+
return String(value ?? '').trim().toLowerCase().replace(/[\s_-]+/g, '-');
|
|
71
|
+
}
|
|
72
|
+
/** A navigation entry as the agent sees it: the sub-actions of a menu item. */
|
|
73
|
+
function describeNavigationActions(actions) {
|
|
74
|
+
return (actions ?? []).map((action) => ({
|
|
75
|
+
id: action.id || action.title,
|
|
76
|
+
title: action.title,
|
|
77
|
+
link: action.link,
|
|
78
|
+
icon: action.icon || undefined,
|
|
79
|
+
target: action.type === 'blank' ? 'blank' : undefined,
|
|
80
|
+
actions: action.subItems?.length ? describeNavigationActions(action.subItems) : undefined,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
function describeNavigationItem(item) {
|
|
84
|
+
return {
|
|
85
|
+
id: item.id,
|
|
86
|
+
title: item.title,
|
|
87
|
+
link: item.link,
|
|
88
|
+
icon: item.icon || undefined,
|
|
89
|
+
target: item.type === 'blank' ? 'blank' : undefined,
|
|
90
|
+
model: item.modelResourceName || undefined,
|
|
91
|
+
actions: item.actions?.length ? describeNavigationActions(item.actions) : undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Field names of a model resource that this user may see or write. */
|
|
95
|
+
function fieldNames(adminizer, user, resource, action) {
|
|
96
|
+
return Object.keys(new DataAccessor(adminizer, user, resource, action).getFieldsConfig());
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Skills every agent gets for free: who it is talking to, and read/edit access
|
|
100
|
+
* scoped to exactly the data that user may reach in the admin panel. Because
|
|
101
|
+
* every call re-checks the user's permissions, an agent built on them can be
|
|
102
|
+
* offered to non-administrator accounts as well.
|
|
103
|
+
*/
|
|
104
|
+
export function buildBuiltinAgentSkills(adminizer) {
|
|
105
|
+
const currentUser = {
|
|
106
|
+
id: 'current_user',
|
|
107
|
+
description: 'Get the admin user this conversation belongs to: login, name, email, locale, groups and administrator flag.',
|
|
108
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
109
|
+
execute: (_input, { userIdentity }) => userIdentity,
|
|
110
|
+
};
|
|
111
|
+
const listModels = {
|
|
112
|
+
id: 'list_data_models',
|
|
113
|
+
description: 'List the data models the current user may read or edit, with their fields, allowed operations and admin URLs.',
|
|
114
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
115
|
+
execute: (_input, { user }) => ({
|
|
116
|
+
models: listPermittedModels(adminizer, user).map(({ resource, canRead, canUpdate, canCreate }) => ({
|
|
117
|
+
name: resource.name,
|
|
118
|
+
title: resource.config?.title ?? resource.name,
|
|
119
|
+
canRead,
|
|
120
|
+
canUpdate,
|
|
121
|
+
canCreate,
|
|
122
|
+
identifierField: identifierField(adminizer, resource),
|
|
123
|
+
listUrl: resource.uri,
|
|
124
|
+
recordUrlTemplate: canUpdate ? `${resource.uri}/edit/:id` : undefined,
|
|
125
|
+
fields: fieldNames(adminizer, user, resource, canUpdate ? 'edit' : 'list'),
|
|
126
|
+
})),
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
const listNavigation = {
|
|
130
|
+
id: 'list_admin_navigation',
|
|
131
|
+
description: 'Show the admin panel navigation menu of the current user: sections, their links with sub-actions,'
|
|
132
|
+
+ ' and the templates of parametrized pages such as a single record. Only entries this user may open are returned.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
query: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'Optional words to filter menu items by title, section or path. Omit to get the whole menu.',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
},
|
|
143
|
+
execute: (input, { user }) => {
|
|
144
|
+
const needle = slugify(input.query);
|
|
145
|
+
const matches = (...values) => !needle || slugify(values.filter(Boolean).join(' ')).includes(needle);
|
|
146
|
+
const sections = new Map();
|
|
147
|
+
for (const item of listAccessibleMenuItems(adminizer, user)) {
|
|
148
|
+
if (!matches(item.title, item.id, item.section, item.link, item.modelResourceName))
|
|
149
|
+
continue;
|
|
150
|
+
const section = item.section || 'Platform';
|
|
151
|
+
if (!sections.has(section))
|
|
152
|
+
sections.set(section, []);
|
|
153
|
+
sections.get(section).push(describeNavigationItem(item));
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
sections: [...sections].map(([section, items]) => ({ section, items })),
|
|
157
|
+
// Record and other parametrized pages have no fixed URL, so the
|
|
158
|
+
// agent gets them as templates to fill in.
|
|
159
|
+
templates: adminizer.adminLinkHandler.listTemplates(user)
|
|
160
|
+
.filter((template) => matches(template.title, template.id, template.section, template.description, template.template))
|
|
161
|
+
.map((template) => ({
|
|
162
|
+
id: template.id,
|
|
163
|
+
title: template.title,
|
|
164
|
+
template: template.template,
|
|
165
|
+
description: template.description,
|
|
166
|
+
section: template.section,
|
|
167
|
+
params: template.params,
|
|
168
|
+
})),
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const readRecords = {
|
|
173
|
+
id: 'read_model_records',
|
|
174
|
+
description: 'Read records of a data model the current user may read.',
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: {
|
|
178
|
+
model: { type: 'string', description: 'Model name as returned by list_data_models.' },
|
|
179
|
+
filter: { type: 'string', description: 'Optional JSON criteria object, e.g. {"title": "example"}.' },
|
|
180
|
+
fields: { type: 'array', items: { type: 'string' }, description: 'Optional subset of fields to return.' },
|
|
181
|
+
limit: {
|
|
182
|
+
type: 'integer', minimum: 1, maximum: MAX_RECORDS,
|
|
183
|
+
description: `Maximum number of records to return (default ${DEFAULT_RECORDS}).`,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
required: ['model'],
|
|
187
|
+
additionalProperties: false,
|
|
188
|
+
},
|
|
189
|
+
requiresUser: true,
|
|
190
|
+
describe: (user) => {
|
|
191
|
+
const models = modelNames(adminizer, user, 'read');
|
|
192
|
+
return {
|
|
193
|
+
description: `Read records of a data model the current user may read. Readable models: ${models.join(', ') || 'none'}.`,
|
|
194
|
+
inputSchema: withModelEnum(readRecords.inputSchema, models),
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
execute: async (input, { user }) => {
|
|
198
|
+
const resource = requireModel(adminizer, user, String(input.model ?? ''), 'read');
|
|
199
|
+
const criteria = parseCriteria(input.filter);
|
|
200
|
+
const limit = Math.min(Math.max(Math.trunc(Number(input.limit) || DEFAULT_RECORDS), 1), MAX_RECORDS);
|
|
201
|
+
const fields = Array.isArray(input.fields) ? input.fields.map(String) : [];
|
|
202
|
+
const accessor = new DataAccessor(adminizer, user, resource, 'list');
|
|
203
|
+
const records = await resource.model.find(criteria, accessor);
|
|
204
|
+
const limited = records.slice(0, limit);
|
|
205
|
+
return toJsonSafe({
|
|
206
|
+
model: resource.name,
|
|
207
|
+
total: records.length,
|
|
208
|
+
count: limited.length,
|
|
209
|
+
records: fields.length ? limited.map((record) => pickFields(record, fields)) : limited,
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
const updateRecord = {
|
|
214
|
+
id: 'update_model_record',
|
|
215
|
+
description: 'Update one record of a data model the current user may edit.',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
model: { type: 'string', description: 'Model name as returned by list_data_models.' },
|
|
220
|
+
id: { type: 'string', description: 'Identifier of the record to update.' },
|
|
221
|
+
values: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
description: 'Field values to write, e.g. {"title": "new title"}. Fields this user may not edit are dropped by the panel.',
|
|
224
|
+
additionalProperties: true,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ['model', 'id', 'values'],
|
|
228
|
+
additionalProperties: false,
|
|
229
|
+
},
|
|
230
|
+
requiresUser: true,
|
|
231
|
+
describe: (user) => {
|
|
232
|
+
const models = modelNames(adminizer, user, 'update');
|
|
233
|
+
return {
|
|
234
|
+
description: `Update one record of a data model the current user may edit. Editable models: ${models.join(', ') || 'none'}.`,
|
|
235
|
+
inputSchema: withModelEnum(updateRecord.inputSchema, models),
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
execute: async (input, { user }) => {
|
|
239
|
+
const resource = requireModel(adminizer, user, String(input.model ?? ''), 'update');
|
|
240
|
+
const id = String(input.id ?? '').trim();
|
|
241
|
+
if (!id)
|
|
242
|
+
throw new Error('id of the record to update is required');
|
|
243
|
+
const values = input.values && typeof input.values === 'object' && !Array.isArray(input.values)
|
|
244
|
+
? input.values
|
|
245
|
+
: null;
|
|
246
|
+
if (!values || !Object.keys(values).length)
|
|
247
|
+
throw new Error('values must be a non-empty object of field values');
|
|
248
|
+
const accessor = new DataAccessor(adminizer, user, resource, 'edit');
|
|
249
|
+
const record = await resource.model.updateOne({ [identifierField(adminizer, resource)]: id }, values, accessor);
|
|
250
|
+
if (!record)
|
|
251
|
+
throw new Error(`Record "${id}" was not found in "${resource.name}".`);
|
|
252
|
+
return toJsonSafe({ model: resource.name, id, record });
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
return [currentUser, listNavigation, listModels, readRecords, updateRecord];
|
|
256
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts arbitrary values (ORM records, Dates, Buffers, cyclic graphs) into
|
|
3
|
+
* plain JSON data.
|
|
4
|
+
*
|
|
5
|
+
* Tool results are stored verbatim in the agent's message history and are
|
|
6
|
+
* re-validated by the AI SDK against its `ModelMessage[]` schema on every
|
|
7
|
+
* subsequent turn. A single non-JSON value — a `Date` from a Sequelize record,
|
|
8
|
+
* for example — therefore does not fail the call that produced it: it poisons
|
|
9
|
+
* the session and every later turn dies with `Invalid prompt: The messages do
|
|
10
|
+
* not match the ModelMessage[] schema`. Passing tool output through this
|
|
11
|
+
* helper keeps that class of failure out of the history.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toJsonSafe<T>(value: T): unknown;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts arbitrary values (ORM records, Dates, Buffers, cyclic graphs) into
|
|
3
|
+
* plain JSON data.
|
|
4
|
+
*
|
|
5
|
+
* Tool results are stored verbatim in the agent's message history and are
|
|
6
|
+
* re-validated by the AI SDK against its `ModelMessage[]` schema on every
|
|
7
|
+
* subsequent turn. A single non-JSON value — a `Date` from a Sequelize record,
|
|
8
|
+
* for example — therefore does not fail the call that produced it: it poisons
|
|
9
|
+
* the session and every later turn dies with `Invalid prompt: The messages do
|
|
10
|
+
* not match the ModelMessage[] schema`. Passing tool output through this
|
|
11
|
+
* helper keeps that class of failure out of the history.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_DEPTH = 12;
|
|
14
|
+
export function toJsonSafe(value) {
|
|
15
|
+
return convert(value, new WeakSet(), 0);
|
|
16
|
+
}
|
|
17
|
+
function convert(value, seen, depth) {
|
|
18
|
+
if (value === null)
|
|
19
|
+
return null;
|
|
20
|
+
switch (typeof value) {
|
|
21
|
+
case 'string':
|
|
22
|
+
case 'boolean':
|
|
23
|
+
return value;
|
|
24
|
+
case 'number':
|
|
25
|
+
return Number.isFinite(value) ? value : null;
|
|
26
|
+
case 'bigint':
|
|
27
|
+
return value.toString();
|
|
28
|
+
case 'undefined':
|
|
29
|
+
case 'function':
|
|
30
|
+
case 'symbol':
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const object = value;
|
|
34
|
+
if (value instanceof Date)
|
|
35
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
36
|
+
if (value instanceof Error)
|
|
37
|
+
return value.message;
|
|
38
|
+
if (value instanceof RegExp)
|
|
39
|
+
return value.toString();
|
|
40
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))
|
|
41
|
+
return `<binary ${value.length} bytes>`;
|
|
42
|
+
if (ArrayBuffer.isView(value))
|
|
43
|
+
return `<binary ${value.byteLength} bytes>`;
|
|
44
|
+
if (value instanceof ArrayBuffer)
|
|
45
|
+
return `<binary ${value.byteLength} bytes>`;
|
|
46
|
+
if (seen.has(object))
|
|
47
|
+
return '[Circular]';
|
|
48
|
+
if (depth >= MAX_DEPTH)
|
|
49
|
+
return '[MaxDepth]';
|
|
50
|
+
seen.add(object);
|
|
51
|
+
try {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
// JSON.stringify turns array holes and `undefined` items into null.
|
|
54
|
+
return value.map((item) => {
|
|
55
|
+
const converted = convert(item, seen, depth + 1);
|
|
56
|
+
return converted === undefined ? null : converted;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (value instanceof Set)
|
|
60
|
+
return convert([...value], seen, depth);
|
|
61
|
+
if (value instanceof Map) {
|
|
62
|
+
return convert(Object.fromEntries([...value].map(([key, item]) => [String(key), item])), seen, depth);
|
|
63
|
+
}
|
|
64
|
+
// Sequelize instances, Decimal, Luxon and friends: let them describe
|
|
65
|
+
// themselves first, then normalize whatever they produced.
|
|
66
|
+
const toJSON = value.toJSON;
|
|
67
|
+
if (typeof toJSON === 'function') {
|
|
68
|
+
return convert(toJSON.call(value), seen, depth + 1);
|
|
69
|
+
}
|
|
70
|
+
const result = {};
|
|
71
|
+
for (const [key, item] of Object.entries(value)) {
|
|
72
|
+
const converted = convert(item, seen, depth + 1);
|
|
73
|
+
if (converted !== undefined)
|
|
74
|
+
result[key] = converted;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
seen.delete(object);
|
|
80
|
+
}
|
|
81
|
+
}
|