@lenne.tech/nest-server 11.25.6 → 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/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.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 +2 -1
- 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/error-code/error-codes.ts +86 -0
- 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,297 @@
|
|
|
1
|
+
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import { InjectConnection, InjectModel } from '@nestjs/mongoose';
|
|
3
|
+
import { Connection, Model } from 'mongoose';
|
|
4
|
+
|
|
5
|
+
import { ConfigService } from '../../../common/services/config.service';
|
|
6
|
+
import { CrudService } from '../../../common/services/crud.service';
|
|
7
|
+
import { CoreModelConstructor } from '../../../common/types/core-model-constructor.type';
|
|
8
|
+
import { ErrorCode } from '../../error-code';
|
|
9
|
+
import { CoreAiBudgetLimitCreateInput } from '../inputs/core-ai-budget-limit-create.input';
|
|
10
|
+
import { CoreAiBudgetLimitInput } from '../inputs/core-ai-budget-limit.input';
|
|
11
|
+
import { AiBudgetLimitDocument, CoreAiBudgetLimit } from '../models/core-ai-budget-limit.model';
|
|
12
|
+
import { CoreAiBudgetSummary, CoreAiUsageInfo, CoreAiUsageScope } from '../models/core-ai-usage-info.model';
|
|
13
|
+
|
|
14
|
+
/** Mongoose injection token for the budget-limit model. */
|
|
15
|
+
export const AI_BUDGET_LIMIT_MODEL = 'AiBudgetLimit';
|
|
16
|
+
/** DI token for the budget-limit model constructor. */
|
|
17
|
+
export const AI_BUDGET_LIMIT_CLASS = 'AI_BUDGET_LIMIT_CLASS';
|
|
18
|
+
|
|
19
|
+
/** Resolved effective limit for a scope. */
|
|
20
|
+
export interface ResolvedAiBudgetLimit {
|
|
21
|
+
maxPrompts?: number;
|
|
22
|
+
maxTokens?: number;
|
|
23
|
+
period: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Usage counters for a scope in the current period. */
|
|
27
|
+
export interface AiBudgetUsage {
|
|
28
|
+
resetAt: Date | null;
|
|
29
|
+
usedPrompts: number;
|
|
30
|
+
usedTokens: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Token/prompt budget service.
|
|
35
|
+
*
|
|
36
|
+
* - Admin CRUD of per-user / per-tenant limit overrides (`aiBudgetLimits`).
|
|
37
|
+
* - {@link resolveLimit}: override → config default (`ai.budget.user|tenant`) → unlimited.
|
|
38
|
+
* - {@link getUsage}: period usage via a read-only native count over `aiInteractions`.
|
|
39
|
+
* - {@link assertWithinBudget}: blocks a run (HTTP 429) when a finite limit is hit.
|
|
40
|
+
* - {@link getUsageInfo} / {@link buildSummary}: usage reporting for clients.
|
|
41
|
+
*
|
|
42
|
+
* All internal limits are optional — a missing/0 limit means unlimited (only the
|
|
43
|
+
* LLM's own limit then applies).
|
|
44
|
+
*/
|
|
45
|
+
@Injectable()
|
|
46
|
+
export class CoreAiBudgetService extends CrudService<
|
|
47
|
+
CoreAiBudgetLimit,
|
|
48
|
+
CoreAiBudgetLimitCreateInput,
|
|
49
|
+
CoreAiBudgetLimitInput
|
|
50
|
+
> {
|
|
51
|
+
/** Collection holding per-run usage records (written by the audit service). */
|
|
52
|
+
protected readonly interactionsCollection = 'aiInteractions';
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
@InjectConnection() protected readonly connection: Connection,
|
|
56
|
+
@InjectModel(AI_BUDGET_LIMIT_MODEL) protected override readonly mainDbModel: Model<AiBudgetLimitDocument>,
|
|
57
|
+
@Inject(AI_BUDGET_LIMIT_CLASS)
|
|
58
|
+
protected override readonly mainModelConstructor: CoreModelConstructor<CoreAiBudgetLimit>,
|
|
59
|
+
) {
|
|
60
|
+
super();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the effective limit for a scope: persisted override, else config default.
|
|
65
|
+
*/
|
|
66
|
+
async resolveLimit(scope: 'tenant' | 'user', refId: string): Promise<ResolvedAiBudgetLimit> {
|
|
67
|
+
const override = await this.mainDbModel.findOne({ refId, scope }).lean().exec();
|
|
68
|
+
const cfg = ConfigService.get<{
|
|
69
|
+
period?: string;
|
|
70
|
+
tenant?: { maxPrompts?: number; maxTokens?: number };
|
|
71
|
+
user?: { maxPrompts?: number; maxTokens?: number };
|
|
72
|
+
}>('ai.budget');
|
|
73
|
+
const defaults = scope === 'tenant' ? cfg?.tenant : cfg?.user;
|
|
74
|
+
return {
|
|
75
|
+
maxPrompts: override?.maxPrompts ?? defaults?.maxPrompts,
|
|
76
|
+
maxTokens: override?.maxTokens ?? defaults?.maxTokens,
|
|
77
|
+
period: override?.period || cfg?.period || 'day',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Count usage (prompts + tokens) for a scope in the current period.
|
|
83
|
+
* Read-only native count over `aiInteractions` (allowed; bypasses tenant auto-scope
|
|
84
|
+
* so explicit user/tenant filters are honored).
|
|
85
|
+
*/
|
|
86
|
+
async getUsage(filter: { tenantId?: string; userId?: string }, period: string): Promise<AiBudgetUsage> {
|
|
87
|
+
const periodStart = this.periodStart(period);
|
|
88
|
+
const match: Record<string, any> = { createdAt: { $gte: periodStart } };
|
|
89
|
+
if (filter.userId) {
|
|
90
|
+
match.userId = filter.userId;
|
|
91
|
+
}
|
|
92
|
+
if (filter.tenantId) {
|
|
93
|
+
match.tenantId = filter.tenantId;
|
|
94
|
+
}
|
|
95
|
+
const db = this.connection.db;
|
|
96
|
+
if (!db) {
|
|
97
|
+
return { resetAt: this.nextReset(period), usedPrompts: 0, usedTokens: 0 };
|
|
98
|
+
}
|
|
99
|
+
// Sum prompts + tokens server-side via aggregation so only the aggregate crosses
|
|
100
|
+
// the wire (a per-prompt `.find().toArray()` would load every interaction doc into
|
|
101
|
+
// the Node heap). Backed by the `{ userId|tenantId, createdAt }` compound indexes.
|
|
102
|
+
const [agg] = await db
|
|
103
|
+
.collection(this.interactionsCollection)
|
|
104
|
+
.aggregate<{ usedPrompts: number; usedTokens: number }>([
|
|
105
|
+
{ $match: match },
|
|
106
|
+
{ $group: { _id: null, usedPrompts: { $sum: 1 }, usedTokens: { $sum: { $ifNull: ['$totalTokens', 0] } } } },
|
|
107
|
+
])
|
|
108
|
+
.toArray();
|
|
109
|
+
return { resetAt: this.nextReset(period), usedPrompts: agg?.usedPrompts ?? 0, usedTokens: agg?.usedTokens ?? 0 };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Block the run with HTTP 429 if the user OR tenant has hit a finite limit.
|
|
114
|
+
*
|
|
115
|
+
* @param language Deprecated and unused — the error-code translation layer now
|
|
116
|
+
* localizes the 429 message. Kept for call-site backward compatibility.
|
|
117
|
+
*/
|
|
118
|
+
async assertWithinBudget(userId?: string, tenantId?: string, language?: string): Promise<void> {
|
|
119
|
+
for (const [scope, refId] of [
|
|
120
|
+
['user', userId],
|
|
121
|
+
['tenant', tenantId],
|
|
122
|
+
] as const) {
|
|
123
|
+
if (!refId) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const limit = await this.resolveLimit(scope, refId);
|
|
127
|
+
if (!this.finite(limit.maxTokens) && !this.finite(limit.maxPrompts)) {
|
|
128
|
+
continue; // unlimited
|
|
129
|
+
}
|
|
130
|
+
const usage = await this.getUsage(scope === 'user' ? { userId: refId } : { tenantId: refId }, limit.period);
|
|
131
|
+
if (this.exceeded(usage, limit)) {
|
|
132
|
+
// Message is translated (de/en) by the error-code translation layer.
|
|
133
|
+
void language;
|
|
134
|
+
throw new HttpException(ErrorCode.AI_BUDGET_EXCEEDED, HttpStatus.TOO_MANY_REQUESTS);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Full usage info for the current user (and tenant when present).
|
|
141
|
+
*/
|
|
142
|
+
async getUsageInfo(userId?: string, tenantId?: string): Promise<CoreAiUsageInfo> {
|
|
143
|
+
const info = new CoreAiUsageInfo();
|
|
144
|
+
if (userId) {
|
|
145
|
+
info.user = await this.buildScope('user', userId);
|
|
146
|
+
}
|
|
147
|
+
if (tenantId) {
|
|
148
|
+
info.tenant = await this.buildScope('tenant', tenantId);
|
|
149
|
+
}
|
|
150
|
+
return info;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Compact per-response summary for the user scope.
|
|
155
|
+
*/
|
|
156
|
+
async buildSummary(
|
|
157
|
+
userId: string | undefined,
|
|
158
|
+
tenantId: string | undefined,
|
|
159
|
+
promptTokens: number,
|
|
160
|
+
llmContextWindow?: number,
|
|
161
|
+
providerQuota?: { maxTokens?: number; period?: string },
|
|
162
|
+
): Promise<CoreAiBudgetSummary> {
|
|
163
|
+
const summary = new CoreAiBudgetSummary();
|
|
164
|
+
summary.promptTokens = promptTokens;
|
|
165
|
+
if (!userId) {
|
|
166
|
+
// No user → only the LLM context window can be reported as a coarse signal.
|
|
167
|
+
if (this.finite(llmContextWindow)) {
|
|
168
|
+
summary.maxTokens = llmContextWindow;
|
|
169
|
+
summary.scope = 'llm';
|
|
170
|
+
}
|
|
171
|
+
return summary;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 1) User-scope hard limit (DB override → config default).
|
|
175
|
+
const userLimit = await this.resolveLimit('user', userId);
|
|
176
|
+
if (this.finite(userLimit.maxTokens) || this.finite(userLimit.maxPrompts)) {
|
|
177
|
+
const usage = await this.getUsage({ userId }, userLimit.period);
|
|
178
|
+
summary.usedTokens = usage.usedTokens;
|
|
179
|
+
summary.remainingTokens = this.finite(userLimit.maxTokens)
|
|
180
|
+
? Math.max(0, (userLimit.maxTokens as number) - usage.usedTokens)
|
|
181
|
+
: undefined;
|
|
182
|
+
summary.resetAt = usage.resetAt ?? undefined;
|
|
183
|
+
if (this.finite(userLimit.maxTokens)) {
|
|
184
|
+
summary.maxTokens = userLimit.maxTokens;
|
|
185
|
+
summary.scope = 'user';
|
|
186
|
+
}
|
|
187
|
+
return summary;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 2) Tenant-scope hard limit.
|
|
191
|
+
if (tenantId) {
|
|
192
|
+
const tenantLimit = await this.resolveLimit('tenant', tenantId);
|
|
193
|
+
if (this.finite(tenantLimit.maxTokens)) {
|
|
194
|
+
const usage = await this.getUsage({ tenantId }, tenantLimit.period);
|
|
195
|
+
summary.usedTokens = usage.usedTokens;
|
|
196
|
+
summary.remainingTokens = Math.max(0, (tenantLimit.maxTokens as number) - usage.usedTokens);
|
|
197
|
+
summary.resetAt = usage.resetAt ?? undefined;
|
|
198
|
+
summary.maxTokens = tenantLimit.maxTokens;
|
|
199
|
+
summary.scope = 'tenant';
|
|
200
|
+
return summary;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// No hard limit configured for this user / tenant. Still report the running
|
|
205
|
+
// CUMULATIVE user usage over the default period so the UI can show a
|
|
206
|
+
// running counter (and a soft bar when a fallback maxTokens exists).
|
|
207
|
+
const period = providerQuota?.period || userLimit.period;
|
|
208
|
+
const usage = await this.getUsage({ userId }, period);
|
|
209
|
+
summary.usedTokens = usage.usedTokens;
|
|
210
|
+
summary.resetAt = usage.resetAt ?? undefined;
|
|
211
|
+
|
|
212
|
+
// 3) Provider-quota soft default — auto-derived from the active connection
|
|
213
|
+
// (the admin pins it per connection; the bar shows it as scope='llm'
|
|
214
|
+
// because it's a provider hint, not a per-user hard limit).
|
|
215
|
+
if (this.finite(providerQuota?.maxTokens)) {
|
|
216
|
+
summary.maxTokens = providerQuota!.maxTokens;
|
|
217
|
+
summary.remainingTokens = Math.max(0, (providerQuota!.maxTokens as number) - usage.usedTokens);
|
|
218
|
+
summary.scope = 'llm';
|
|
219
|
+
return summary;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 4) Last-resort soft default — the LLM context window. Coarse but
|
|
223
|
+
// better than rendering nothing.
|
|
224
|
+
if (this.finite(llmContextWindow)) {
|
|
225
|
+
summary.maxTokens = llmContextWindow;
|
|
226
|
+
summary.scope = 'llm';
|
|
227
|
+
}
|
|
228
|
+
return summary;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ===================================================================================================================
|
|
232
|
+
// Helpers
|
|
233
|
+
// ===================================================================================================================
|
|
234
|
+
|
|
235
|
+
protected async buildScope(scope: 'tenant' | 'user', refId: string): Promise<CoreAiUsageScope> {
|
|
236
|
+
const limit = await this.resolveLimit(scope, refId);
|
|
237
|
+
const usage = await this.getUsage(scope === 'user' ? { userId: refId } : { tenantId: refId }, limit.period);
|
|
238
|
+
const result = new CoreAiUsageScope();
|
|
239
|
+
result.maxPrompts = this.finite(limit.maxPrompts) ? limit.maxPrompts : undefined;
|
|
240
|
+
result.maxTokens = this.finite(limit.maxTokens) ? limit.maxTokens : undefined;
|
|
241
|
+
result.period = limit.period;
|
|
242
|
+
result.refId = refId;
|
|
243
|
+
result.remainingPrompts = this.finite(limit.maxPrompts)
|
|
244
|
+
? Math.max(0, (limit.maxPrompts as number) - usage.usedPrompts)
|
|
245
|
+
: undefined;
|
|
246
|
+
result.remainingTokens = this.finite(limit.maxTokens)
|
|
247
|
+
? Math.max(0, (limit.maxTokens as number) - usage.usedTokens)
|
|
248
|
+
: undefined;
|
|
249
|
+
result.resetAt = usage.resetAt ?? undefined;
|
|
250
|
+
result.scope = scope;
|
|
251
|
+
result.usedPrompts = usage.usedPrompts;
|
|
252
|
+
result.usedTokens = usage.usedTokens;
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** A limit value is "finite" (enforced) only when it is a positive number. */
|
|
257
|
+
protected finite(value?: number): boolean {
|
|
258
|
+
return typeof value === 'number' && value > 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
protected exceeded(usage: AiBudgetUsage, limit: ResolvedAiBudgetLimit): boolean {
|
|
262
|
+
return (
|
|
263
|
+
(this.finite(limit.maxTokens) && usage.usedTokens >= (limit.maxTokens as number)) ||
|
|
264
|
+
(this.finite(limit.maxPrompts) && usage.usedPrompts >= (limit.maxPrompts as number))
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Start of the current period. */
|
|
269
|
+
protected periodStart(period: string): Date {
|
|
270
|
+
const now = new Date();
|
|
271
|
+
if (period === 'month') {
|
|
272
|
+
return new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
|
273
|
+
}
|
|
274
|
+
if (period === 'none') {
|
|
275
|
+
return new Date(0);
|
|
276
|
+
}
|
|
277
|
+
// 'day' (default)
|
|
278
|
+
const start = new Date(now);
|
|
279
|
+
start.setHours(0, 0, 0, 0);
|
|
280
|
+
return start;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Next reset boundary (null for 'none'). */
|
|
284
|
+
protected nextReset(period: string): Date | null {
|
|
285
|
+
const now = new Date();
|
|
286
|
+
if (period === 'month') {
|
|
287
|
+
return new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0, 0);
|
|
288
|
+
}
|
|
289
|
+
if (period === 'none') {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const next = new Date(now);
|
|
293
|
+
next.setHours(0, 0, 0, 0);
|
|
294
|
+
next.setDate(next.getDate() + 1);
|
|
295
|
+
return next;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import { InjectModel } from '@nestjs/mongoose';
|
|
3
|
+
import { Model } from 'mongoose';
|
|
4
|
+
|
|
5
|
+
import { CrudService } from '../../../common/services/crud.service';
|
|
6
|
+
import { CoreModelConstructor } from '../../../common/types/core-model-constructor.type';
|
|
7
|
+
import { CoreAiConnectionPreferenceInput } from '../inputs/core-ai-connection-preference.input';
|
|
8
|
+
import {
|
|
9
|
+
AiConnectionPreferenceDocument,
|
|
10
|
+
CoreAiConnectionPreference,
|
|
11
|
+
} from '../models/core-ai-connection-preference.model';
|
|
12
|
+
|
|
13
|
+
/** Mongoose injection token for the connection-preference model. */
|
|
14
|
+
export const AI_CONNECTION_PREFERENCE_MODEL = 'AiConnectionPreference';
|
|
15
|
+
/** DI token for the connection-preference model constructor. */
|
|
16
|
+
export const AI_CONNECTION_PREFERENCE_CLASS = 'AI_CONNECTION_PREFERENCE_CLASS';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* CRUD + lookup for {@link CoreAiConnectionPreference} (tenant/user connection
|
|
20
|
+
* defaults + tenant-enforced). Used by the resolution chain.
|
|
21
|
+
*/
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class CoreAiConnectionPreferenceService extends CrudService<
|
|
24
|
+
CoreAiConnectionPreference,
|
|
25
|
+
CoreAiConnectionPreferenceInput,
|
|
26
|
+
CoreAiConnectionPreferenceInput
|
|
27
|
+
> {
|
|
28
|
+
constructor(
|
|
29
|
+
@InjectModel(AI_CONNECTION_PREFERENCE_MODEL)
|
|
30
|
+
protected override readonly mainDbModel: Model<AiConnectionPreferenceDocument>,
|
|
31
|
+
@Inject(AI_CONNECTION_PREFERENCE_CLASS)
|
|
32
|
+
protected override readonly mainModelConstructor: CoreModelConstructor<CoreAiConnectionPreference>,
|
|
33
|
+
) {
|
|
34
|
+
super();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read a preference for a scope/ref (system-internal lean read).
|
|
39
|
+
*/
|
|
40
|
+
async getPreference(
|
|
41
|
+
scope: 'tenant' | 'user',
|
|
42
|
+
refId: string,
|
|
43
|
+
): Promise<{ connectionId: string; enforced?: boolean } | null> {
|
|
44
|
+
const doc = await this.mainDbModel.findOne({ refId, scope }).lean().exec();
|
|
45
|
+
return doc ? { connectionId: doc.connectionId, enforced: doc.enforced } : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Delete all preferences pointing to a connection (used when a connection is
|
|
50
|
+
* removed, to avoid dangling tenant/user preferences). Returns the deleted count.
|
|
51
|
+
*/
|
|
52
|
+
async deleteByConnectionId(connectionId: string): Promise<number> {
|
|
53
|
+
const result = await this.mainDbModel.deleteMany({ connectionId }).exec();
|
|
54
|
+
return result.deletedCount ?? 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Upsert a preference (one per scope/ref). System-internal; callers must
|
|
59
|
+
* authorize the scope/ref (e.g. a user may only set their own user preference).
|
|
60
|
+
* Returns the persisted preference as a mapped model.
|
|
61
|
+
*/
|
|
62
|
+
async upsertPreference(
|
|
63
|
+
scope: 'tenant' | 'user',
|
|
64
|
+
refId: string,
|
|
65
|
+
connectionId: string,
|
|
66
|
+
enforced = false,
|
|
67
|
+
): Promise<CoreAiConnectionPreference> {
|
|
68
|
+
const doc = await this.mainDbModel
|
|
69
|
+
.findOneAndUpdate({ refId, scope }, { $set: { connectionId, enforced } }, { new: true, upsert: true })
|
|
70
|
+
.lean()
|
|
71
|
+
.exec();
|
|
72
|
+
return this.mainModelConstructor.map(doc);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { ForbiddenException, Injectable, Logger, NotFoundException, Optional } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { ErrorCode } from '../../error-code';
|
|
4
|
+
import { ResolvedAiConnection } from '../interfaces/resolved-ai-connection.interface';
|
|
5
|
+
import { CoreAiAvailableConnection } from '../models/core-ai-available-connection.model';
|
|
6
|
+
import { CoreAiConnectionPreference } from '../models/core-ai-connection-preference.model';
|
|
7
|
+
import { CoreAiConnectionPreferenceService } from './core-ai-connection-preference.service';
|
|
8
|
+
import { CoreAiConnectionService } from './core-ai-connection.service';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lightweight connection shape used during resolution (no secrets). `model` and
|
|
12
|
+
* `name` are display-only and not used by the resolution logic itself.
|
|
13
|
+
*/
|
|
14
|
+
export interface AiResolvableConnection {
|
|
15
|
+
enforced?: boolean;
|
|
16
|
+
enforcedTenantIds?: string[];
|
|
17
|
+
id: string;
|
|
18
|
+
isDefault?: boolean;
|
|
19
|
+
model?: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
tenantIds?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Inputs for resolving which connection to use for a prompt.
|
|
26
|
+
*/
|
|
27
|
+
export interface AiConnectionResolveContext {
|
|
28
|
+
/** Connection explicitly pinned by a module/service/function (highest priority). */
|
|
29
|
+
codeOverride?: string;
|
|
30
|
+
/** Connection requested by the client (`input.connectionId`). */
|
|
31
|
+
requested?: string;
|
|
32
|
+
/** Current tenant id (multi-tenancy), if any. */
|
|
33
|
+
tenantId?: string;
|
|
34
|
+
/** Current user id, if any. */
|
|
35
|
+
userId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolves WHICH database connection a prompt uses, via a prioritized, fully
|
|
40
|
+
* overridable chain. Connections live in the DB and can be restricted per tenant
|
|
41
|
+
* (`tenantIds`). If there are no usable connections, AI handling is effectively
|
|
42
|
+
* disabled (resolution returns `undefined`). If nothing is explicitly selected,
|
|
43
|
+
* the first available connection is used (so exactly one connection is the
|
|
44
|
+
* implicit default, and multiple connections never produce a dead state).
|
|
45
|
+
*
|
|
46
|
+
* Resolution order (ascending priority — a later layer overrides an earlier one).
|
|
47
|
+
* "Soft" layers (1–4) must pick a connection from the tenant's available set;
|
|
48
|
+
* "hard"/mandatory layers (5–8) are authoritative and win regardless:
|
|
49
|
+
*
|
|
50
|
+
* 1. Global default (`isDefault`)
|
|
51
|
+
* 2. Tenant default (preference, not enforced)
|
|
52
|
+
* 3. User default (preference)
|
|
53
|
+
* 4. Client request (`requested`)
|
|
54
|
+
* 5. Tenant-enforced (preference, enforced)
|
|
55
|
+
* 6. Admin-enforced global (`enforced`)
|
|
56
|
+
* 7. Admin-enforced for tenant (`enforcedTenantIds`)
|
|
57
|
+
* 8. Explicit code override (`codeOverride`) — deliberate, trusted; document that it
|
|
58
|
+
* bypasses mandates (projects that need mandates to be absolute can reorder).
|
|
59
|
+
*
|
|
60
|
+
* Each layer is an overridable `protected` method, and the whole chain
|
|
61
|
+
* ({@link resolutionLayers}) can be reordered/extended/replaced. Pass a custom
|
|
62
|
+
* subclass via `CoreModule.forRoot(env, { ai: { connectionResolver } })`.
|
|
63
|
+
*/
|
|
64
|
+
@Injectable()
|
|
65
|
+
export class CoreAiConnectionResolverService {
|
|
66
|
+
protected readonly logger = new Logger(CoreAiConnectionResolverService.name);
|
|
67
|
+
|
|
68
|
+
/** Per-resolution memo for the tenant preference (one DB read per `ctx`). */
|
|
69
|
+
private readonly tenantPrefCache = new WeakMap<
|
|
70
|
+
AiConnectionResolveContext,
|
|
71
|
+
Promise<{ connectionId: string; enforced?: boolean } | null>
|
|
72
|
+
>();
|
|
73
|
+
|
|
74
|
+
constructor(
|
|
75
|
+
protected readonly connectionService: CoreAiConnectionService,
|
|
76
|
+
@Optional() protected readonly preferenceService?: CoreAiConnectionPreferenceService,
|
|
77
|
+
) {}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve and load the connection to use (with decrypted key), or `undefined`
|
|
81
|
+
* when AI is effectively disabled (no usable connection).
|
|
82
|
+
*/
|
|
83
|
+
async resolveConnection(ctx: AiConnectionResolveContext): Promise<ResolvedAiConnection | undefined> {
|
|
84
|
+
const id = await this.resolveConnectionId(ctx);
|
|
85
|
+
return id ? this.connectionService.resolve(id) : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the connection id via the prioritized chain.
|
|
90
|
+
*/
|
|
91
|
+
async resolveConnectionId(ctx: AiConnectionResolveContext): Promise<string | undefined> {
|
|
92
|
+
const connections = await this.connectionService.listUsable();
|
|
93
|
+
return (await this.resolveFrom(connections, ctx)).selected;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* List the connections available to the caller (non-sensitive), marking the
|
|
98
|
+
* currently resolved one (`selected`) and whether the selection is locked by a
|
|
99
|
+
* mandatory layer. Empty when AI handling is disabled (no usable connections).
|
|
100
|
+
*/
|
|
101
|
+
async listAvailable(ctx: AiConnectionResolveContext): Promise<CoreAiAvailableConnection[]> {
|
|
102
|
+
const connections = await this.connectionService.listUsable();
|
|
103
|
+
if (!connections.length) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const available = this.availableConnections(connections, ctx.tenantId);
|
|
107
|
+
const { locked, selected } = await this.resolveFrom(connections, ctx);
|
|
108
|
+
return available.map((c) => {
|
|
109
|
+
const item = new CoreAiAvailableConnection();
|
|
110
|
+
item.id = c.id;
|
|
111
|
+
item.isDefault = !!c.isDefault;
|
|
112
|
+
item.locked = locked;
|
|
113
|
+
item.model = c.model;
|
|
114
|
+
item.name = c.name;
|
|
115
|
+
item.selected = c.id === selected;
|
|
116
|
+
return item;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Set the caller's own user-default connection. Validates that the connection is
|
|
122
|
+
* available to the caller's tenant before storing the preference. Throws when no
|
|
123
|
+
* preference service is wired or the connection is not available.
|
|
124
|
+
*/
|
|
125
|
+
async setUserConnection(userId: string, connectionId: string, tenantId?: string): Promise<void> {
|
|
126
|
+
if (!this.preferenceService) {
|
|
127
|
+
throw new ForbiddenException(ErrorCode.AI_PREFERENCES_UNAVAILABLE);
|
|
128
|
+
}
|
|
129
|
+
const connections = await this.connectionService.listUsable();
|
|
130
|
+
const available = this.availableConnections(connections, tenantId);
|
|
131
|
+
if (!available.some((c) => c.id === connectionId)) {
|
|
132
|
+
throw new ForbiddenException(ErrorCode.AI_CONNECTION_NOT_AVAILABLE);
|
|
133
|
+
}
|
|
134
|
+
await this.preferenceService.upsertPreference('user', userId, connectionId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Set a tenant/user connection preference (admin self-service). Validates that the
|
|
139
|
+
* connection exists and is usable before persisting, so an admin gets immediate
|
|
140
|
+
* feedback instead of creating a dangling preference. Returns the stored preference.
|
|
141
|
+
*/
|
|
142
|
+
async setPreference(
|
|
143
|
+
scope: 'tenant' | 'user',
|
|
144
|
+
refId: string,
|
|
145
|
+
connectionId: string,
|
|
146
|
+
enforced = false,
|
|
147
|
+
): Promise<CoreAiConnectionPreference> {
|
|
148
|
+
if (!this.preferenceService) {
|
|
149
|
+
throw new ForbiddenException(ErrorCode.AI_PREFERENCES_UNAVAILABLE);
|
|
150
|
+
}
|
|
151
|
+
await this.assertConnectionUsable(connectionId);
|
|
152
|
+
return this.preferenceService.upsertPreference(scope, refId, connectionId, enforced);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Throw {@link NotFoundException} when the connection id does not exist or is disabled.
|
|
157
|
+
*/
|
|
158
|
+
protected async assertConnectionUsable(connectionId: string): Promise<void> {
|
|
159
|
+
const connections = await this.connectionService.listUsable();
|
|
160
|
+
if (!connections.some((c) => c.id === connectionId)) {
|
|
161
|
+
throw new NotFoundException(ErrorCode.AI_CONNECTION_NOT_FOUND);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Run the resolution chain against a pre-fetched connection list, returning the
|
|
167
|
+
* selected id and whether the selection came from a mandatory (hard) layer.
|
|
168
|
+
*/
|
|
169
|
+
protected async resolveFrom(
|
|
170
|
+
connections: AiResolvableConnection[],
|
|
171
|
+
ctx: AiConnectionResolveContext,
|
|
172
|
+
): Promise<{ locked: boolean; selected?: string }> {
|
|
173
|
+
if (!connections.length) {
|
|
174
|
+
return { locked: false }; // no connections → AI disabled
|
|
175
|
+
}
|
|
176
|
+
const available = this.availableConnections(connections, ctx.tenantId);
|
|
177
|
+
const availableIds = new Set(available.map((c) => c.id));
|
|
178
|
+
const allIds = new Set(connections.map((c) => c.id));
|
|
179
|
+
|
|
180
|
+
let locked = false;
|
|
181
|
+
let selected: string | undefined;
|
|
182
|
+
for (const layer of this.resolutionLayers()) {
|
|
183
|
+
const candidate = await layer.resolve(connections, available, ctx);
|
|
184
|
+
if (!candidate) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Soft layers may only pick a connection available to the tenant.
|
|
188
|
+
if (layer.soft && !availableIds.has(candidate)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
selected = candidate;
|
|
192
|
+
// A mandatory (hard) layer dictates the selection — the user cannot override it.
|
|
193
|
+
locked = !layer.soft;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Robustness: hard layers (enforced preferences, code override) and externally
|
|
197
|
+
// sourced ids are not pre-filtered by availability, so they can point to a
|
|
198
|
+
// connection that no longer exists or has been disabled. Drop such a stale
|
|
199
|
+
// reference and degrade gracefully instead of returning a dead id that would make
|
|
200
|
+
// `connectionService.resolve()` throw mid-prompt.
|
|
201
|
+
if (selected && !allIds.has(selected)) {
|
|
202
|
+
this.logger.warn(
|
|
203
|
+
`AI connection "${selected}" selected by the resolution chain no longer exists or is disabled; falling back.`,
|
|
204
|
+
);
|
|
205
|
+
locked = false;
|
|
206
|
+
selected = undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Nothing explicitly selected → fall back to the first available connection.
|
|
210
|
+
// This covers the "exactly one connection = implicit default" rule and avoids a
|
|
211
|
+
// dead state when multiple connections exist without a default/preference/selection.
|
|
212
|
+
if (!selected && available.length) {
|
|
213
|
+
selected = available[0].id;
|
|
214
|
+
}
|
|
215
|
+
return { locked, selected };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The ordered resolution layers (ascending priority). Override to reorder,
|
|
220
|
+
* extend or replace the chain.
|
|
221
|
+
*/
|
|
222
|
+
protected resolutionLayers(): {
|
|
223
|
+
resolve: (
|
|
224
|
+
all: AiResolvableConnection[],
|
|
225
|
+
available: AiResolvableConnection[],
|
|
226
|
+
ctx: AiConnectionResolveContext,
|
|
227
|
+
) => Promise<string | undefined> | string | undefined;
|
|
228
|
+
soft: boolean;
|
|
229
|
+
}[] {
|
|
230
|
+
return [
|
|
231
|
+
{ resolve: (_all, available) => this.globalDefault(available), soft: true },
|
|
232
|
+
{ resolve: (_all, _available, ctx) => this.tenantDefault(ctx), soft: true },
|
|
233
|
+
{ resolve: (_all, _available, ctx) => this.userDefault(ctx), soft: true },
|
|
234
|
+
{ resolve: (_all, _available, ctx) => this.clientSelection(ctx), soft: true },
|
|
235
|
+
{ resolve: (_all, _available, ctx) => this.tenantEnforced(ctx), soft: false },
|
|
236
|
+
{ resolve: (all) => this.adminEnforcedGlobal(all), soft: false },
|
|
237
|
+
{ resolve: (all, _available, ctx) => this.adminEnforcedForTenant(all, ctx), soft: false },
|
|
238
|
+
{ resolve: (_all, _available, ctx) => this.codeOverride(ctx), soft: false },
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ===================================================================================================================
|
|
243
|
+
// Layers (overridable)
|
|
244
|
+
// ===================================================================================================================
|
|
245
|
+
|
|
246
|
+
/** Connections available to the tenant (empty `tenantIds` = available to all). */
|
|
247
|
+
protected availableConnections(connections: AiResolvableConnection[], tenantId?: string): AiResolvableConnection[] {
|
|
248
|
+
return connections.filter((c) => !c.tenantIds?.length || (tenantId ? c.tenantIds.includes(tenantId) : false));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
protected globalDefault(available: AiResolvableConnection[]): string | undefined {
|
|
252
|
+
return available.find((c) => c.isDefault)?.id;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
protected async tenantDefault(ctx: AiConnectionResolveContext): Promise<string | undefined> {
|
|
256
|
+
const pref = await this.loadTenantPreference(ctx);
|
|
257
|
+
return pref && !pref.enforced ? pref.connectionId : undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
protected async userDefault(ctx: AiConnectionResolveContext): Promise<string | undefined> {
|
|
261
|
+
if (!ctx.userId || !this.preferenceService) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
return (await this.preferenceService.getPreference('user', ctx.userId))?.connectionId;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
protected clientSelection(ctx: AiConnectionResolveContext): string | undefined {
|
|
268
|
+
return ctx.requested;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
protected async tenantEnforced(ctx: AiConnectionResolveContext): Promise<string | undefined> {
|
|
272
|
+
const pref = await this.loadTenantPreference(ctx);
|
|
273
|
+
return pref?.enforced ? pref.connectionId : undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Load the tenant preference once per resolution (`tenantDefault` + `tenantEnforced`
|
|
278
|
+
* both need it). Memoized on the `ctx` object so a single prompt triggers one DB read.
|
|
279
|
+
*/
|
|
280
|
+
protected loadTenantPreference(
|
|
281
|
+
ctx: AiConnectionResolveContext,
|
|
282
|
+
): Promise<{ connectionId: string; enforced?: boolean } | null> {
|
|
283
|
+
if (!ctx.tenantId || !this.preferenceService) {
|
|
284
|
+
return Promise.resolve(null);
|
|
285
|
+
}
|
|
286
|
+
let cached = this.tenantPrefCache.get(ctx);
|
|
287
|
+
if (!cached) {
|
|
288
|
+
cached = this.preferenceService.getPreference('tenant', ctx.tenantId);
|
|
289
|
+
this.tenantPrefCache.set(ctx, cached);
|
|
290
|
+
}
|
|
291
|
+
return cached;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
protected adminEnforcedGlobal(all: AiResolvableConnection[]): string | undefined {
|
|
295
|
+
return all.find((c) => c.enforced)?.id;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
protected adminEnforcedForTenant(all: AiResolvableConnection[], ctx: AiConnectionResolveContext): string | undefined {
|
|
299
|
+
if (!ctx.tenantId) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
return all.find((c) => c.enforcedTenantIds?.includes(ctx.tenantId as string))?.id;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
protected codeOverride(ctx: AiConnectionResolveContext): string | undefined {
|
|
306
|
+
return ctx.codeOverride;
|
|
307
|
+
}
|
|
308
|
+
}
|