@lenne.tech/nest-server 11.25.5 → 11.26.0
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/.claude/rules/configurable-features.md +2 -0
- package/CLAUDE.md +18 -0
- package/FRAMEWORK-API.md +48 -1
- package/dist/config.env.js +35 -1
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/config.helper.js +2 -2
- package/dist/core/common/helpers/config.helper.js.map +1 -1
- package/dist/core/common/interceptors/check-security.interceptor.js +9 -2
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +89 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +21 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js +191 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -0
- package/dist/core/modules/ai/core-ai.controller.d.ts +90 -0
- package/dist/core/modules/ai/core-ai.controller.js +576 -0
- package/dist/core/modules/ai/core-ai.controller.js.map +1 -0
- package/dist/core/modules/ai/core-ai.module.d.ts +43 -0
- package/dist/core/modules/ai/core-ai.module.js +164 -0
- package/dist/core/modules/ai/core-ai.module.js.map +1 -0
- package/dist/core/modules/ai/core-ai.resolver.d.ts +84 -0
- package/dist/core/modules/ai/core-ai.resolver.js +530 -0
- package/dist/core/modules/ai/core-ai.resolver.js.map +1 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.d.ts +7 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +16 -0
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -0
- package/dist/core/modules/ai/hooks/ai-hook.base.d.ts +10 -0
- package/dist/core/modules/ai/hooks/ai-hook.base.js +30 -0
- package/dist/core/modules/ai/hooks/ai-hook.base.js.map +1 -0
- package/dist/core/modules/ai/hooks/ai-hook.registry.d.ts +22 -0
- package/dist/core/modules/ai/hooks/ai-hook.registry.js +98 -0
- package/dist/core/modules/ai/hooks/ai-hook.registry.js.map +1 -0
- package/dist/core/modules/ai/index.d.ts +69 -0
- package/dist/core/modules/ai/index.js +86 -0
- package/dist/core/modules/ai/index.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit-create.input.d.ts +5 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit-create.input.js +43 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit.input.d.ts +7 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit.input.js +74 -0
- package/dist/core/modules/ai/inputs/core-ai-budget-limit.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-create.input.d.ts +6 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-create.input.js +49 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-preference.input.d.ts +6 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-preference.input.js +60 -0
- package/dist/core/modules/ai/inputs/core-ai-connection-preference.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.d.ts +23 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.js +229 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation-create.input.d.ts +3 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation-create.input.js +21 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation.input.d.ts +4 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation.input.js +42 -0
- package/dist/core/modules/ai/inputs/core-ai-conversation.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-create.input.d.ts +10 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-create.input.js +74 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint-create.input.d.ts +5 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint-create.input.js +41 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint.input.d.ts +6 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint.input.js +63 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-hint.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-update.input.d.ts +10 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-update.input.js +70 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt-update.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt.input.d.ts +18 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt.input.js +131 -0
- package/dist/core/modules/ai/inputs/core-ai-prompt.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-create.input.d.ts +5 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-create.input.js +41 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-create.input.js.map +1 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-update.input.d.ts +9 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-update.input.js +91 -0
- package/dist/core/modules/ai/inputs/core-ai-slot-update.input.js.map +1 -0
- package/dist/core/modules/ai/interfaces/ai-hook.interface.d.ts +28 -0
- package/dist/core/modules/ai/interfaces/ai-hook.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/ai-hook.interface.js.map +1 -0
- package/dist/core/modules/ai/interfaces/ai-placeholder.interface.d.ts +21 -0
- package/dist/core/modules/ai/interfaces/ai-placeholder.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/ai-placeholder.interface.js.map +1 -0
- package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +30 -0
- package/dist/core/modules/ai/interfaces/ai-tool.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/ai-tool.interface.js.map +1 -0
- package/dist/core/modules/ai/interfaces/llm-provider.interface.d.ts +55 -0
- package/dist/core/modules/ai/interfaces/llm-provider.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/llm-provider.interface.js.map +1 -0
- package/dist/core/modules/ai/interfaces/resolved-ai-connection.interface.d.ts +16 -0
- package/dist/core/modules/ai/interfaces/resolved-ai-connection.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/resolved-ai-connection.interface.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-action.model.d.ts +6 -0
- package/dist/core/modules/ai/models/core-ai-action.model.js +44 -0
- package/dist/core/modules/ai/models/core-ai-action.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-available-connection.model.d.ts +8 -0
- package/dist/core/modules/ai/models/core-ai-available-connection.model.js +53 -0
- package/dist/core/modules/ai/models/core-ai-available-connection.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-budget-limit.model.d.ts +97 -0
- package/dist/core/modules/ai/models/core-ai-budget-limit.model.js +79 -0
- package/dist/core/modules/ai/models/core-ai-budget-limit.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-connection-preference.model.d.ts +91 -0
- package/dist/core/modules/ai/models/core-ai-connection-preference.model.js +67 -0
- package/dist/core/modules/ai/models/core-ai-connection-preference.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-connection.model.d.ts +200 -0
- package/dist/core/modules/ai/models/core-ai-connection.model.js +263 -0
- package/dist/core/modules/ai/models/core-ai-connection.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-conversation.model.d.ts +93 -0
- package/dist/core/modules/ai/models/core-ai-conversation.model.js +82 -0
- package/dist/core/modules/ai/models/core-ai-conversation.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-interaction.model.d.ts +133 -0
- package/dist/core/modules/ai/models/core-ai-interaction.model.js +137 -0
- package/dist/core/modules/ai/models/core-ai-interaction.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-message.model.d.ts +5 -0
- package/dist/core/modules/ai/models/core-ai-message.model.js +38 -0
- package/dist/core/modules/ai/models/core-ai-message.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-mode.model.d.ts +109 -0
- package/dist/core/modules/ai/models/core-ai-mode.model.js +100 -0
- package/dist/core/modules/ai/models/core-ai-mode.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-prompt-hint.model.d.ts +103 -0
- package/dist/core/modules/ai/models/core-ai-prompt-hint.model.js +89 -0
- package/dist/core/modules/ai/models/core-ai-prompt-hint.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-prompt.model.d.ts +122 -0
- package/dist/core/modules/ai/models/core-ai-prompt.model.js +117 -0
- package/dist/core/modules/ai/models/core-ai-prompt.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-response.model.d.ts +29 -0
- package/dist/core/modules/ai/models/core-ai-response.model.js +105 -0
- package/dist/core/modules/ai/models/core-ai-response.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-slot.model.d.ts +121 -0
- package/dist/core/modules/ai/models/core-ai-slot.model.js +119 -0
- package/dist/core/modules/ai/models/core-ai-slot.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-tool-grant.model.d.ts +103 -0
- package/dist/core/modules/ai/models/core-ai-tool-grant.model.js +88 -0
- package/dist/core/modules/ai/models/core-ai-tool-grant.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-tool-policy.model.d.ts +122 -0
- package/dist/core/modules/ai/models/core-ai-tool-policy.model.js +121 -0
- package/dist/core/modules/ai/models/core-ai-tool-policy.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-usage-info.model.d.ts +24 -0
- package/dist/core/modules/ai/models/core-ai-usage-info.model.js +127 -0
- package/dist/core/modules/ai/models/core-ai-usage-info.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-usage.model.d.ts +5 -0
- package/dist/core/modules/ai/models/core-ai-usage.model.js +38 -0
- package/dist/core/modules/ai/models/core-ai-usage.model.js.map +1 -0
- package/dist/core/modules/ai/providers/claude-cli.provider.d.ts +16 -0
- package/dist/core/modules/ai/providers/claude-cli.provider.js +151 -0
- package/dist/core/modules/ai/providers/claude-cli.provider.js.map +1 -0
- package/dist/core/modules/ai/providers/llm-provider.factory.d.ts +10 -0
- package/dist/core/modules/ai/providers/llm-provider.factory.js +45 -0
- package/dist/core/modules/ai/providers/llm-provider.factory.js.map +1 -0
- package/dist/core/modules/ai/providers/openai-compatible.provider.d.ts +32 -0
- package/dist/core/modules/ai/providers/openai-compatible.provider.js +236 -0
- package/dist/core/modules/ai/providers/openai-compatible.provider.js.map +1 -0
- package/dist/core/modules/ai/services/ai-crypto.service.d.ts +11 -0
- package/dist/core/modules/ai/services/ai-crypto.service.js +84 -0
- package/dist/core/modules/ai/services/ai-crypto.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-budget.service.d.ts +42 -0
- package/dist/core/modules/ai/services/core-ai-budget.service.js +211 -0
- package/dist/core/modules/ai/services/core-ai-budget.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.d.ts +18 -0
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.js +53 -0
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-connection-resolver.service.d.ts +55 -0
- package/dist/core/modules/ai/services/core-ai-connection-resolver.service.js +177 -0
- package/dist/core/modules/ai/services/core-ai-connection-resolver.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +43 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.js +272 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +26 -0
- package/dist/core/modules/ai/services/core-ai-conversation.service.js +72 -0
- package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-interaction.service.d.ts +13 -0
- package/dist/core/modules/ai/services/core-ai-interaction.service.js +52 -0
- package/dist/core/modules/ai/services/core-ai-interaction.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-mcp-client.service.d.ts +51 -0
- package/dist/core/modules/ai/services/core-ai-mcp-client.service.js +123 -0
- package/dist/core/modules/ai/services/core-ai-mcp-client.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.d.ts +68 -0
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +279 -0
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +22 -0
- package/dist/core/modules/ai/services/core-ai-mcp.service.js +59 -0
- package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-mode.service.d.ts +14 -0
- package/dist/core/modules/ai/services/core-ai-mode.service.js +56 -0
- package/dist/core/modules/ai/services/core-ai-mode.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-placeholder.registry.d.ts +15 -0
- package/dist/core/modules/ai/services/core-ai-placeholder.registry.js +102 -0
- package/dist/core/modules/ai/services/core-ai-placeholder.registry.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.d.ts +42 -0
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +136 -0
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.d.ts +29 -0
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js +98 -0
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-prompt.service.d.ts +21 -0
- package/dist/core/modules/ai/services/core-ai-prompt.service.js +143 -0
- package/dist/core/modules/ai/services/core-ai-prompt.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-slot.service.d.ts +51 -0
- package/dist/core/modules/ai/services/core-ai-slot.service.js +372 -0
- package/dist/core/modules/ai/services/core-ai-slot.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.d.ts +22 -0
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.js +97 -0
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.d.ts +23 -0
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.js +102 -0
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai.service.d.ts +144 -0
- package/dist/core/modules/ai/services/core-ai.service.js +991 -0
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -0
- package/dist/core/modules/ai/tools/ai-tool.base.d.ts +15 -0
- package/dist/core/modules/ai/tools/ai-tool.base.js +30 -0
- package/dist/core/modules/ai/tools/ai-tool.base.js.map +1 -0
- package/dist/core/modules/ai/tools/ai-tool.registry.d.ts +19 -0
- package/dist/core/modules/ai/tools/ai-tool.registry.js +71 -0
- package/dist/core/modules/ai/tools/ai-tool.registry.js.map +1 -0
- package/dist/core/modules/ai/tools/ask-user-question.tool.d.ts +38 -0
- package/dist/core/modules/ai/tools/ask-user-question.tool.js +65 -0
- package/dist/core/modules/ai/tools/ask-user-question.tool.js.map +1 -0
- package/dist/core/modules/ai/tools/search-tools.tool.d.ts +24 -0
- package/dist/core/modules/ai/tools/search-tools.tool.js +68 -0
- package/dist/core/modules/ai/tools/search-tools.tool.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +7 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +81 -0
- package/dist/core/modules/error-code/error-codes.js +72 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.service.js +3 -3
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
- package/dist/core.module.js +8 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/ai/ai-tools.module.d.ts +5 -0
- package/dist/server/modules/ai/ai-tools.module.js +36 -0
- package/dist/server/modules/ai/ai-tools.module.js.map +1 -0
- package/dist/server/modules/ai/tools/delete-user.tool.d.ts +30 -0
- package/dist/server/modules/ai/tools/delete-user.tool.js +49 -0
- package/dist/server/modules/ai/tools/delete-user.tool.js.map +1 -0
- package/dist/server/modules/ai/tools/find-users.tool.d.ts +30 -0
- package/dist/server/modules/ai/tools/find-users.tool.js +52 -0
- package/dist/server/modules/ai/tools/find-users.tool.js.map +1 -0
- package/dist/server/modules/ai/tools/get-user.tool.d.ts +26 -0
- package/dist/server/modules/ai/tools/get-user.tool.js +48 -0
- package/dist/server/modules/ai/tools/get-user.tool.js.map +1 -0
- package/dist/server/modules/ai/tools/update-user-job-title.tool.d.ts +32 -0
- package/dist/server/modules/ai/tools/update-user-job-title.tool.js +54 -0
- package/dist/server/modules/ai/tools/update-user-job-title.tool.js.map +1 -0
- package/dist/server/modules/error-code/error-codes.d.ts +9 -0
- package/dist/server/server.module.js +2 -0
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +10 -0
- package/migration-guides/11.25.x-to-11.26.0.md +278 -0
- package/package.json +44 -26
- package/src/config.env.ts +48 -1
- package/src/core/common/helpers/config.helper.ts +5 -2
- package/src/core/common/interceptors/check-security.interceptor.ts +13 -3
- package/src/core/common/interfaces/server-options.interface.ts +312 -0
- package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +220 -0
- package/src/core/modules/ai/README.md +526 -0
- package/src/core/modules/ai/core-ai-mcp.controller.ts +223 -0
- package/src/core/modules/ai/core-ai.controller.ts +547 -0
- package/src/core/modules/ai/core-ai.module.ts +269 -0
- package/src/core/modules/ai/core-ai.resolver.ts +526 -0
- package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +35 -0
- package/src/core/modules/ai/hooks/ai-hook.base.ts +23 -0
- package/src/core/modules/ai/hooks/ai-hook.registry.ts +119 -0
- package/src/core/modules/ai/index.ts +70 -0
- package/src/core/modules/ai/inputs/core-ai-budget-limit-create.input.ts +27 -0
- package/src/core/modules/ai/inputs/core-ai-budget-limit.input.ts +52 -0
- package/src/core/modules/ai/inputs/core-ai-connection-create.input.ts +32 -0
- package/src/core/modules/ai/inputs/core-ai-connection-preference.input.ts +42 -0
- package/src/core/modules/ai/inputs/core-ai-connection.input.ts +235 -0
- package/src/core/modules/ai/inputs/core-ai-conversation-create.input.ts +13 -0
- package/src/core/modules/ai/inputs/core-ai-conversation.input.ts +33 -0
- package/src/core/modules/ai/inputs/core-ai-prompt-create.input.ts +50 -0
- package/src/core/modules/ai/inputs/core-ai-prompt-hint-create.input.ts +26 -0
- package/src/core/modules/ai/inputs/core-ai-prompt-hint.input.ts +43 -0
- package/src/core/modules/ai/inputs/core-ai-prompt-update.input.ts +42 -0
- package/src/core/modules/ai/inputs/core-ai-prompt.input.ts +144 -0
- package/src/core/modules/ai/inputs/core-ai-slot-create.input.ts +26 -0
- package/src/core/modules/ai/inputs/core-ai-slot-update.input.ts +65 -0
- package/src/core/modules/ai/interfaces/ai-hook.interface.ts +74 -0
- package/src/core/modules/ai/interfaces/ai-placeholder.interface.ts +49 -0
- package/src/core/modules/ai/interfaces/ai-tool.interface.ts +112 -0
- package/src/core/modules/ai/interfaces/llm-provider.interface.ts +187 -0
- package/src/core/modules/ai/interfaces/resolved-ai-connection.interface.ts +55 -0
- package/src/core/modules/ai/models/core-ai-action.model.ts +37 -0
- package/src/core/modules/ai/models/core-ai-available-connection.model.ts +42 -0
- package/src/core/modules/ai/models/core-ai-budget-limit.model.ts +82 -0
- package/src/core/modules/ai/models/core-ai-connection-preference.model.ts +72 -0
- package/src/core/modules/ai/models/core-ai-connection.model.ts +330 -0
- package/src/core/modules/ai/models/core-ai-conversation.model.ts +87 -0
- package/src/core/modules/ai/models/core-ai-interaction.model.ts +145 -0
- package/src/core/modules/ai/models/core-ai-message.model.ts +29 -0
- package/src/core/modules/ai/models/core-ai-mode.model.ts +93 -0
- package/src/core/modules/ai/models/core-ai-prompt-hint.model.ts +98 -0
- package/src/core/modules/ai/models/core-ai-prompt.model.ts +120 -0
- package/src/core/modules/ai/models/core-ai-response.model.ts +119 -0
- package/src/core/modules/ai/models/core-ai-slot.model.ts +146 -0
- package/src/core/modules/ai/models/core-ai-tool-grant.model.ts +87 -0
- package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +115 -0
- package/src/core/modules/ai/models/core-ai-usage-info.model.ts +107 -0
- package/src/core/modules/ai/models/core-ai-usage.model.ts +29 -0
- package/src/core/modules/ai/providers/claude-cli.provider.ts +240 -0
- package/src/core/modules/ai/providers/llm-provider.factory.ts +63 -0
- package/src/core/modules/ai/providers/openai-compatible.provider.ts +329 -0
- package/src/core/modules/ai/services/ai-crypto.service.ts +120 -0
- package/src/core/modules/ai/services/core-ai-budget.service.ts +297 -0
- package/src/core/modules/ai/services/core-ai-connection-preference.service.ts +74 -0
- package/src/core/modules/ai/services/core-ai-connection-resolver.service.ts +308 -0
- package/src/core/modules/ai/services/core-ai-connection.service.ts +387 -0
- package/src/core/modules/ai/services/core-ai-conversation.service.ts +105 -0
- package/src/core/modules/ai/services/core-ai-interaction.service.ts +56 -0
- package/src/core/modules/ai/services/core-ai-mcp-client.service.ts +169 -0
- package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +445 -0
- package/src/core/modules/ai/services/core-ai-mcp.service.ts +94 -0
- package/src/core/modules/ai/services/core-ai-mode.service.ts +43 -0
- package/src/core/modules/ai/services/core-ai-placeholder.registry.ts +123 -0
- package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +228 -0
- package/src/core/modules/ai/services/core-ai-prompt-hint.service.ts +133 -0
- package/src/core/modules/ai/services/core-ai-prompt.service.ts +164 -0
- package/src/core/modules/ai/services/core-ai-slot.service.ts +526 -0
- package/src/core/modules/ai/services/core-ai-tool-grant.service.ts +107 -0
- package/src/core/modules/ai/services/core-ai-tool-policy.service.ts +124 -0
- package/src/core/modules/ai/services/core-ai.service.ts +1396 -0
- package/src/core/modules/ai/tools/ai-tool.base.ts +46 -0
- package/src/core/modules/ai/tools/ai-tool.registry.ts +124 -0
- package/src/core/modules/ai/tools/ask-user-question.tool.ts +76 -0
- package/src/core/modules/ai/tools/search-tools.tool.ts +70 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +15 -2
- package/src/core/modules/error-code/error-codes.ts +86 -0
- package/src/core/modules/tenant/core-tenant.service.ts +3 -3
- package/src/core.module.ts +12 -0
- package/src/index.ts +6 -0
- package/src/server/modules/ai/ai-tools.module.ts +33 -0
- package/src/server/modules/ai/tools/delete-user.tool.ts +44 -0
- package/src/server/modules/ai/tools/find-users.tool.ts +49 -0
- package/src/server/modules/ai/tools/get-user.tool.ts +43 -0
- package/src/server/modules/ai/tools/update-user-job-title.tool.ts +62 -0
- package/src/server/server.module.ts +5 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { ForbiddenException, Inject, Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { InjectModel } from '@nestjs/mongoose';
|
|
3
|
+
import { Model } from 'mongoose';
|
|
4
|
+
|
|
5
|
+
import { ServiceOptions } from '../../../common/interfaces/service-options.interface';
|
|
6
|
+
import { RoleEnum } from '../../../common/enums/role.enum';
|
|
7
|
+
import { CrudService } from '../../../common/services/crud.service';
|
|
8
|
+
import { CoreModelConstructor } from '../../../common/types/core-model-constructor.type';
|
|
9
|
+
import { CoreAiPromptCreateInput } from '../inputs/core-ai-prompt-create.input';
|
|
10
|
+
import { CoreAiPromptUpdateInput } from '../inputs/core-ai-prompt-update.input';
|
|
11
|
+
import { AiPromptDocument, CoreAiPrompt } from '../models/core-ai-prompt.model';
|
|
12
|
+
|
|
13
|
+
export const AI_PROMPT_MODEL = 'AiPrompt';
|
|
14
|
+
export const AI_PROMPT_CLASS = 'AI_PROMPT_CLASS';
|
|
15
|
+
|
|
16
|
+
const VALID_SCOPES = new Set(['tenant', 'user']);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* User-facing prompt store. End users can author re-usable prompts for
|
|
20
|
+
* themselves (`scope: 'user'` = private) or for their tenant
|
|
21
|
+
* (`scope: 'tenant'` = public). See {@link CoreAiPrompt}.
|
|
22
|
+
*
|
|
23
|
+
* `listVisible()` returns only prompts the caller is allowed to see (own +
|
|
24
|
+
* tenant). `create()` / `update()` / `delete()` enforce owner-only mutations
|
|
25
|
+
* (admins still pass via the standard admin pipeline).
|
|
26
|
+
*/
|
|
27
|
+
@Injectable()
|
|
28
|
+
export class CoreAiPromptService extends CrudService<CoreAiPrompt, CoreAiPromptCreateInput, CoreAiPromptUpdateInput> {
|
|
29
|
+
protected readonly logger = new Logger(CoreAiPromptService.name);
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
@InjectModel(AI_PROMPT_MODEL) protected override readonly mainDbModel: Model<AiPromptDocument>,
|
|
33
|
+
@Inject(AI_PROMPT_CLASS)
|
|
34
|
+
protected override readonly mainModelConstructor: CoreModelConstructor<CoreAiPrompt>,
|
|
35
|
+
) {
|
|
36
|
+
super();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override async create(input: CoreAiPromptCreateInput, serviceOptions: ServiceOptions = {}): Promise<CoreAiPrompt> {
|
|
40
|
+
const user = serviceOptions?.currentUser;
|
|
41
|
+
if (!user?.id) {
|
|
42
|
+
throw new ForbiddenException('Sign in to create a prompt.');
|
|
43
|
+
}
|
|
44
|
+
const scope = (input.scope || 'user').toLowerCase();
|
|
45
|
+
if (!VALID_SCOPES.has(scope)) {
|
|
46
|
+
throw new ForbiddenException(`Invalid prompt scope "${scope}".`);
|
|
47
|
+
}
|
|
48
|
+
const userTenantId = (user as any).tenantId || (user as any).currentTenantId || ((user as any).tenantIds || [])[0];
|
|
49
|
+
const tenantId = scope === 'tenant' ? (userTenantId ? String(userTenantId) : undefined) : undefined;
|
|
50
|
+
if (scope === 'tenant' && !tenantId) {
|
|
51
|
+
throw new ForbiddenException('Cannot share a prompt with a tenant when no tenant context exists.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Run the standard create pipeline first (validation + per-input whitelist).
|
|
55
|
+
// The ownerId/scope/tenantId are SYSTEM-OWNED — the input DTO deliberately
|
|
56
|
+
// doesn't expose them, so `prepareInput` would strip them if we passed them
|
|
57
|
+
// through `super.create(...)`. Persist them via a direct update afterwards,
|
|
58
|
+
// then reload the row through the full pipeline so the response carries
|
|
59
|
+
// every field (and `securityCheck` returns it to the owner).
|
|
60
|
+
const created = await super.create(input as any, serviceOptions);
|
|
61
|
+
await this.mainDbModel
|
|
62
|
+
.updateOne({ _id: created.id }, { $set: { ownerId: String(user.id), scope, tenantId } })
|
|
63
|
+
.exec();
|
|
64
|
+
return this.get(created.id, serviceOptions);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
override async update(
|
|
68
|
+
id: string,
|
|
69
|
+
input: CoreAiPromptUpdateInput,
|
|
70
|
+
serviceOptions: ServiceOptions = {},
|
|
71
|
+
): Promise<CoreAiPrompt> {
|
|
72
|
+
await this.assertOwner(id, serviceOptions);
|
|
73
|
+
// Strip read-only fields the client should never overwrite.
|
|
74
|
+
const sanitized: Record<string, unknown> = { ...input };
|
|
75
|
+
delete sanitized.ownerId;
|
|
76
|
+
delete sanitized.tenantId;
|
|
77
|
+
let scopeChange: null | { scope: string; tenantId?: string } = null;
|
|
78
|
+
if (sanitized.scope) {
|
|
79
|
+
const scope = String(sanitized.scope).toLowerCase();
|
|
80
|
+
if (!VALID_SCOPES.has(scope)) {
|
|
81
|
+
throw new ForbiddenException(`Invalid prompt scope "${scope}".`);
|
|
82
|
+
}
|
|
83
|
+
const user = serviceOptions?.currentUser;
|
|
84
|
+
sanitized.scope = scope;
|
|
85
|
+
let nextTenantId: string | undefined;
|
|
86
|
+
if (scope === 'tenant') {
|
|
87
|
+
const userTenantId =
|
|
88
|
+
(user as any)?.tenantId || (user as any)?.currentTenantId || ((user as any)?.tenantIds || [])[0];
|
|
89
|
+
nextTenantId = userTenantId ? String(userTenantId) : undefined;
|
|
90
|
+
if (!nextTenantId) {
|
|
91
|
+
throw new ForbiddenException('Cannot share a prompt with a tenant when no tenant context exists.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
scopeChange = { scope, tenantId: nextTenantId };
|
|
95
|
+
}
|
|
96
|
+
// Persist user-editable fields via the standard pipeline (whitelist /
|
|
97
|
+
// validation), then write the system-owned `tenantId` directly when the
|
|
98
|
+
// scope changed — the input DTO doesn't expose `tenantId`, so it would be
|
|
99
|
+
// stripped by `prepareInput` if passed through `super.update`.
|
|
100
|
+
const updated = await super.update(id, sanitized as any, serviceOptions);
|
|
101
|
+
if (scopeChange) {
|
|
102
|
+
await this.mainDbModel.updateOne({ _id: id }, { $set: { tenantId: scopeChange.tenantId } }).exec();
|
|
103
|
+
return this.get(id, serviceOptions);
|
|
104
|
+
}
|
|
105
|
+
return updated;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override async delete(id: string, serviceOptions: ServiceOptions = {}): Promise<CoreAiPrompt> {
|
|
109
|
+
await this.assertOwner(id, serviceOptions);
|
|
110
|
+
return super.delete(id, serviceOptions);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* List prompts visible to the current user: own + tenant prompts of the
|
|
115
|
+
* user's tenant. Ordered by `order` asc then `name`.
|
|
116
|
+
*/
|
|
117
|
+
async listVisible(serviceOptions: ServiceOptions = {}): Promise<CoreAiPrompt[]> {
|
|
118
|
+
const user = serviceOptions?.currentUser;
|
|
119
|
+
if (!user?.id) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const tenantId = (user as any).tenantId || (user as any).currentTenantId || ((user as any).tenantIds || [])[0];
|
|
123
|
+
const or: Record<string, unknown>[] = [{ ownerId: user.id }];
|
|
124
|
+
if (tenantId) {
|
|
125
|
+
or.push({ scope: 'tenant', tenantId: String(tenantId) });
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const rows = await this.mainDbModel
|
|
129
|
+
.find({ $or: or, enabled: { $ne: false } })
|
|
130
|
+
.sort({ order: 1, name: 1 })
|
|
131
|
+
.lean<CoreAiPrompt[]>()
|
|
132
|
+
.exec();
|
|
133
|
+
return rows || [];
|
|
134
|
+
} catch (err) {
|
|
135
|
+
this.logger.warn(`Failed to list prompts: ${(err as Error).message}`);
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Ensure the prompt at `id` is owned by the current user (or is admin). */
|
|
141
|
+
protected async assertOwner(id: string, serviceOptions: ServiceOptions): Promise<void> {
|
|
142
|
+
const user = serviceOptions?.currentUser;
|
|
143
|
+
if (!user?.id) {
|
|
144
|
+
throw new ForbiddenException('Sign in to modify a prompt.');
|
|
145
|
+
}
|
|
146
|
+
if ((user.roles || []).includes(RoleEnum.ADMIN)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const row = await this.mainDbModel.findById(id).lean<CoreAiPrompt>().exec();
|
|
151
|
+
if (!row) {
|
|
152
|
+
throw new ForbiddenException(`Prompt ${id} not found.`);
|
|
153
|
+
}
|
|
154
|
+
if (row.ownerId !== user.id) {
|
|
155
|
+
throw new ForbiddenException('Only the owner can modify this prompt.');
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof ForbiddenException) {
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
throw new ForbiddenException(`Could not verify prompt ownership: ${(err as Error).message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { ForbiddenException, Inject, Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { InjectModel } from '@nestjs/mongoose';
|
|
3
|
+
import { Model } from 'mongoose';
|
|
4
|
+
|
|
5
|
+
import { RoleEnum } from '../../../common/enums/role.enum';
|
|
6
|
+
import { ServiceOptions } from '../../../common/interfaces/service-options.interface';
|
|
7
|
+
import { CrudService } from '../../../common/services/crud.service';
|
|
8
|
+
import { RequestContext } from '../../../common/services/request-context.service';
|
|
9
|
+
import { CoreModelConstructor } from '../../../common/types/core-model-constructor.type';
|
|
10
|
+
import { CoreAiSlotCreateInput } from '../inputs/core-ai-slot-create.input';
|
|
11
|
+
import { CoreAiSlotUpdateInput } from '../inputs/core-ai-slot-update.input';
|
|
12
|
+
import { AiSlotDocument, CoreAiSlot } from '../models/core-ai-slot.model';
|
|
13
|
+
|
|
14
|
+
/** Mongoose injection token for the slot model. */
|
|
15
|
+
export const AI_SLOT_MODEL = 'AiSlot';
|
|
16
|
+
|
|
17
|
+
/** DI token for the slot model constructor. */
|
|
18
|
+
export const AI_SLOT_CLASS = 'AI_SLOT_CLASS';
|
|
19
|
+
|
|
20
|
+
/** A resolved prompt fragment ready for placeholder rendering + assembly. */
|
|
21
|
+
export interface ResolvedPromptFragment {
|
|
22
|
+
capability?: string;
|
|
23
|
+
content: string;
|
|
24
|
+
key: string;
|
|
25
|
+
order: number;
|
|
26
|
+
scope?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A slot row enriched with system/override metadata, returned by
|
|
31
|
+
* {@link CoreAiSlotService.listEffective} for the admin UI:
|
|
32
|
+
*
|
|
33
|
+
* - `isSystem: true` rows are framework defaults (no DB row) — virtual entries
|
|
34
|
+
* that exist purely so the UI can render them and let admins override.
|
|
35
|
+
* - `isSystem: false` + `isOverride: true` rows override a system default for
|
|
36
|
+
* the current tenant (admin edited an existing system slot).
|
|
37
|
+
* - `isSystem: false` + `isOverride: false` rows are custom tenant-only slots
|
|
38
|
+
* (admin created a new key that isn't a system default).
|
|
39
|
+
*
|
|
40
|
+
* `id` is missing on virtual system rows; UI shows "Bearbeiten" → creates an
|
|
41
|
+
* override; "Zurücksetzen" deletes the override row; "Löschen" on a system
|
|
42
|
+
* row creates a disabled override (soft-delete for that tenant);
|
|
43
|
+
* "Wiederherstellen" deletes the disabled override; "Löschen" on a custom
|
|
44
|
+
* row is a real delete.
|
|
45
|
+
*/
|
|
46
|
+
export interface EffectiveSlot extends ResolvedPromptFragment {
|
|
47
|
+
capability?: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
id?: string;
|
|
51
|
+
/** True when a tenant-specific row overrides a system default. */
|
|
52
|
+
isOverride: boolean;
|
|
53
|
+
/** True for built-in framework defaults (no DB row). */
|
|
54
|
+
isSystem: boolean;
|
|
55
|
+
locale?: string;
|
|
56
|
+
/** Key matching a built-in system default — only set on override rows. */
|
|
57
|
+
systemKey?: string;
|
|
58
|
+
tenantId?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Built-in framework default slots. Returned by {@link getSystemDefaultSlots};
|
|
63
|
+
* the prompt builder + slot service both consume them. Extracted to a module
|
|
64
|
+
* function so neither service has to depend on the other.
|
|
65
|
+
*
|
|
66
|
+
* Override via `ai.systemPrompt` (replaces the `base` slot content) or by
|
|
67
|
+
* subclassing {@link CoreAiPromptBuilderService}.
|
|
68
|
+
*/
|
|
69
|
+
export function getSystemDefaultSlots(baseSystemPrompt?: string): ResolvedPromptFragment[] {
|
|
70
|
+
const base =
|
|
71
|
+
baseSystemPrompt ||
|
|
72
|
+
'You are a helpful assistant integrated into a business application. ' +
|
|
73
|
+
'Answer concisely and only use information you can obtain through the provided tools. ' +
|
|
74
|
+
'Never invent data. If a request cannot be fulfilled with the available tools, say so.';
|
|
75
|
+
return [
|
|
76
|
+
{ content: base, key: 'base', order: 10 },
|
|
77
|
+
{ content: 'System documentation:\n{{documentation}}', key: 'documentation', order: 20 },
|
|
78
|
+
{
|
|
79
|
+
content:
|
|
80
|
+
'Your permissions and capabilities:\n' +
|
|
81
|
+
'- roles: {{roles}}\n' +
|
|
82
|
+
'- available tools (you may ONLY use these): {{tools}}\n' +
|
|
83
|
+
'Never claim to perform an action you have no tool for, and never assume rights you do not have. ' +
|
|
84
|
+
'Tools are executed with the current user permissions; the backend rejects anything beyond them.',
|
|
85
|
+
key: 'permissions',
|
|
86
|
+
order: 30,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
content:
|
|
90
|
+
'Accuracy rules — follow strictly:\n' +
|
|
91
|
+
'- NEVER invent, guess or assume data (ids, names, emails, numbers, dates, statuses).\n' +
|
|
92
|
+
'- Use a tool to obtain any fact you are unsure about.\n' +
|
|
93
|
+
"- If no available tool can provide the needed information, tell the user you don't have it " +
|
|
94
|
+
'instead of fabricating an answer.\n' +
|
|
95
|
+
'- Only report a value you actually received from a tool result.',
|
|
96
|
+
key: 'anti_hallucination',
|
|
97
|
+
order: 40,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
capability: 'native',
|
|
101
|
+
content:
|
|
102
|
+
'You can call the provided tools (native function calling). Available tools:\n{{toolCatalog}}\n' +
|
|
103
|
+
'Call a tool whenever it is needed to obtain data or perform an action.',
|
|
104
|
+
key: 'tool_catalog',
|
|
105
|
+
order: 50,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
capability: 'emulated',
|
|
109
|
+
content:
|
|
110
|
+
'You can call backend tools to fetch or modify data. Available tools:\n{{toolCatalog}}\n\n' +
|
|
111
|
+
'To call tools, respond with ONLY a JSON object (no prose, no markdown code fences):\n' +
|
|
112
|
+
'{"tool_calls":[{"name":"<tool_name>","arguments":{ ... }}]}\n' +
|
|
113
|
+
'You may request multiple tools at once. After emitting tool_calls, STOP and output nothing ' +
|
|
114
|
+
'else — do NOT write the results yourself. The system will send the real results back in a ' +
|
|
115
|
+
'message starting with "TOOL_RESULTS:"; only then continue.\n\n' +
|
|
116
|
+
'CRITICAL: To perform any action you MUST emit a tool_calls request and wait for its ' +
|
|
117
|
+
'TOOL_RESULTS. Never state in a final answer that you executed, performed, deleted, updated, ' +
|
|
118
|
+
'or created anything unless you actually called the matching tool and received its results. ' +
|
|
119
|
+
'If you have not called the tool yet, call it — do not claim success.',
|
|
120
|
+
key: 'tool_protocol_emulated',
|
|
121
|
+
order: 50,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
content:
|
|
125
|
+
'PLAN MODE: Do NOT execute anything. Available tools:\n{{toolCatalog}}\n\n' +
|
|
126
|
+
'Respond with ONLY a JSON object describing the COMPLETE ordered plan of tool calls needed to ' +
|
|
127
|
+
'fulfil the request:\n' +
|
|
128
|
+
'{"plan":[{"name":"<tool_name>","arguments":{ ... }}],"summary":"<short summary>"}\n' +
|
|
129
|
+
'List every required step in order. If no tools are needed, return an empty plan array. ' +
|
|
130
|
+
'Reply with valid JSON only — no prose, no markdown code fences.',
|
|
131
|
+
key: 'plan_protocol',
|
|
132
|
+
order: 55,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
capability: 'emulated',
|
|
136
|
+
content:
|
|
137
|
+
'When you have the final answer for the user, respond with ONLY a JSON object:\n' +
|
|
138
|
+
'{"final":"<your natural language answer>","data": <optional structured data or null>}\n' +
|
|
139
|
+
'Never mix tool_calls and final in the same response. Always reply with valid JSON only.',
|
|
140
|
+
key: 'output_contract',
|
|
141
|
+
order: 60,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
content:
|
|
145
|
+
'Tool error handling: a tool result with "success": false includes an "error" object ' +
|
|
146
|
+
'({ code, message, hint }). When that happens, do NOT pretend it worked. Read the hint, ' +
|
|
147
|
+
'correct your arguments and retry if it is sensible, otherwise clearly explain the problem to ' +
|
|
148
|
+
'the user in plain language.',
|
|
149
|
+
key: 'error_guidance',
|
|
150
|
+
order: 70,
|
|
151
|
+
},
|
|
152
|
+
{ content: 'Learned guidance (avoid past mistakes):\n{{learnedHints}}', key: 'learned_hints', order: 80 },
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Tenant-scoped store of system-prompt slots. Ships built-in defaults via
|
|
158
|
+
* {@link getSystemDefaultSlots} so the prompt works with zero DB rows. An
|
|
159
|
+
* admin-stored row OVERRIDES the framework default for the same `key` within
|
|
160
|
+
* the admin's tenant; custom keys add additional slots for that tenant.
|
|
161
|
+
*
|
|
162
|
+
* Tenancy enforcement:
|
|
163
|
+
* - Every CRUD operation auto-sets / filters by the admin's tenant (via
|
|
164
|
+
* `RequestContext.getTenantId()` or `serviceOptions.currentUser.tenantId`).
|
|
165
|
+
* - When multi-tenancy is OFF, `tenantId` stays undefined and slots are
|
|
166
|
+
* effectively system-wide for that deployment.
|
|
167
|
+
*
|
|
168
|
+
* Override this class via `CoreModule.forRoot(env, { ai: { slotService } })`.
|
|
169
|
+
*/
|
|
170
|
+
@Injectable()
|
|
171
|
+
export class CoreAiSlotService extends CrudService<CoreAiSlot, CoreAiSlotCreateInput, CoreAiSlotUpdateInput> {
|
|
172
|
+
protected readonly logger = new Logger(CoreAiSlotService.name);
|
|
173
|
+
|
|
174
|
+
constructor(
|
|
175
|
+
@InjectModel(AI_SLOT_MODEL) protected override readonly mainDbModel: Model<AiSlotDocument>,
|
|
176
|
+
@Inject(AI_SLOT_CLASS)
|
|
177
|
+
protected override readonly mainModelConstructor: CoreModelConstructor<CoreAiSlot>,
|
|
178
|
+
) {
|
|
179
|
+
super();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create or replace a slot for the current tenant. `tenantId` is set
|
|
184
|
+
* system-side from the calling admin — it's NEVER taken from the client
|
|
185
|
+
* input. **Idempotent by `(tenantId, key)`:** if an existing row for the
|
|
186
|
+
* same tenant + key is found, it's UPDATED in place instead of creating
|
|
187
|
+
* a duplicate. This makes "Override anlegen" / "Deaktivieren" safely
|
|
188
|
+
* repeatable in the admin UI without leaking orphan rows.
|
|
189
|
+
*/
|
|
190
|
+
override async create(input: CoreAiSlotCreateInput, serviceOptions: ServiceOptions = {}): Promise<CoreAiSlot> {
|
|
191
|
+
this.assertAdmin(serviceOptions);
|
|
192
|
+
const tenantId = this.tenantOf(serviceOptions);
|
|
193
|
+
if (input?.key) {
|
|
194
|
+
const existing = await this.mainDbModel
|
|
195
|
+
.findOne({ key: input.key, tenantId: tenantId ?? { $in: [null, undefined] } })
|
|
196
|
+
.lean<CoreAiSlot>()
|
|
197
|
+
.exec();
|
|
198
|
+
if (existing?.id || (existing as any)?._id) {
|
|
199
|
+
const existingId = String(existing?.id || (existing as any)?._id);
|
|
200
|
+
const updated = await super.update(existingId, input as any, serviceOptions);
|
|
201
|
+
return this.decorateWithSystemFlags(updated);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const created = await super.create(input as any, serviceOptions);
|
|
205
|
+
await this.mainDbModel.updateOne({ _id: created.id }, { $set: { tenantId } }).exec();
|
|
206
|
+
const reloaded = await this.get(created.id, serviceOptions);
|
|
207
|
+
return this.decorateWithSystemFlags(reloaded);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Annotate a freshly persisted slot with `isSystem` / `isOverride` flags so
|
|
212
|
+
* the admin UI gets the same shape from `create`/`update` as from
|
|
213
|
+
* `listEffective`. The flags are computed against the built-in system-default
|
|
214
|
+
* key set: a row whose key matches a system default is an override
|
|
215
|
+
* (`isOverride: true`); anything else is a custom tenant-only slot
|
|
216
|
+
* (`isOverride: false`). DB rows are never `isSystem: true` — system
|
|
217
|
+
* defaults are virtual.
|
|
218
|
+
*/
|
|
219
|
+
protected decorateWithSystemFlags(slot: CoreAiSlot): CoreAiSlot {
|
|
220
|
+
const systemKeys = new Set(getSystemDefaultSlots().map((d) => d.key));
|
|
221
|
+
return Object.assign(slot, {
|
|
222
|
+
isOverride: systemKeys.has(slot.key),
|
|
223
|
+
isSystem: false,
|
|
224
|
+
systemKey: systemKeys.has(slot.key) ? slot.key : undefined,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Update a slot. The slot must belong to the calling admin's tenant.
|
|
230
|
+
* `tenantId` is never overwritten from the client input.
|
|
231
|
+
*/
|
|
232
|
+
override async update(
|
|
233
|
+
id: string,
|
|
234
|
+
input: CoreAiSlotUpdateInput,
|
|
235
|
+
serviceOptions: ServiceOptions = {},
|
|
236
|
+
): Promise<CoreAiSlot> {
|
|
237
|
+
this.assertAdmin(serviceOptions);
|
|
238
|
+
await this.assertSameTenant(id, serviceOptions);
|
|
239
|
+
const sanitized: Record<string, unknown> = { ...input };
|
|
240
|
+
delete sanitized.tenantId;
|
|
241
|
+
const updated = await super.update(id, sanitized as any, serviceOptions);
|
|
242
|
+
return this.decorateWithSystemFlags(updated);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Delete a slot. The slot must belong to the calling admin's tenant.
|
|
247
|
+
*/
|
|
248
|
+
override async delete(id: string, serviceOptions: ServiceOptions = {}): Promise<CoreAiSlot> {
|
|
249
|
+
this.assertAdmin(serviceOptions);
|
|
250
|
+
await this.assertSameTenant(id, serviceOptions);
|
|
251
|
+
return super.delete(id, serviceOptions);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Effective slots for the admin UI: the framework defaults overlaid by the
|
|
256
|
+
* tenant's stored overrides + custom rows. Each entry carries
|
|
257
|
+
* `isSystem` / `isOverride` flags so the UI can render the right action
|
|
258
|
+
* (Bearbeiten / Zurücksetzen / Löschen / Wiederherstellen).
|
|
259
|
+
*/
|
|
260
|
+
async listEffective(serviceOptions: ServiceOptions = {}): Promise<EffectiveSlot[]> {
|
|
261
|
+
this.assertAdmin(serviceOptions);
|
|
262
|
+
const tenantId = this.tenantOf(serviceOptions);
|
|
263
|
+
const defaults = getSystemDefaultSlots();
|
|
264
|
+
const defaultKeys = new Set(defaults.map((d) => d.key));
|
|
265
|
+
|
|
266
|
+
let rows: CoreAiSlot[] = [];
|
|
267
|
+
try {
|
|
268
|
+
rows = await this.mainDbModel
|
|
269
|
+
.find(tenantId ? { tenantId } : { $or: [{ tenantId: { $exists: false } }, { tenantId: null }] })
|
|
270
|
+
.lean<CoreAiSlot[]>()
|
|
271
|
+
.exec();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
this.logger.warn(`listEffective failed to load rows: ${(err as Error).message}`);
|
|
274
|
+
rows = [];
|
|
275
|
+
}
|
|
276
|
+
// Tenant-row index by `key` for quick lookup (one override per key — multiple
|
|
277
|
+
// rows with the same key just take the most-recent).
|
|
278
|
+
const byKey = new Map<string, CoreAiSlot>();
|
|
279
|
+
for (const row of rows) {
|
|
280
|
+
if (!row?.key) continue;
|
|
281
|
+
byKey.set(row.key, row);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const out: EffectiveSlot[] = [];
|
|
285
|
+
// 1. System defaults (with optional override applied)
|
|
286
|
+
for (const def of defaults) {
|
|
287
|
+
const override = byKey.get(def.key);
|
|
288
|
+
if (override) {
|
|
289
|
+
out.push({
|
|
290
|
+
capability: override.capability ?? def.capability,
|
|
291
|
+
content: override.content ?? def.content,
|
|
292
|
+
description: override.description,
|
|
293
|
+
enabled: override.enabled !== false,
|
|
294
|
+
id: override.id,
|
|
295
|
+
isOverride: true,
|
|
296
|
+
isSystem: false,
|
|
297
|
+
key: override.key,
|
|
298
|
+
locale: override.locale,
|
|
299
|
+
order: typeof override.order === 'number' ? override.order : def.order,
|
|
300
|
+
scope: override.scope ?? def.scope,
|
|
301
|
+
systemKey: def.key,
|
|
302
|
+
tenantId: override.tenantId,
|
|
303
|
+
});
|
|
304
|
+
} else {
|
|
305
|
+
out.push({
|
|
306
|
+
capability: def.capability,
|
|
307
|
+
content: def.content,
|
|
308
|
+
enabled: true,
|
|
309
|
+
isOverride: false,
|
|
310
|
+
isSystem: true,
|
|
311
|
+
key: def.key,
|
|
312
|
+
order: def.order,
|
|
313
|
+
scope: def.scope,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// 2. Custom tenant slots (keys NOT in the framework defaults)
|
|
318
|
+
for (const row of rows) {
|
|
319
|
+
if (!row?.key || defaultKeys.has(row.key)) continue;
|
|
320
|
+
out.push({
|
|
321
|
+
capability: row.capability,
|
|
322
|
+
content: row.content,
|
|
323
|
+
description: row.description,
|
|
324
|
+
enabled: row.enabled !== false,
|
|
325
|
+
id: row.id,
|
|
326
|
+
isOverride: false,
|
|
327
|
+
isSystem: false,
|
|
328
|
+
key: row.key,
|
|
329
|
+
locale: row.locale,
|
|
330
|
+
order: typeof row.order === 'number' ? row.order : 100,
|
|
331
|
+
scope: row.scope,
|
|
332
|
+
tenantId: row.tenantId,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return out.sort((a, b) => a.order - b.order);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Reset a tenant override: delete the row that overrides a system default,
|
|
340
|
+
* restoring the framework value. The slot must (a) belong to the tenant
|
|
341
|
+
* and (b) match a system-default `key` — calling this on a custom slot
|
|
342
|
+
* is rejected (use {@link delete} for that).
|
|
343
|
+
*
|
|
344
|
+
* Returns the now-effective slot for that key (a synthetic
|
|
345
|
+
* `isSystem: true, isOverride: false` shape) so the caller can refresh its
|
|
346
|
+
* UI without a second list call. The slot has no `id` because system
|
|
347
|
+
* defaults are virtual rows (they live in code, not in the DB).
|
|
348
|
+
*/
|
|
349
|
+
async resetSystemSlot(id: string, serviceOptions: ServiceOptions = {}): Promise<EffectiveSlot> {
|
|
350
|
+
this.assertAdmin(serviceOptions);
|
|
351
|
+
await this.assertSameTenant(id, serviceOptions);
|
|
352
|
+
const row = await this.mainDbModel.findById(id).lean<CoreAiSlot>().exec();
|
|
353
|
+
if (!row) {
|
|
354
|
+
throw new ForbiddenException(`Slot ${id} not found.`);
|
|
355
|
+
}
|
|
356
|
+
const defaults = getSystemDefaultSlots();
|
|
357
|
+
const fallback = defaults.find((d) => d.key === row.key);
|
|
358
|
+
if (!fallback) {
|
|
359
|
+
throw new ForbiddenException('Only system-slot overrides can be reset. Use delete for custom slots.');
|
|
360
|
+
}
|
|
361
|
+
await this.mainDbModel.deleteOne({ _id: id }).exec();
|
|
362
|
+
return {
|
|
363
|
+
capability: fallback.capability,
|
|
364
|
+
content: fallback.content,
|
|
365
|
+
enabled: true,
|
|
366
|
+
isOverride: false,
|
|
367
|
+
isSystem: true,
|
|
368
|
+
key: fallback.key,
|
|
369
|
+
order: fallback.order ?? 100,
|
|
370
|
+
scope: fallback.scope,
|
|
371
|
+
systemKey: fallback.key,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Resolve the effective, ordered prompt fragments for a run: the built-in
|
|
377
|
+
* defaults overlaid by the current tenant's DB rows (key match, honoring
|
|
378
|
+
* locale + capability + `scope`). Placeholders are NOT yet rendered — the
|
|
379
|
+
* prompt builder does that.
|
|
380
|
+
*
|
|
381
|
+
* Tenant: pulled from `RequestContext.getTenantId()`. When undefined (no
|
|
382
|
+
* multi-tenancy active), only rows without `tenantId` apply.
|
|
383
|
+
*/
|
|
384
|
+
async resolveFragments(
|
|
385
|
+
defaults: ResolvedPromptFragment[],
|
|
386
|
+
options?: { capability?: string; locale?: string; scopes?: string[] },
|
|
387
|
+
): Promise<ResolvedPromptFragment[]> {
|
|
388
|
+
const capability = options?.capability ?? 'all';
|
|
389
|
+
const locale = options?.locale;
|
|
390
|
+
const scopes = options?.scopes ?? [];
|
|
391
|
+
const tenantId = RequestContext.getTenantId();
|
|
392
|
+
|
|
393
|
+
// Start from the built-in defaults keyed by their slot.
|
|
394
|
+
const byKey = new Map<string, ResolvedPromptFragment>();
|
|
395
|
+
for (const def of defaults) {
|
|
396
|
+
if (this.fragmentApplies(def.capability, capability) && this.scopeApplies(def.scope, scopes)) {
|
|
397
|
+
byKey.set(def.key, { ...def });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Overlay DB rows (enabled only, current tenant only).
|
|
402
|
+
let rows: CoreAiSlot[] = [];
|
|
403
|
+
try {
|
|
404
|
+
const tenantFilter = tenantId
|
|
405
|
+
? { tenantId: String(tenantId) }
|
|
406
|
+
: { $or: [{ tenantId: { $exists: false } }, { tenantId: null }] };
|
|
407
|
+
rows = await this.mainDbModel
|
|
408
|
+
.find({ enabled: { $ne: false }, ...tenantFilter })
|
|
409
|
+
.lean<CoreAiSlot[]>()
|
|
410
|
+
.exec();
|
|
411
|
+
} catch {
|
|
412
|
+
rows = [];
|
|
413
|
+
}
|
|
414
|
+
// Also consider disabled overrides of system keys: they HIDE the default
|
|
415
|
+
// for this tenant ("soft delete" the system slot).
|
|
416
|
+
let disabled: CoreAiSlot[] = [];
|
|
417
|
+
try {
|
|
418
|
+
const tenantFilter = tenantId
|
|
419
|
+
? { tenantId: String(tenantId) }
|
|
420
|
+
: { $or: [{ tenantId: { $exists: false } }, { tenantId: null }] };
|
|
421
|
+
disabled = await this.mainDbModel
|
|
422
|
+
.find({ enabled: false, ...tenantFilter })
|
|
423
|
+
.lean<CoreAiSlot[]>()
|
|
424
|
+
.exec();
|
|
425
|
+
} catch {
|
|
426
|
+
disabled = [];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const rank = (row: CoreAiSlot): number =>
|
|
430
|
+
(row.locale && row.locale === locale ? 2 : row.locale ? 0 : 1) + (row.scope ? 4 : 0);
|
|
431
|
+
const chosen = new Map<string, { rank: number; row: CoreAiSlot }>();
|
|
432
|
+
for (const row of rows) {
|
|
433
|
+
if (!row?.key || !row?.content) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (row.locale && locale && row.locale !== locale) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (!this.fragmentApplies(row.capability, capability)) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (!this.scopeApplies(row.scope, scopes)) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const r = rank(row);
|
|
446
|
+
const current = chosen.get(row.key);
|
|
447
|
+
if (!current || r > current.rank) {
|
|
448
|
+
chosen.set(row.key, { rank: r, row });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
for (const [key, { row }] of chosen) {
|
|
452
|
+
byKey.set(key, {
|
|
453
|
+
capability: row.capability,
|
|
454
|
+
content: row.content,
|
|
455
|
+
key,
|
|
456
|
+
order: typeof row.order === 'number' ? row.order : (byKey.get(key)?.order ?? 100),
|
|
457
|
+
scope: row.scope,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
// Soft-deleted system slots: remove them from the result for this tenant.
|
|
461
|
+
for (const row of disabled) {
|
|
462
|
+
if (row?.key && byKey.has(row.key)) {
|
|
463
|
+
byKey.delete(row.key);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return [...byKey.values()].filter((f) => f.content?.trim()).sort((a, b) => a.order - b.order);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Throws when the slot at `id` doesn't belong to the calling admin's tenant.
|
|
472
|
+
* Projects to `tenantId` only — cheaper than loading the full document just
|
|
473
|
+
* to compare tenant ownership.
|
|
474
|
+
*/
|
|
475
|
+
protected async assertSameTenant(id: string, serviceOptions: ServiceOptions): Promise<void> {
|
|
476
|
+
const tenantId = this.tenantOf(serviceOptions);
|
|
477
|
+
const row = await this.mainDbModel.findById(id, { tenantId: 1 }).lean<{ tenantId?: string }>().exec();
|
|
478
|
+
if (!row) {
|
|
479
|
+
throw new ForbiddenException(`Slot ${id} not found.`);
|
|
480
|
+
}
|
|
481
|
+
const rowTenant = row.tenantId ? String(row.tenantId) : undefined;
|
|
482
|
+
const callerTenant = tenantId ? String(tenantId) : undefined;
|
|
483
|
+
if (rowTenant !== callerTenant) {
|
|
484
|
+
throw new ForbiddenException('Slot belongs to a different tenant.');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Throws when the caller is not an admin. */
|
|
489
|
+
protected assertAdmin(serviceOptions: ServiceOptions): void {
|
|
490
|
+
const roles = serviceOptions?.currentUser?.roles || [];
|
|
491
|
+
if (!roles.includes(RoleEnum.ADMIN)) {
|
|
492
|
+
throw new ForbiddenException('Slot management requires admin role.');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Whether a slot's capability scope applies to the run's capability.
|
|
498
|
+
* `undefined`/`'all'` always applies; otherwise must match exactly.
|
|
499
|
+
*/
|
|
500
|
+
protected fragmentApplies(fragmentCapability: string | undefined, runCapability: string): boolean {
|
|
501
|
+
if (!fragmentCapability || fragmentCapability === 'all') {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
return fragmentCapability === runCapability;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Whether a slot's `scope` filter applies to the run's active scopes.
|
|
509
|
+
* Empty/undefined scope always applies; otherwise the slot scope must equal
|
|
510
|
+
* one of the active run scopes (exact-match — no patterns).
|
|
511
|
+
*/
|
|
512
|
+
protected scopeApplies(fragmentScope: string | undefined, runScopes: string[]): boolean {
|
|
513
|
+
if (!fragmentScope) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
return runScopes.includes(fragmentScope);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Tenant id of the calling user (request context or user object). */
|
|
520
|
+
protected tenantOf(serviceOptions: ServiceOptions): string | undefined {
|
|
521
|
+
const ctx = RequestContext.getTenantId();
|
|
522
|
+
if (ctx) return String(ctx);
|
|
523
|
+
const user = serviceOptions?.currentUser as any;
|
|
524
|
+
return user?.tenantId || user?.currentTenantId || user?.tenantIds?.[0];
|
|
525
|
+
}
|
|
526
|
+
}
|