@hed-hog/core 0.0.387 → 0.0.390
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/README.md +12 -11
- package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
- package/dist/access-log/access-log.interceptor.js +4 -2
- package/dist/access-log/access-log.interceptor.js.map +1 -1
- package/dist/ai/ai-attachment.fragments.d.ts +23 -0
- package/dist/ai/ai-attachment.fragments.d.ts.map +1 -0
- package/dist/ai/ai-attachment.fragments.js +34 -0
- package/dist/ai/ai-attachment.fragments.js.map +1 -0
- package/dist/ai/ai.controller.d.ts +7 -16
- package/dist/ai/ai.controller.d.ts.map +1 -1
- package/dist/ai/ai.prompts.d.ts +16 -0
- package/dist/ai/ai.prompts.d.ts.map +1 -0
- package/dist/ai/ai.prompts.js +37 -0
- package/dist/ai/ai.prompts.js.map +1 -0
- package/dist/ai/ai.service.d.ts +63 -12
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +215 -88
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai/dto/chat.dto.d.ts +4 -1
- package/dist/ai/dto/chat.dto.d.ts.map +1 -1
- package/dist/ai/dto/chat.dto.js +23 -1
- package/dist/ai/dto/chat.dto.js.map +1 -1
- package/dist/ai/dto/create-agent.dto.d.ts +1 -1
- package/dist/ai/dto/create-agent.dto.d.ts.map +1 -1
- package/dist/ai/dto/create-agent.dto.js +1 -1
- package/dist/ai/dto/create-agent.dto.js.map +1 -1
- package/dist/ai/dto/update-agent.dto.d.ts +1 -1
- package/dist/ai/dto/update-agent.dto.d.ts.map +1 -1
- package/dist/ai/dto/update-agent.dto.js +1 -1
- package/dist/ai/dto/update-agent.dto.js.map +1 -1
- package/dist/ai-instruction/ai-instruction.controller.d.ts +61 -7
- package/dist/ai-instruction/ai-instruction.controller.d.ts.map +1 -1
- package/dist/ai-instruction/ai-instruction.controller.js +63 -20
- package/dist/ai-instruction/ai-instruction.controller.js.map +1 -1
- package/dist/ai-instruction/ai-instruction.module.d.ts.map +1 -1
- package/dist/ai-instruction/ai-instruction.module.js +13 -2
- package/dist/ai-instruction/ai-instruction.module.js.map +1 -1
- package/dist/ai-instruction/ai-instruction.service.d.ts +131 -6
- package/dist/ai-instruction/ai-instruction.service.d.ts.map +1 -1
- package/dist/ai-instruction/ai-instruction.service.js +533 -60
- package/dist/ai-instruction/ai-instruction.service.js.map +1 -1
- package/dist/ai-instruction/ai-prompt-builder.service.d.ts +8 -0
- package/dist/ai-instruction/ai-prompt-builder.service.d.ts.map +1 -1
- package/dist/ai-instruction/ai-prompt-builder.service.js +10 -1
- package/dist/ai-instruction/ai-prompt-builder.service.js.map +1 -1
- package/dist/ai-instruction/ai-prompt-registry.d.ts +45 -0
- package/dist/ai-instruction/ai-prompt-registry.d.ts.map +1 -0
- package/dist/ai-instruction/ai-prompt-registry.js +49 -0
- package/dist/ai-instruction/ai-prompt-registry.js.map +1 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +89 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.js +222 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -0
- package/dist/ai-instruction/ai-prompt-template.util.d.ts +70 -0
- package/dist/ai-instruction/ai-prompt-template.util.d.ts.map +1 -0
- package/dist/ai-instruction/ai-prompt-template.util.js +154 -0
- package/dist/ai-instruction/ai-prompt-template.util.js.map +1 -0
- package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts +5 -0
- package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts.map +1 -1
- package/dist/ai-instruction/dto/create-ai-instruction.dto.js +27 -1
- package/dist/ai-instruction/dto/create-ai-instruction.dto.js.map +1 -1
- package/dist/ai-instruction/dto/preview-ai-instruction.dto.d.ts +14 -0
- package/dist/ai-instruction/dto/preview-ai-instruction.dto.d.ts.map +1 -0
- package/dist/ai-instruction/dto/preview-ai-instruction.dto.js +42 -0
- package/dist/ai-instruction/dto/preview-ai-instruction.dto.js.map +1 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts +11 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts.map +1 -1
- package/dist/ai-instruction/dto/update-ai-instruction.dto.js +34 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.js.map +1 -1
- package/dist/ai-instruction/types/ai-instruction.types.d.ts +13 -1
- package/dist/ai-instruction/types/ai-instruction.types.d.ts.map +1 -1
- package/dist/ai-instruction/types/ai-instruction.types.js +10 -0
- package/dist/ai-instruction/types/ai-instruction.types.js.map +1 -1
- package/dist/auth/auth.controller.d.ts +24 -9
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +49 -21
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +33 -8
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +78 -29
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/captcha/captcha.module.d.ts +3 -0
- package/dist/captcha/captcha.module.d.ts.map +1 -0
- package/dist/captcha/captcha.module.js +23 -0
- package/dist/captcha/captcha.module.js.map +1 -0
- package/dist/captcha/captcha.service.d.ts +38 -0
- package/dist/captcha/captcha.service.d.ts.map +1 -0
- package/dist/captcha/captcha.service.js +144 -0
- package/dist/captcha/captcha.service.js.map +1 -0
- package/dist/challenge/challenge.service.d.ts +16 -7
- package/dist/challenge/challenge.service.d.ts.map +1 -1
- package/dist/challenge/challenge.service.js +64 -16
- package/dist/challenge/challenge.service.js.map +1 -1
- package/dist/core.module.d.ts +3 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +29 -2
- package/dist/core.module.js.map +1 -1
- package/dist/dashboard/dashboard/dashboard.controller.d.ts +15 -15
- package/dist/dashboard/dashboard/dashboard.service.d.ts +15 -15
- package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +6 -6
- package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +6 -6
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +20 -20
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +20 -20
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +1 -1
- package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +13 -13
- package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +13 -13
- package/dist/file/file.service.d.ts +6 -0
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +44 -4
- package/dist/file/file.service.js.map +1 -1
- package/dist/incident/incident-notifier.service.d.ts +36 -0
- package/dist/incident/incident-notifier.service.d.ts.map +1 -0
- package/dist/incident/incident-notifier.service.js +208 -0
- package/dist/incident/incident-notifier.service.js.map +1 -0
- package/dist/incident/incident.controller.d.ts +23 -0
- package/dist/incident/incident.controller.d.ts.map +1 -0
- package/dist/incident/incident.controller.js +75 -0
- package/dist/incident/incident.controller.js.map +1 -0
- package/dist/incident/incident.module.d.ts +7 -0
- package/dist/incident/incident.module.d.ts.map +1 -0
- package/dist/incident/incident.module.js +32 -0
- package/dist/incident/incident.module.js.map +1 -0
- package/dist/incident/incident.service.d.ts +72 -0
- package/dist/incident/incident.service.d.ts.map +1 -0
- package/dist/incident/incident.service.js +184 -0
- package/dist/incident/incident.service.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-profile/dto/test.dto.d.ts +13 -0
- package/dist/integration-profile/dto/test.dto.d.ts.map +1 -0
- package/dist/integration-profile/dto/test.dto.js +26 -0
- package/dist/integration-profile/dto/test.dto.js.map +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts +12 -1
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.controller.js +9 -1
- package/dist/integration-profile/integration-profile.controller.js.map +1 -1
- package/dist/integration-profile/integration-profile.module.d.ts +6 -1
- package/dist/integration-profile/integration-profile.module.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.module.js +26 -3
- package/dist/integration-profile/integration-profile.module.js.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +9 -1
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +26 -7
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +94 -91
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.d.ts +13 -1
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +20 -5
- package/dist/integration-profile/integration-profile.utils.js.map +1 -1
- package/dist/integration-profile/testers/ai.testers.d.ts +3 -0
- package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -0
- package/dist/integration-profile/testers/ai.testers.js +125 -0
- package/dist/integration-profile/testers/ai.testers.js.map +1 -0
- package/dist/integration-profile/testers/captcha.testers.d.ts +3 -0
- package/dist/integration-profile/testers/captcha.testers.d.ts.map +1 -0
- package/dist/integration-profile/testers/captcha.testers.js +121 -0
- package/dist/integration-profile/testers/captcha.testers.js.map +1 -0
- package/dist/integration-profile/testers/email.testers.d.ts +3 -0
- package/dist/integration-profile/testers/email.testers.d.ts.map +1 -0
- package/dist/integration-profile/testers/email.testers.js +128 -0
- package/dist/integration-profile/testers/email.testers.js.map +1 -0
- package/dist/integration-profile/testers/http-probe.d.ts +57 -0
- package/dist/integration-profile/testers/http-probe.d.ts.map +1 -0
- package/dist/integration-profile/testers/http-probe.js +140 -0
- package/dist/integration-profile/testers/http-probe.js.map +1 -0
- package/dist/integration-profile/testers/index.d.ts +12 -0
- package/dist/integration-profile/testers/index.d.ts.map +1 -0
- package/dist/integration-profile/testers/index.js +45 -0
- package/dist/integration-profile/testers/index.js.map +1 -0
- package/dist/integration-profile/testers/infrastructure.testers.d.ts +3 -0
- package/dist/integration-profile/testers/infrastructure.testers.d.ts.map +1 -0
- package/dist/integration-profile/testers/infrastructure.testers.js +105 -0
- package/dist/integration-profile/testers/infrastructure.testers.js.map +1 -0
- package/dist/integration-profile/testers/integration-tester.registry.d.ts +17 -0
- package/dist/integration-profile/testers/integration-tester.registry.d.ts.map +1 -0
- package/dist/integration-profile/testers/integration-tester.registry.js +52 -0
- package/dist/integration-profile/testers/integration-tester.registry.js.map +1 -0
- package/dist/integration-profile/testers/integration-tester.types.d.ts +55 -0
- package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -0
- package/dist/integration-profile/testers/integration-tester.types.js +16 -0
- package/dist/integration-profile/testers/integration-tester.types.js.map +1 -0
- package/dist/integration-profile/testers/messaging.testers.d.ts +3 -0
- package/dist/integration-profile/testers/messaging.testers.d.ts.map +1 -0
- package/dist/integration-profile/testers/messaging.testers.js +70 -0
- package/dist/integration-profile/testers/messaging.testers.js.map +1 -0
- package/dist/integration-profile/testers/oauth.testers.d.ts +3 -0
- package/dist/integration-profile/testers/oauth.testers.d.ts.map +1 -0
- package/dist/integration-profile/testers/oauth.testers.js +285 -0
- package/dist/integration-profile/testers/oauth.testers.js.map +1 -0
- package/dist/integration-profile/testers/payment.testers.d.ts +3 -0
- package/dist/integration-profile/testers/payment.testers.d.ts.map +1 -0
- package/dist/integration-profile/testers/payment.testers.js +169 -0
- package/dist/integration-profile/testers/payment.testers.js.map +1 -0
- package/dist/integration-profile/testers/storage.testers.d.ts +3 -0
- package/dist/integration-profile/testers/storage.testers.d.ts.map +1 -0
- package/dist/integration-profile/testers/storage.testers.js +208 -0
- package/dist/integration-profile/testers/storage.testers.js.map +1 -0
- package/dist/language/en.json +4 -2
- package/dist/language/pt.json +4 -2
- package/dist/leaked-password/leaked-password.controller.d.ts +24 -0
- package/dist/leaked-password/leaked-password.controller.d.ts.map +1 -0
- package/dist/leaked-password/leaked-password.controller.js +78 -0
- package/dist/leaked-password/leaked-password.controller.js.map +1 -0
- package/dist/leaked-password/leaked-password.module.d.ts +8 -0
- package/dist/leaked-password/leaked-password.module.d.ts.map +1 -0
- package/dist/leaked-password/leaked-password.module.js +31 -0
- package/dist/leaked-password/leaked-password.module.js.map +1 -0
- package/dist/leaked-password/leaked-password.service.d.ts +64 -0
- package/dist/leaked-password/leaked-password.service.d.ts.map +1 -0
- package/dist/leaked-password/leaked-password.service.js +218 -0
- package/dist/leaked-password/leaked-password.service.js.map +1 -0
- package/dist/locale/locale.controller.d.ts +1 -1
- package/dist/locale/locale.service.d.ts +1 -1
- package/dist/mail/dto/preview.dto.d.ts +18 -0
- package/dist/mail/dto/preview.dto.d.ts.map +1 -0
- package/dist/mail/dto/preview.dto.js +66 -0
- package/dist/mail/dto/preview.dto.js.map +1 -0
- package/dist/mail/dto/send.dto.d.ts +7 -0
- package/dist/mail/dto/send.dto.d.ts.map +1 -1
- package/dist/mail/dto/send.dto.js +10 -0
- package/dist/mail/dto/send.dto.js.map +1 -1
- package/dist/mail/mail.controller.d.ts +4 -0
- package/dist/mail/mail.controller.d.ts.map +1 -1
- package/dist/mail/mail.controller.js +16 -2
- package/dist/mail/mail.controller.js.map +1 -1
- package/dist/mail/mail.service.d.ts +23 -2
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +102 -13
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mail-layout/dto/update.dto.d.ts +17 -0
- package/dist/mail-layout/dto/update.dto.d.ts.map +1 -0
- package/dist/mail-layout/dto/update.dto.js +27 -0
- package/dist/mail-layout/dto/update.dto.js.map +1 -0
- package/dist/mail-layout/mail-layout.controller.d.ts +15 -0
- package/dist/mail-layout/mail-layout.controller.d.ts.map +1 -0
- package/dist/mail-layout/mail-layout.controller.js +71 -0
- package/dist/mail-layout/mail-layout.controller.js.map +1 -0
- package/dist/mail-layout/mail-layout.module.d.ts +3 -0
- package/dist/mail-layout/mail-layout.module.d.ts.map +1 -0
- package/dist/mail-layout/mail-layout.module.js +35 -0
- package/dist/mail-layout/mail-layout.module.js.map +1 -0
- package/dist/mail-layout/mail-layout.service.d.ts +27 -0
- package/dist/mail-layout/mail-layout.service.d.ts.map +1 -0
- package/dist/mail-layout/mail-layout.service.js +80 -0
- package/dist/mail-layout/mail-layout.service.js.map +1 -0
- package/dist/mcp-chat/mcp-chat.controller.d.ts +2 -2
- package/dist/mcp-chat/mcp-chat.service.d.ts +24 -2
- package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +80 -24
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/menu/menu.controller.d.ts +3 -3
- package/dist/menu/menu.service.d.ts +3 -3
- package/dist/notification/notification.controller.d.ts +12 -12
- package/dist/notification/notification.service.d.ts +12 -12
- package/dist/oauth/oauth-token-broker.service.d.ts +72 -0
- package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -0
- package/dist/oauth/oauth-token-broker.service.js +258 -0
- package/dist/oauth/oauth-token-broker.service.js.map +1 -0
- package/dist/oauth/oauth.module.d.ts.map +1 -1
- package/dist/oauth/oauth.module.js +3 -1
- package/dist/oauth/oauth.module.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +2 -0
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +6 -3
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/pdf/pdf.service.d.ts +8 -0
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +52 -0
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/presence/presence.controller.d.ts +16 -0
- package/dist/presence/presence.controller.d.ts.map +1 -0
- package/dist/presence/presence.controller.js +60 -0
- package/dist/presence/presence.controller.js.map +1 -0
- package/dist/presence/presence.listener.d.ts +29 -0
- package/dist/presence/presence.listener.d.ts.map +1 -0
- package/dist/presence/presence.listener.js +82 -0
- package/dist/presence/presence.listener.js.map +1 -0
- package/dist/presence/presence.module.d.ts +3 -0
- package/dist/presence/presence.module.d.ts.map +1 -0
- package/dist/presence/presence.module.js +32 -0
- package/dist/presence/presence.module.js.map +1 -0
- package/dist/presence/presence.service.d.ts +42 -0
- package/dist/presence/presence.service.d.ts.map +1 -0
- package/dist/presence/presence.service.js +147 -0
- package/dist/presence/presence.service.js.map +1 -0
- package/dist/profile/dto/delete-account.dto.d.ts +8 -0
- package/dist/profile/dto/delete-account.dto.d.ts.map +1 -0
- package/dist/profile/dto/delete-account.dto.js +22 -0
- package/dist/profile/dto/delete-account.dto.js.map +1 -0
- package/dist/profile/events/profile-account-deleted.event.d.ts +12 -0
- package/dist/profile/events/profile-account-deleted.event.d.ts.map +1 -0
- package/dist/profile/events/profile-account-deleted.event.js +17 -0
- package/dist/profile/events/profile-account-deleted.event.js.map +1 -0
- package/dist/profile/profile.controller.d.ts +21 -15
- package/dist/profile/profile.controller.d.ts.map +1 -1
- package/dist/profile/profile.controller.js +75 -46
- package/dist/profile/profile.controller.js.map +1 -1
- package/dist/profile/profile.service.d.ts +30 -18
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +113 -37
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/realtime/realtime.gateway.d.ts +48 -0
- package/dist/realtime/realtime.gateway.d.ts.map +1 -0
- package/dist/realtime/realtime.gateway.js +156 -0
- package/dist/realtime/realtime.gateway.js.map +1 -0
- package/dist/realtime/realtime.module.d.ts +8 -0
- package/dist/realtime/realtime.module.d.ts.map +1 -0
- package/dist/realtime/realtime.module.js +29 -0
- package/dist/realtime/realtime.module.js.map +1 -0
- package/dist/realtime/realtime.service.d.ts +51 -0
- package/dist/realtime/realtime.service.d.ts.map +1 -0
- package/dist/realtime/realtime.service.js +177 -0
- package/dist/realtime/realtime.service.js.map +1 -0
- package/dist/redis/redis-pubsub.d.ts +27 -0
- package/dist/redis/redis-pubsub.d.ts.map +1 -0
- package/dist/redis/redis-pubsub.js +140 -0
- package/dist/redis/redis-pubsub.js.map +1 -0
- package/dist/role/role.controller.d.ts +3 -3
- package/dist/role/role.service.d.ts +3 -3
- package/dist/setting/dto/create.dto.d.ts.map +1 -1
- package/dist/setting/dto/create.dto.js +9 -2
- package/dist/setting/dto/create.dto.js.map +1 -1
- package/dist/setting/setting.controller.d.ts +5 -5
- package/dist/setting/setting.service.d.ts +8 -5
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +25 -1
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/user/dto/create-with-email-and-password.dto.d.ts +2 -0
- package/dist/user/dto/create-with-email-and-password.dto.d.ts.map +1 -1
- package/dist/user/dto/create-with-email-and-password.dto.js +5 -0
- package/dist/user/dto/create-with-email-and-password.dto.js.map +1 -1
- package/dist/user/dto/query-user-mail-list.dto.d.ts +5 -0
- package/dist/user/dto/query-user-mail-list.dto.d.ts.map +1 -0
- package/dist/user/dto/query-user-mail-list.dto.js +25 -0
- package/dist/user/dto/query-user-mail-list.dto.js.map +1 -0
- package/dist/user/user-credential.util.d.ts +19 -0
- package/dist/user/user-credential.util.d.ts.map +1 -0
- package/dist/user/user-credential.util.js +32 -0
- package/dist/user/user-credential.util.js.map +1 -0
- package/dist/user/user.controller.d.ts +17 -5
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.controller.js +18 -3
- package/dist/user/user.controller.js.map +1 -1
- package/dist/user/user.service.d.ts +28 -6
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +204 -13
- package/dist/user/user.service.js.map +1 -1
- package/dist/validators/is-strong-password-with-settings.validator.d.ts.map +1 -1
- package/dist/validators/is-strong-password-with-settings.validator.js +11 -0
- package/dist/validators/is-strong-password-with-settings.validator.js.map +1 -1
- package/dist/validators/service-locator.d.ts +9 -0
- package/dist/validators/service-locator.d.ts.map +1 -1
- package/dist/validators/service-locator.js +12 -0
- package/dist/validators/service-locator.js.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.module.d.ts +3 -0
- package/dist/whatsapp-sender/whatsapp-sender.module.d.ts.map +1 -0
- package/dist/whatsapp-sender/whatsapp-sender.module.js +23 -0
- package/dist/whatsapp-sender/whatsapp-sender.module.js.map +1 -0
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +50 -0
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -0
- package/dist/whatsapp-sender/whatsapp-sender.service.js +224 -0
- package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -0
- package/hedhog/data/integration_provider.yaml +16 -0
- package/hedhog/data/mail.yaml +88 -61
- package/hedhog/data/mail_layout.yaml +188 -0
- package/hedhog/data/menu.yaml +67 -3
- package/hedhog/data/route.yaml +135 -0
- package/hedhog/data/setting_group.yaml +139 -1
- package/hedhog/frontend/app/account/components/mfa-setup-dialog.tsx.ejs +36 -0
- package/hedhog/frontend/app/account/components/two-factor-auth.tsx.ejs +2 -0
- package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +10 -7
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +7 -4
- package/hedhog/frontend/app/ai_instruction/_components/ai-instruction-form.tsx.ejs +580 -0
- package/hedhog/frontend/app/ai_instruction/_components/ai-instruction-preview.tsx.ejs +150 -0
- package/hedhog/frontend/app/ai_instruction/_components/ai-instruction-versions.tsx.ejs +199 -0
- package/hedhog/frontend/app/ai_instruction/_components/prompt-diff.tsx.ejs +63 -0
- package/hedhog/frontend/app/ai_instruction/_lib/line-diff.spec.ts.ejs +59 -0
- package/hedhog/frontend/app/ai_instruction/_lib/line-diff.ts.ejs +72 -0
- package/hedhog/frontend/app/ai_instruction/_lib/prompt-template.spec.ts.ejs +170 -0
- package/hedhog/frontend/app/ai_instruction/_lib/prompt-template.ts.ejs +167 -0
- package/hedhog/frontend/app/ai_instruction/_lib/types.ts.ejs +99 -0
- package/hedhog/frontend/app/ai_instruction/page.tsx.ejs +422 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +51 -4
- package/hedhog/frontend/app/incidents/page.tsx.ejs +400 -0
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +153 -84
- package/hedhog/frontend/app/mail/layout/page.tsx.ejs +344 -0
- package/hedhog/frontend/app/mail/template/page.tsx.ejs +155 -121
- package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +129 -17
- package/hedhog/frontend/app/pages/_components/page-delete-dialog.tsx.ejs +343 -0
- package/hedhog/frontend/app/pages/_components/page-detail-sheet.tsx.ejs +438 -0
- package/hedhog/frontend/app/pages/_components/page-wizard-sheet.tsx.ejs +1009 -0
- package/hedhog/frontend/app/pages/_components/system-page-types.ts.ejs +56 -0
- package/hedhog/frontend/app/pages/_components/use-system-pages.ts.ejs +171 -0
- package/hedhog/frontend/app/pages/page.tsx.ejs +514 -0
- package/hedhog/frontend/app/users/page.tsx.ejs +119 -13
- package/hedhog/frontend/app/users/user-activity-timeline.tsx.ejs +231 -0
- package/hedhog/frontend/app/users/user-mails.tsx.ejs +399 -0
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +60 -75
- package/hedhog/frontend/messages/en.json +499 -3
- package/hedhog/frontend/messages/pt.json +499 -3
- package/hedhog/table/ai_agent.yaml +1 -0
- package/hedhog/table/ai_instruction.yaml +15 -0
- package/hedhog/table/leaked_password.yaml +18 -0
- package/hedhog/table/mail_layout.yaml +35 -0
- package/hedhog/table/mail_var.yaml +4 -0
- package/hedhog/table/setting.yaml +1 -0
- package/hedhog/table/system_incident.yaml +59 -0
- package/hedhog/table/user_presence.yaml +36 -0
- package/package.json +8 -4
- package/src/access-log/access-log.interceptor.ts +4 -2
- package/src/ai/ai-attachment.fragments.spec.ts +319 -0
- package/src/ai/ai-attachment.fragments.ts +35 -0
- package/src/ai/ai.prompts.ts +38 -0
- package/src/ai/ai.service.spec.ts +280 -0
- package/src/ai/ai.service.ts +338 -82
- package/src/ai/dto/chat.dto.ts +33 -3
- package/src/ai/dto/create-agent.dto.ts +2 -2
- package/src/ai/dto/update-agent.dto.ts +2 -2
- package/src/ai-instruction/ai-instruction.controller.ts +40 -9
- package/src/ai-instruction/ai-instruction.module.ts +13 -2
- package/src/ai-instruction/ai-instruction.service.spec.ts +658 -0
- package/src/ai-instruction/ai-instruction.service.ts +661 -62
- package/src/ai-instruction/ai-prompt-builder.service.ts +10 -1
- package/src/ai-instruction/ai-prompt-registry.ts +87 -0
- package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +375 -0
- package/src/ai-instruction/ai-prompt-resolver.service.ts +295 -0
- package/src/ai-instruction/ai-prompt-template.util.spec.ts +162 -0
- package/src/ai-instruction/ai-prompt-template.util.ts +199 -0
- package/src/ai-instruction/dto/create-ai-instruction.dto.ts +39 -3
- package/src/ai-instruction/dto/preview-ai-instruction.dto.ts +26 -0
- package/src/ai-instruction/dto/update-ai-instruction.dto.ts +42 -1
- package/src/ai-instruction/types/ai-instruction.types.ts +24 -1
- package/src/auth/auth.controller.ts +31 -5
- package/src/auth/auth.service.spec.ts +3 -1
- package/src/auth/auth.service.ts +101 -36
- package/src/captcha/captcha.module.ts +10 -0
- package/src/captcha/captcha.service.spec.ts +210 -0
- package/src/captcha/captcha.service.ts +191 -0
- package/src/challenge/challenge.service.spec.ts +39 -4
- package/src/challenge/challenge.service.ts +72 -17
- package/src/core.module.ts +27 -1
- package/src/file/file.service.spec.ts +105 -0
- package/src/file/file.service.ts +60 -3
- package/src/incident/incident-notifier.service.spec.ts +205 -0
- package/src/incident/incident-notifier.service.ts +241 -0
- package/src/incident/incident.controller.ts +38 -0
- package/src/incident/incident.module.ts +19 -0
- package/src/incident/incident.service.spec.ts +150 -0
- package/src/incident/incident.service.ts +209 -0
- package/src/index.ts +37 -0
- package/src/integration-profile/dto/test.dto.ts +19 -0
- package/src/integration-profile/integration-profile.controller.ts +10 -1
- package/src/integration-profile/integration-profile.module.spec.ts +51 -0
- package/src/integration-profile/integration-profile.module.ts +23 -4
- package/src/integration-profile/integration-profile.secrets.ts +9 -1
- package/src/integration-profile/integration-profile.service.spec.ts +279 -0
- package/src/integration-profile/integration-profile.service.ts +120 -124
- package/src/integration-profile/integration-profile.utils.spec.ts +22 -0
- package/src/integration-profile/integration-profile.utils.ts +40 -3
- package/src/integration-profile/testers/ai.testers.ts +165 -0
- package/src/integration-profile/testers/captcha.testers.spec.ts +92 -0
- package/src/integration-profile/testers/captcha.testers.ts +139 -0
- package/src/integration-profile/testers/email.testers.ts +161 -0
- package/src/integration-profile/testers/http-probe.spec.ts +153 -0
- package/src/integration-profile/testers/http-probe.ts +171 -0
- package/src/integration-profile/testers/index.ts +30 -0
- package/src/integration-profile/testers/infrastructure.testers.ts +122 -0
- package/src/integration-profile/testers/integration-tester.registry.spec.ts +75 -0
- package/src/integration-profile/testers/integration-tester.registry.ts +43 -0
- package/src/integration-profile/testers/integration-tester.types.ts +61 -0
- package/src/integration-profile/testers/messaging.testers.ts +102 -0
- package/src/integration-profile/testers/oauth.testers.spec.ts +220 -0
- package/src/integration-profile/testers/oauth.testers.ts +305 -0
- package/src/integration-profile/testers/payment.testers.ts +185 -0
- package/src/integration-profile/testers/remaining.testers.spec.ts +401 -0
- package/src/integration-profile/testers/storage.testers.spec.ts +185 -0
- package/src/integration-profile/testers/storage.testers.ts +226 -0
- package/src/integration-profile/testers/tester-coverage.spec.ts +58 -0
- package/src/language/en.json +4 -2
- package/src/language/pt.json +4 -2
- package/src/leaked-password/README.md +83 -0
- package/src/leaked-password/leaked-password.controller.ts +52 -0
- package/src/leaked-password/leaked-password.module.ts +18 -0
- package/src/leaked-password/leaked-password.service.ts +242 -0
- package/src/leaked-password/seed/build-seed-sql.mjs +180 -0
- package/src/leaked-password/seed/common-passwords.txt +335 -0
- package/src/mail/dto/preview.dto.ts +50 -0
- package/src/mail/dto/send.dto.ts +14 -1
- package/src/mail/mail-template.spec.ts +197 -0
- package/src/mail/mail.controller.ts +9 -2
- package/src/mail/mail.service.spec.ts +116 -1
- package/src/mail/mail.service.ts +118 -12
- package/src/mail-layout/dto/update.dto.ts +19 -0
- package/src/mail-layout/mail-layout.controller.ts +47 -0
- package/src/mail-layout/mail-layout.module.ts +22 -0
- package/src/mail-layout/mail-layout.service.ts +94 -0
- package/src/mcp-chat/mcp-chat.service.spec.ts +236 -0
- package/src/mcp-chat/mcp-chat.service.ts +122 -26
- package/src/oauth/oauth-token-broker.service.spec.ts +172 -0
- package/src/oauth/oauth-token-broker.service.ts +363 -0
- package/src/oauth/oauth.module.ts +3 -1
- package/src/oauth/oauth.service.ts +8 -2
- package/src/pdf/pdf.service.ts +75 -0
- package/src/presence/presence.controller.ts +29 -0
- package/src/presence/presence.listener.ts +53 -0
- package/src/presence/presence.module.ts +19 -0
- package/src/presence/presence.service.spec.ts +97 -0
- package/src/presence/presence.service.ts +160 -0
- package/src/profile/dto/delete-account.dto.ts +11 -0
- package/src/profile/events/profile-account-deleted.event.ts +13 -0
- package/src/profile/profile.controller.ts +46 -34
- package/src/profile/profile.service.ts +138 -38
- package/src/realtime/realtime.gateway.ts +138 -0
- package/src/realtime/realtime.module.ts +16 -0
- package/src/realtime/realtime.service.spec.ts +50 -0
- package/src/realtime/realtime.service.ts +187 -0
- package/src/redis/redis-pubsub.ts +179 -0
- package/src/setting/dto/create.dto.ts +13 -2
- package/src/setting/setting.service.spec.ts +56 -0
- package/src/setting/setting.service.ts +29 -1
- package/src/user/dto/create-with-email-and-password.dto.ts +6 -1
- package/src/user/dto/query-user-mail-list.dto.ts +10 -0
- package/src/user/user-credential.util.ts +35 -0
- package/src/user/user.controller.ts +17 -2
- package/src/user/user.service.spec.ts +153 -0
- package/src/user/user.service.ts +255 -12
- package/src/validators/is-strong-password-with-settings.validator.ts +12 -0
- package/src/validators/service-locator.ts +15 -0
- package/src/whatsapp-sender/whatsapp-sender.module.ts +10 -0
- package/src/whatsapp-sender/whatsapp-sender.service.ts +313 -0
|
@@ -1,35 +1,100 @@
|
|
|
1
1
|
import { PaginationDTO } from '@hed-hog/api-pagination';
|
|
2
2
|
import { Prisma, PrismaService } from '@hed-hog/api-prisma';
|
|
3
3
|
import {
|
|
4
|
+
ConflictException,
|
|
4
5
|
forwardRef,
|
|
5
6
|
Inject,
|
|
6
7
|
Injectable,
|
|
7
8
|
Logger,
|
|
8
9
|
NotFoundException,
|
|
10
|
+
OnModuleDestroy,
|
|
9
11
|
OnModuleInit,
|
|
12
|
+
UnprocessableEntityException,
|
|
10
13
|
} from '@nestjs/common';
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
11
15
|
import { DeleteDTO } from '../dto/delete.dto';
|
|
16
|
+
import { createRedisPubSub, type RedisPubSub } from '../redis/redis-pubsub';
|
|
17
|
+
import {
|
|
18
|
+
type AiPromptDefinition,
|
|
19
|
+
getAiPromptDefinition,
|
|
20
|
+
listAiPromptDefinitions,
|
|
21
|
+
} from './ai-prompt-registry';
|
|
22
|
+
import { estimateTokens, renderTemplate, validateTemplate } from './ai-prompt-template.util';
|
|
12
23
|
import { CreateAiInstructionDTO } from './dto/create-ai-instruction.dto';
|
|
24
|
+
import { PreviewAiInstructionDTO } from './dto/preview-ai-instruction.dto';
|
|
13
25
|
import { UpdateAiInstructionDTO } from './dto/update-ai-instruction.dto';
|
|
14
26
|
import type {
|
|
15
27
|
AiInstructionLayer,
|
|
28
|
+
AiInstructionListFilters,
|
|
16
29
|
AiInstructionRecord,
|
|
17
30
|
} from './types/ai-instruction.types';
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
/**
|
|
33
|
+
* `record: null` is a negative cache entry — the slug/layer does not exist.
|
|
34
|
+
* Without it, every lookup for a missing slug hits the database on every
|
|
35
|
+
* request (task prompts are resolved on every LLM call).
|
|
36
|
+
*/
|
|
37
|
+
type CacheEntry = { record: AiInstructionRecord | null; fetchedAt: number; ttl: number };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cross-pod invalidation happens through Redis, with the database version check
|
|
41
|
+
* as backup. These TTLs are the last resort for when both are unavailable.
|
|
42
|
+
*/
|
|
21
43
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
22
|
-
|
|
23
|
-
const
|
|
44
|
+
const TASK_CACHE_TTL_MS = 60 * 1000;
|
|
45
|
+
const NEGATIVE_CACHE_TTL_MS = 60 * 1000;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An empty `slugs` list means "drop everything" — the database version path
|
|
49
|
+
* knows a change happened but not which prompts it touched.
|
|
50
|
+
*/
|
|
51
|
+
type AiInstructionInvalidationMessage = {
|
|
52
|
+
originInstanceId: string;
|
|
53
|
+
version: number;
|
|
54
|
+
slugs: string[];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reuses the generic runtime-version table owned by the settings service. No
|
|
59
|
+
* column there is setting-specific, and the unique index on `scope` means a new
|
|
60
|
+
* scope needs no migration — the idempotent INSERT below creates its own row.
|
|
61
|
+
*/
|
|
62
|
+
const RUNTIME_STATE_SCOPE = 'ai_instruction';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Columns are cast explicitly because `$queryRaw` returns DECIMAL as
|
|
66
|
+
* `Prisma.Decimal`, which serializes as a string over HTTP and becomes NaN in
|
|
67
|
+
* arithmetic. Keep every SELECT in this file using this list.
|
|
68
|
+
*/
|
|
69
|
+
const INSTRUCTION_COLUMNS = Prisma.sql`
|
|
70
|
+
id, slug, layer, name, content, variables_schema, locale, module_slug,
|
|
71
|
+
is_active, version, model, temperature::float8 AS temperature, max_tokens,
|
|
72
|
+
is_locked, created_at, updated_at
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
export const FALLBACK_SYSTEM_PROMPT =
|
|
24
76
|
'You are a helpful assistant with access to tools from the HedHog system. ' +
|
|
25
77
|
"Use the available tools to answer the user's questions accurately. " +
|
|
26
78
|
'Always respond in the same language the user writes in.';
|
|
27
79
|
|
|
28
80
|
@Injectable()
|
|
29
|
-
export class AiInstructionService implements OnModuleInit {
|
|
81
|
+
export class AiInstructionService implements OnModuleInit, OnModuleDestroy {
|
|
30
82
|
private readonly logger = new Logger(AiInstructionService.name);
|
|
31
83
|
private readonly cache = new Map<string, CacheEntry>();
|
|
32
84
|
|
|
85
|
+
private readonly instanceId = randomUUID();
|
|
86
|
+
private readonly redisChannel = 'core:ai-instruction:invalidated';
|
|
87
|
+
private readonly versionCheckIntervalMs = Math.max(
|
|
88
|
+
1000,
|
|
89
|
+
Number(process.env.AI_INSTRUCTION_VERSION_CHECK_INTERVAL_MS || 5000),
|
|
90
|
+
);
|
|
91
|
+
private localCacheVersion = 0;
|
|
92
|
+
private lastVersionCheckAt = 0;
|
|
93
|
+
private versionCheckInFlight: Promise<void> | null = null;
|
|
94
|
+
private versionStateEnsured = false;
|
|
95
|
+
private lastVersionCheckErrorLogAt = 0;
|
|
96
|
+
private pubsub: RedisPubSub | null = null;
|
|
97
|
+
|
|
33
98
|
constructor(
|
|
34
99
|
@Inject(forwardRef(() => PrismaService))
|
|
35
100
|
private readonly prisma: PrismaService,
|
|
@@ -38,10 +103,46 @@ export class AiInstructionService implements OnModuleInit {
|
|
|
38
103
|
async onModuleInit() {
|
|
39
104
|
// Warm up global and product layers on startup
|
|
40
105
|
try {
|
|
106
|
+
// Before the warm-up: starting at version 0 would make the first read
|
|
107
|
+
// think it is behind and throw the freshly warmed cache away.
|
|
108
|
+
await this.syncCacheVersionFromDatabase();
|
|
41
109
|
await this.getLayer('system');
|
|
42
110
|
await this.getLayer('product');
|
|
111
|
+
await this.warnAboutUnseededPrompts();
|
|
43
112
|
} catch {
|
|
44
|
-
// DB may not be ready yet; cache
|
|
113
|
+
// DB may not be ready yet; cache and version fill in on first request
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Outside the try: createRedisPubSub already handles its own failures.
|
|
117
|
+
this.pubsub = await createRedisPubSub({
|
|
118
|
+
channel: this.redisChannel,
|
|
119
|
+
logger: this.logger,
|
|
120
|
+
label: 'ai instruction',
|
|
121
|
+
onMessage: (raw) => this.handleInvalidationMessage(raw),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async onModuleDestroy() {
|
|
126
|
+
await this.pubsub?.close();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reports prompts declared in code that have no row yet. Without this, a
|
|
131
|
+
* forgotten seed only surfaces the first time that AI flow runs — which for
|
|
132
|
+
* essay grading means the first graded exam.
|
|
133
|
+
*/
|
|
134
|
+
private async warnAboutUnseededPrompts() {
|
|
135
|
+
const definitions = listAiPromptDefinitions();
|
|
136
|
+
if (!definitions.length) return;
|
|
137
|
+
|
|
138
|
+
const records = await this.getRecordsBySlugs(definitions.map((d) => d.slug));
|
|
139
|
+
const missing = definitions.filter((d) => !records.get(d.slug)).map((d) => d.slug);
|
|
140
|
+
|
|
141
|
+
if (missing.length) {
|
|
142
|
+
this.logger.warn(
|
|
143
|
+
`${missing.length} AI prompt(s) declared in code have no ai_instruction row ` +
|
|
144
|
+
`and will use the code fallback: ${missing.join(', ')}`,
|
|
145
|
+
);
|
|
45
146
|
}
|
|
46
147
|
}
|
|
47
148
|
|
|
@@ -50,70 +151,349 @@ export class AiInstructionService implements OnModuleInit {
|
|
|
50
151
|
async getLayer(layer: AiInstructionLayer, moduleSlug?: string): Promise<string> {
|
|
51
152
|
const cacheKey = moduleSlug ? `${layer}:${moduleSlug}` : layer;
|
|
52
153
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
154
|
+
await this.ensureCacheVersionFresh();
|
|
155
|
+
|
|
156
|
+
const cached = this.readCache(cacheKey);
|
|
157
|
+
if (cached !== undefined) {
|
|
158
|
+
return cached?.content ?? (layer === 'system' ? FALLBACK_SYSTEM_PROMPT : '');
|
|
56
159
|
}
|
|
57
160
|
|
|
58
161
|
const instruction = await this.findActiveByLayer(layer, moduleSlug);
|
|
162
|
+
this.writeCache(cacheKey, instruction);
|
|
163
|
+
|
|
59
164
|
if (!instruction) {
|
|
60
|
-
|
|
61
|
-
return FALLBACK_SYSTEM_PROMPT;
|
|
62
|
-
}
|
|
63
|
-
return '';
|
|
165
|
+
return layer === 'system' ? FALLBACK_SYSTEM_PROMPT : '';
|
|
64
166
|
}
|
|
65
|
-
|
|
66
|
-
this.cache.set(cacheKey, { content: instruction.content, fetchedAt: Date.now() });
|
|
67
167
|
return instruction.content;
|
|
68
168
|
}
|
|
69
169
|
|
|
70
170
|
async getAgentInstruction(agentSlug: string): Promise<string> {
|
|
71
171
|
const cacheKey = `agent:${agentSlug}`;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
172
|
+
|
|
173
|
+
await this.ensureCacheVersionFresh();
|
|
174
|
+
|
|
175
|
+
const cached = this.readCache(cacheKey);
|
|
176
|
+
if (cached !== undefined) return cached?.content ?? '';
|
|
76
177
|
|
|
77
178
|
const instruction = await this.findActiveBySlug(`agent.${agentSlug}`);
|
|
78
|
-
|
|
179
|
+
this.writeCache(cacheKey, instruction);
|
|
79
180
|
|
|
80
|
-
|
|
81
|
-
return instruction.content;
|
|
181
|
+
return instruction?.content ?? '';
|
|
82
182
|
}
|
|
83
183
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Full record by exact slug, cached. This is what `AiPromptResolverService`
|
|
186
|
+
* uses for task prompts — it needs the model params and `is_locked`, not just
|
|
187
|
+
* the content.
|
|
188
|
+
*/
|
|
189
|
+
async getRecordBySlug(slug: string): Promise<AiInstructionRecord | null> {
|
|
190
|
+
const cacheKey = `slug:${slug}`;
|
|
191
|
+
|
|
192
|
+
await this.ensureCacheVersionFresh();
|
|
193
|
+
|
|
194
|
+
const cached = this.readCache(cacheKey);
|
|
195
|
+
if (cached !== undefined) return cached;
|
|
196
|
+
|
|
197
|
+
const record = await this.findBySlug(slug);
|
|
198
|
+
this.writeCache(cacheKey, record);
|
|
199
|
+
|
|
200
|
+
return record;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** One query for pipelines that resolve several prompts per call. */
|
|
204
|
+
async getRecordsBySlugs(slugs: string[]): Promise<Map<string, AiInstructionRecord | null>> {
|
|
205
|
+
const result = new Map<string, AiInstructionRecord | null>();
|
|
206
|
+
const missing: string[] = [];
|
|
207
|
+
|
|
208
|
+
await this.ensureCacheVersionFresh();
|
|
209
|
+
|
|
210
|
+
for (const slug of slugs) {
|
|
211
|
+
const cached = this.readCache(`slug:${slug}`);
|
|
212
|
+
if (cached !== undefined) {
|
|
213
|
+
result.set(slug, cached);
|
|
91
214
|
} else {
|
|
92
|
-
|
|
93
|
-
const identifier = slug.slice(dotIndex + 1);
|
|
94
|
-
this.cache.delete(layer);
|
|
95
|
-
this.cache.delete(`${layer}:${identifier}`);
|
|
215
|
+
missing.push(slug);
|
|
96
216
|
}
|
|
97
|
-
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (missing.length) {
|
|
220
|
+
const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
|
|
221
|
+
SELECT ${INSTRUCTION_COLUMNS}
|
|
222
|
+
FROM ai_instruction WHERE slug IN (${Prisma.join(missing)})
|
|
223
|
+
`);
|
|
224
|
+
|
|
225
|
+
const found = new Map(rows.map((row) => [row.slug, row]));
|
|
226
|
+
for (const slug of missing) {
|
|
227
|
+
const record = found.get(slug) || null;
|
|
228
|
+
this.writeCache(`slug:${slug}`, record);
|
|
229
|
+
result.set(slug, record);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
invalidateCache(slug?: string) {
|
|
237
|
+
if (!slug) {
|
|
98
238
|
this.cache.clear();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Exact-slug key used by getRecordBySlug/getRecordsBySlugs.
|
|
243
|
+
this.cache.delete(`slug:${slug}`);
|
|
244
|
+
|
|
245
|
+
// Layer keys use format "layer" or "layer:identifier", while slugs use
|
|
246
|
+
// "layer.identifier" (e.g. "module.lms", "agent.admin-support").
|
|
247
|
+
const dotIndex = slug.indexOf('.');
|
|
248
|
+
if (dotIndex === -1) {
|
|
249
|
+
this.cache.delete(slug);
|
|
250
|
+
} else {
|
|
251
|
+
const layer = slug.slice(0, dotIndex);
|
|
252
|
+
const identifier = slug.slice(dotIndex + 1);
|
|
253
|
+
this.cache.delete(layer);
|
|
254
|
+
this.cache.delete(`${layer}:${identifier}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** `undefined` = cache miss/expired. `null` = cached absence. */
|
|
259
|
+
private readCache(cacheKey: string): AiInstructionRecord | null | undefined {
|
|
260
|
+
const cached = this.cache.get(cacheKey);
|
|
261
|
+
if (!cached) return undefined;
|
|
262
|
+
if (Date.now() - cached.fetchedAt >= cached.ttl) {
|
|
263
|
+
this.cache.delete(cacheKey);
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
return cached.record;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private writeCache(cacheKey: string, record: AiInstructionRecord | null) {
|
|
270
|
+
const ttl = !record
|
|
271
|
+
? NEGATIVE_CACHE_TTL_MS
|
|
272
|
+
: record.layer === 'task'
|
|
273
|
+
? TASK_CACHE_TTL_MS
|
|
274
|
+
: CACHE_TTL_MS;
|
|
275
|
+
|
|
276
|
+
this.cache.set(cacheKey, { record, fetchedAt: Date.now(), ttl });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Cross-pod invalidation ───────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
private handleInvalidationMessage(raw: string) {
|
|
282
|
+
let payload: AiInstructionInvalidationMessage;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
payload = JSON.parse(raw) as AiInstructionInvalidationMessage;
|
|
286
|
+
} catch {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!payload || payload.originInstanceId === this.instanceId) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// The version is only a watermark, so the lazy check does not re-clear a
|
|
295
|
+
// cache this message already fixed selectively. The invalidation itself is
|
|
296
|
+
// applied unconditionally: dropping an out-of-order message — this pod may
|
|
297
|
+
// have bumped past it on a write of its own — would leave the slug stale
|
|
298
|
+
// here until its TTL, which is exactly the bug this whole path fixes.
|
|
299
|
+
if (Number.isFinite(payload.version)) {
|
|
300
|
+
this.localCacheVersion = Math.max(this.localCacheVersion, payload.version);
|
|
301
|
+
this.lastVersionCheckAt = Date.now();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const slugs = Array.isArray(payload.slugs) ? payload.slugs.filter(Boolean) : [];
|
|
305
|
+
if (!slugs.length) {
|
|
306
|
+
this.invalidateCache();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const slug of slugs) {
|
|
311
|
+
this.invalidateCache(slug);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async ensureVersionState() {
|
|
316
|
+
// The row is created once and never removed; repeating the INSERT on every
|
|
317
|
+
// version check would double the queries of a path that runs every 5s.
|
|
318
|
+
if (this.versionStateEnsured) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
await this.prisma.$executeRawUnsafe(`
|
|
323
|
+
INSERT INTO "setting_runtime_state" ("scope", "version", "updated_at")
|
|
324
|
+
VALUES ('${RUNTIME_STATE_SCOPE}', 0, NOW())
|
|
325
|
+
ON CONFLICT ("scope") DO NOTHING
|
|
326
|
+
`);
|
|
327
|
+
this.versionStateEnsured = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** BIGINT comes back from `$queryRawUnsafe` as a `bigint`, not a number. */
|
|
331
|
+
private parseVersionValue(value: unknown): number {
|
|
332
|
+
if (typeof value === 'number') {
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (typeof value === 'bigint') {
|
|
337
|
+
return Number(value);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (typeof value === 'string') {
|
|
341
|
+
return Number(value);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private async getCacheVersionFromDatabase(): Promise<number> {
|
|
348
|
+
await this.ensureVersionState();
|
|
349
|
+
|
|
350
|
+
const rows = await this.prisma.$queryRawUnsafe<Array<{ version: unknown }>>(`
|
|
351
|
+
SELECT "version"
|
|
352
|
+
FROM "setting_runtime_state"
|
|
353
|
+
WHERE "scope" = '${RUNTIME_STATE_SCOPE}'
|
|
354
|
+
LIMIT 1
|
|
355
|
+
`);
|
|
356
|
+
|
|
357
|
+
return this.parseVersionValue(rows[0]?.version);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async syncCacheVersionFromDatabase() {
|
|
361
|
+
this.localCacheVersion = await this.getCacheVersionFromDatabase();
|
|
362
|
+
this.lastVersionCheckAt = Date.now();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async bumpCacheVersion(): Promise<number> {
|
|
366
|
+
await this.ensureVersionState();
|
|
367
|
+
|
|
368
|
+
const rows = await this.prisma.$queryRawUnsafe<Array<{ version: unknown }>>(`
|
|
369
|
+
UPDATE "setting_runtime_state"
|
|
370
|
+
SET "version" = "version" + 1,
|
|
371
|
+
"updated_at" = NOW()
|
|
372
|
+
WHERE "scope" = '${RUNTIME_STATE_SCOPE}'
|
|
373
|
+
RETURNING "version"
|
|
374
|
+
`);
|
|
375
|
+
|
|
376
|
+
const nextVersion = this.parseVersionValue(rows[0]?.version);
|
|
377
|
+
this.localCacheVersion = nextVersion;
|
|
378
|
+
this.lastVersionCheckAt = Date.now();
|
|
379
|
+
return nextVersion;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Catches up with writes from other pods when Redis is unavailable. Throttled
|
|
384
|
+
* and coalesced, so a burst of prompt resolutions costs one query at most.
|
|
385
|
+
*/
|
|
386
|
+
private async ensureCacheVersionFresh() {
|
|
387
|
+
const now = Date.now();
|
|
388
|
+
if (now - this.lastVersionCheckAt < this.versionCheckIntervalMs) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// `lastVersionCheckAt` only advances after the await, so parallel reads
|
|
393
|
+
// would each fire the same query and tie up the connection pool.
|
|
394
|
+
if (this.versionCheckInFlight) {
|
|
395
|
+
return this.versionCheckInFlight;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
this.versionCheckInFlight = this.runVersionCheck(now).finally(() => {
|
|
399
|
+
this.versionCheckInFlight = null;
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return this.versionCheckInFlight;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private async runVersionCheck(startedAt: number) {
|
|
406
|
+
try {
|
|
407
|
+
const currentVersion = await this.getCacheVersionFromDatabase();
|
|
408
|
+
this.lastVersionCheckAt = startedAt;
|
|
409
|
+
|
|
410
|
+
if (currentVersion > this.localCacheVersion) {
|
|
411
|
+
this.localCacheVersion = currentVersion;
|
|
412
|
+
// This path knows a change happened but not which slugs it touched.
|
|
413
|
+
this.cache.clear();
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
// Bookkeeping must never break prompt resolution: the resolver handles a
|
|
417
|
+
// throw by falling back to the code prompt, so a slow database would
|
|
418
|
+
// silently downgrade prompt quality. Serving the current cache is better.
|
|
419
|
+
this.lastVersionCheckAt = startedAt; // and do not hammer a sick database
|
|
420
|
+
this.logVersionCheckError(error);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private logVersionCheckError(error: unknown) {
|
|
425
|
+
const now = Date.now();
|
|
426
|
+
if (now - this.lastVersionCheckErrorLogAt < 30000) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.lastVersionCheckErrorLogAt = now;
|
|
430
|
+
this.logger.warn(
|
|
431
|
+
`Failed to check ai instruction cache version: ${error instanceof Error ? error.message : String(error)}`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Tells the other pods to drop these slugs. Bumps the database version first
|
|
437
|
+
* so pods without Redis still catch up on their next version check.
|
|
438
|
+
*/
|
|
439
|
+
private async publishInvalidation(slugs: string[]) {
|
|
440
|
+
const filteredSlugs = Array.from(new Set(slugs.filter(Boolean)));
|
|
441
|
+
if (filteredSlugs.length === 0) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let version = this.localCacheVersion;
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
version = await this.bumpCacheVersion();
|
|
449
|
+
} catch (error) {
|
|
450
|
+
// The prompt write already committed; failing here must not turn a
|
|
451
|
+
// successful edit into a 500.
|
|
452
|
+
this.logger.warn(
|
|
453
|
+
`Failed to bump ai instruction cache version: ${error instanceof Error ? error.message : String(error)}`,
|
|
454
|
+
);
|
|
99
455
|
}
|
|
456
|
+
|
|
457
|
+
// A stale version here is harmless: receivers take the max, and the lazy
|
|
458
|
+
// check compares against the database, not against this number.
|
|
459
|
+
await this.pubsub?.publish({
|
|
460
|
+
originInstanceId: this.instanceId,
|
|
461
|
+
version,
|
|
462
|
+
slugs: filteredSlugs,
|
|
463
|
+
} satisfies AiInstructionInvalidationMessage);
|
|
100
464
|
}
|
|
101
465
|
|
|
102
466
|
// ── CRUD ─────────────────────────────────────────────────────────────────
|
|
103
467
|
|
|
104
|
-
async list(paginationParams: PaginationDTO) {
|
|
468
|
+
async list(paginationParams: PaginationDTO, filters?: AiInstructionListFilters) {
|
|
105
469
|
const page = Math.max(Number(paginationParams?.page || 1), 1);
|
|
106
470
|
const pageSize = Math.min(Math.max(Number(paginationParams?.pageSize || 20), 1), 100);
|
|
107
471
|
const offset = (page - 1) * pageSize;
|
|
108
472
|
const search = String(paginationParams?.search || '').trim();
|
|
109
473
|
|
|
110
|
-
const
|
|
111
|
-
|
|
474
|
+
const conditions: Prisma.Sql[] = [];
|
|
475
|
+
|
|
476
|
+
if (search) {
|
|
477
|
+
conditions.push(
|
|
478
|
+
Prisma.sql`(LOWER(slug) LIKE LOWER(${`%${search}%`}) OR LOWER(name) LIKE LOWER(${`%${search}%`}))`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
if (filters?.layer) {
|
|
482
|
+
conditions.push(Prisma.sql`layer = CAST(${filters.layer} AS ai_instruction_layer_enum)`);
|
|
483
|
+
}
|
|
484
|
+
if (filters?.module_slug) {
|
|
485
|
+
conditions.push(Prisma.sql`module_slug = ${filters.module_slug}`);
|
|
486
|
+
}
|
|
487
|
+
if (filters?.is_active !== undefined) {
|
|
488
|
+
conditions.push(Prisma.sql`is_active = ${filters.is_active}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const whereClause = conditions.length
|
|
492
|
+
? Prisma.sql`WHERE ${Prisma.join(conditions, ' AND ')}`
|
|
112
493
|
: Prisma.empty;
|
|
113
494
|
|
|
114
495
|
const data = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
|
|
115
|
-
SELECT
|
|
116
|
-
is_active, version, created_at, updated_at
|
|
496
|
+
SELECT ${INSTRUCTION_COLUMNS}
|
|
117
497
|
FROM ai_instruction
|
|
118
498
|
${whereClause}
|
|
119
499
|
ORDER BY layer, slug
|
|
@@ -136,14 +516,95 @@ export class AiInstructionService implements OnModuleInit {
|
|
|
136
516
|
};
|
|
137
517
|
}
|
|
138
518
|
|
|
519
|
+
/** Distinct module slugs, for the module filter in the admin. */
|
|
520
|
+
async listModules(): Promise<string[]> {
|
|
521
|
+
const rows = await this.prisma.$queryRaw<Array<{ module_slug: string }>>(Prisma.sql`
|
|
522
|
+
SELECT DISTINCT module_slug FROM ai_instruction
|
|
523
|
+
WHERE module_slug IS NOT NULL
|
|
524
|
+
ORDER BY module_slug
|
|
525
|
+
`);
|
|
526
|
+
return rows.map((row) => row.module_slug);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The prompts declared in code, paired with what the database currently holds.
|
|
531
|
+
*
|
|
532
|
+
* `dbMatchesFallback` is what lets the admin show "default" vs "customized"
|
|
533
|
+
* and offer "restore default"; it is also the fidelity check after a seed —
|
|
534
|
+
* every freshly seeded prompt must report `true`.
|
|
535
|
+
*/
|
|
536
|
+
async listRegistry(moduleSlug?: string) {
|
|
537
|
+
const definitions = listAiPromptDefinitions(moduleSlug);
|
|
538
|
+
const records = await this.getRecordsBySlugs(definitions.map((d) => d.slug));
|
|
539
|
+
|
|
540
|
+
return definitions.map((definition) => {
|
|
541
|
+
const record = records.get(definition.slug) || null;
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
slug: definition.slug,
|
|
545
|
+
name: definition.name,
|
|
546
|
+
module_slug: definition.moduleSlug,
|
|
547
|
+
locale: definition.locale || 'pt',
|
|
548
|
+
fallback: definition.fallback,
|
|
549
|
+
variables: definition.variables,
|
|
550
|
+
security_sensitive: Boolean(definition.securitySensitive),
|
|
551
|
+
required_phrases: definition.requiredPhrases || [],
|
|
552
|
+
defaults: definition.defaults || {},
|
|
553
|
+
exists_in_db: Boolean(record),
|
|
554
|
+
db_matches_fallback: record ? record.content === definition.fallback : false,
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Renders a draft without saving it, so the admin can check interpolation. */
|
|
560
|
+
previewPrompt(input: PreviewAiInstructionDTO) {
|
|
561
|
+
const definition = input.slug ? getAiPromptDefinition(input.slug) : undefined;
|
|
562
|
+
const schema = definition?.variables ?? input.variables_schema ?? null;
|
|
563
|
+
const validation = validateTemplate(input.content, schema);
|
|
564
|
+
|
|
565
|
+
let rendered = '';
|
|
566
|
+
let renderError: string | null = null;
|
|
567
|
+
try {
|
|
568
|
+
rendered = renderTemplate(input.content, input.variables || {});
|
|
569
|
+
} catch (error) {
|
|
570
|
+
renderError = error instanceof Error ? error.message : String(error);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const warnings: string[] = [];
|
|
574
|
+
if (validation.unknownPlaceholders.length) {
|
|
575
|
+
warnings.push(
|
|
576
|
+
`Variáveis não declaradas: ${validation.unknownPlaceholders.join(', ')}. ` +
|
|
577
|
+
'Elas serão renderizadas literalmente.',
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
warnings.push(...this.missingPhraseWarnings(definition, input.content));
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
rendered,
|
|
584
|
+
render_error: renderError,
|
|
585
|
+
char_count: rendered.length,
|
|
586
|
+
estimated_tokens: estimateTokens(rendered),
|
|
587
|
+
missing_required: validation.missingRequired,
|
|
588
|
+
unknown_placeholders: validation.unknownPlaceholders,
|
|
589
|
+
unbalanced_blocks: validation.unbalancedBlocks,
|
|
590
|
+
warnings,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
139
594
|
async getById(id: number): Promise<AiInstructionRecord> {
|
|
140
595
|
const record = await this.findById(id);
|
|
141
596
|
if (!record) throw new NotFoundException(`AI instruction ${id} not found.`);
|
|
142
597
|
return record;
|
|
143
598
|
}
|
|
144
599
|
|
|
145
|
-
|
|
146
|
-
|
|
600
|
+
/**
|
|
601
|
+
* Inactive records are returned on purpose: the admin needs to open one to
|
|
602
|
+
* reactivate it. Pass `activeOnly` for the resolution path.
|
|
603
|
+
*/
|
|
604
|
+
async getBySlug(slug: string, activeOnly = false): Promise<AiInstructionRecord> {
|
|
605
|
+
const record = activeOnly
|
|
606
|
+
? await this.findActiveBySlug(slug)
|
|
607
|
+
: await this.findBySlug(slug);
|
|
147
608
|
if (!record) throw new NotFoundException(`AI instruction "${slug}" not found.`);
|
|
148
609
|
return record;
|
|
149
610
|
}
|
|
@@ -152,8 +613,18 @@ export class AiInstructionService implements OnModuleInit {
|
|
|
152
613
|
const locale = dto.locale || 'en';
|
|
153
614
|
const isActive = dto.is_active ?? true;
|
|
154
615
|
|
|
616
|
+
const existing = await this.findBySlug(dto.slug);
|
|
617
|
+
if (existing) {
|
|
618
|
+
throw new ConflictException(`AI instruction "${dto.slug}" already exists.`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
this.validateContent(dto.slug, dto.content, {
|
|
622
|
+
fallbackSchema: dto.variables_schema ?? null,
|
|
623
|
+
allowMissingVariables: dto.allow_missing_variables,
|
|
624
|
+
});
|
|
625
|
+
|
|
155
626
|
await this.prisma.$executeRaw`
|
|
156
|
-
INSERT INTO ai_instruction (slug, layer, name, content, variables_schema, locale, module_slug, is_active, version)
|
|
627
|
+
INSERT INTO ai_instruction (slug, layer, name, content, variables_schema, locale, module_slug, is_active, version, model, temperature, max_tokens)
|
|
157
628
|
VALUES (
|
|
158
629
|
${dto.slug},
|
|
159
630
|
CAST(${dto.layer} AS ai_instruction_layer_enum),
|
|
@@ -163,28 +634,61 @@ export class AiInstructionService implements OnModuleInit {
|
|
|
163
634
|
${locale},
|
|
164
635
|
${dto.module_slug || null},
|
|
165
636
|
${isActive},
|
|
166
|
-
1
|
|
637
|
+
1,
|
|
638
|
+
${dto.model ?? null},
|
|
639
|
+
${dto.temperature ?? null},
|
|
640
|
+
${dto.max_tokens ?? null}
|
|
167
641
|
)
|
|
168
642
|
`;
|
|
169
643
|
|
|
170
|
-
const created = await this.
|
|
644
|
+
const created = await this.findBySlug(dto.slug);
|
|
171
645
|
|
|
172
646
|
// Record version 1
|
|
173
647
|
if (created) {
|
|
174
648
|
await this.saveVersion(created.id, 1, dto.content, 'Initial version', createdById);
|
|
175
649
|
}
|
|
176
650
|
|
|
651
|
+
// Negative cache entries for this slug must go, otherwise the prompt stays
|
|
652
|
+
// invisible to the resolver until the TTL expires. Kept separate from the
|
|
653
|
+
// publish below so a Redis outage never leaves this pod itself stale.
|
|
654
|
+
this.invalidateCache(dto.slug);
|
|
655
|
+
await this.publishInvalidation([dto.slug]);
|
|
656
|
+
|
|
177
657
|
return created;
|
|
178
658
|
}
|
|
179
659
|
|
|
180
660
|
async update(id: number, dto: UpdateAiInstructionDTO, updatedById?: number) {
|
|
181
661
|
const current = await this.getById(id);
|
|
182
662
|
|
|
663
|
+
// Locked records are security-sensitive prompts (the essay-grading
|
|
664
|
+
// injection defence). Still editable, but never by accident.
|
|
665
|
+
if (current.is_locked && !dto.acknowledge_security_review) {
|
|
666
|
+
throw new ConflictException(
|
|
667
|
+
`AI instruction "${current.slug}" is a security-sensitive prompt. ` +
|
|
668
|
+
'Send acknowledge_security_review=true to confirm you reviewed the change.',
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
183
672
|
const newContent = dto.content ?? current.content;
|
|
184
673
|
const newVersion = dto.content && dto.content !== current.content
|
|
185
674
|
? current.version + 1
|
|
186
675
|
: current.version;
|
|
187
676
|
|
|
677
|
+
const warnings = dto.content !== undefined
|
|
678
|
+
? this.validateContent(current.slug, dto.content, {
|
|
679
|
+
fallbackSchema: current.variables_schema,
|
|
680
|
+
allowMissingVariables: dto.allow_missing_variables,
|
|
681
|
+
})
|
|
682
|
+
: [];
|
|
683
|
+
|
|
684
|
+
// Losing every guard phrase is recorded in the immutable version row, not
|
|
685
|
+
// just in the log — the resolver will also refuse the stored copy.
|
|
686
|
+
const changeNote = warnings.length
|
|
687
|
+
? [dto.change_note, `[warnings: ${warnings.join(' | ')}]`].filter(Boolean).join(' ').slice(0, 500)
|
|
688
|
+
: dto.change_note;
|
|
689
|
+
|
|
690
|
+
// `undefined` means "leave alone", `null` means "clear" — `??` collapses the
|
|
691
|
+
// two, so the model params use an explicit undefined check.
|
|
188
692
|
await this.prisma.$executeRaw`
|
|
189
693
|
UPDATE ai_instruction
|
|
190
694
|
SET
|
|
@@ -194,6 +698,9 @@ export class AiInstructionService implements OnModuleInit {
|
|
|
194
698
|
locale = ${dto.locale ?? current.locale},
|
|
195
699
|
module_slug = ${dto.module_slug !== undefined ? dto.module_slug : current.module_slug},
|
|
196
700
|
is_active = ${dto.is_active ?? current.is_active},
|
|
701
|
+
model = ${dto.model !== undefined ? dto.model : current.model},
|
|
702
|
+
temperature = ${dto.temperature !== undefined ? dto.temperature : current.temperature},
|
|
703
|
+
max_tokens = ${dto.max_tokens !== undefined ? dto.max_tokens : current.max_tokens},
|
|
197
704
|
version = ${newVersion},
|
|
198
705
|
updated_at = NOW()
|
|
199
706
|
WHERE id = ${id}
|
|
@@ -201,12 +708,13 @@ export class AiInstructionService implements OnModuleInit {
|
|
|
201
708
|
|
|
202
709
|
// Save new version if content changed
|
|
203
710
|
if (dto.content && dto.content !== current.content) {
|
|
204
|
-
await this.saveVersion(id, newVersion, newContent,
|
|
711
|
+
await this.saveVersion(id, newVersion, newContent, changeNote, updatedById);
|
|
205
712
|
}
|
|
206
713
|
|
|
207
714
|
this.invalidateCache(current.slug);
|
|
715
|
+
await this.publishInvalidation([current.slug]);
|
|
208
716
|
|
|
209
|
-
return this.getById(id);
|
|
717
|
+
return { ...(await this.getById(id)), warnings };
|
|
210
718
|
}
|
|
211
719
|
|
|
212
720
|
async delete({ ids }: DeleteDTO) {
|
|
@@ -224,6 +732,8 @@ export class AiInstructionService implements OnModuleInit {
|
|
|
224
732
|
`;
|
|
225
733
|
|
|
226
734
|
existing.forEach((r) => this.invalidateCache(r.slug));
|
|
735
|
+
// One bump and one message for the whole batch.
|
|
736
|
+
await this.publishInvalidation(existing.map((r) => r.slug));
|
|
227
737
|
|
|
228
738
|
return { count: ids.length };
|
|
229
739
|
}
|
|
@@ -270,27 +780,116 @@ export class AiInstructionService implements OnModuleInit {
|
|
|
270
780
|
);
|
|
271
781
|
|
|
272
782
|
this.invalidateCache(current.slug);
|
|
783
|
+
await this.publishInvalidation([current.slug]);
|
|
273
784
|
|
|
274
785
|
return this.getById(id);
|
|
275
786
|
}
|
|
276
787
|
|
|
277
788
|
// ── Private helpers ───────────────────────────────────────────────────────
|
|
278
789
|
|
|
790
|
+
/**
|
|
791
|
+
* Rejects content that would break at render time, and returns non-fatal
|
|
792
|
+
* warnings for the caller to surface.
|
|
793
|
+
*
|
|
794
|
+
* An unclosed block is a hard 422: it silently swallows the rest of the
|
|
795
|
+
* prompt. A removed required variable is also a 422, overridable with
|
|
796
|
+
* `allow_missing_variables` — sometimes a rewrite genuinely no longer needs a
|
|
797
|
+
* variable. An unknown placeholder is only a warning: it renders literally,
|
|
798
|
+
* which is ugly but harmless.
|
|
799
|
+
*/
|
|
800
|
+
private validateContent(
|
|
801
|
+
slug: string,
|
|
802
|
+
content: string,
|
|
803
|
+
options: {
|
|
804
|
+
fallbackSchema?: Record<string, any> | null;
|
|
805
|
+
allowMissingVariables?: boolean;
|
|
806
|
+
},
|
|
807
|
+
): string[] {
|
|
808
|
+
const definition = getAiPromptDefinition(slug);
|
|
809
|
+
const schema = definition?.variables ?? options.fallbackSchema ?? null;
|
|
810
|
+
const { missingRequired, unknownPlaceholders, unbalancedBlocks } = validateTemplate(
|
|
811
|
+
content,
|
|
812
|
+
schema,
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
if (unbalancedBlocks.length) {
|
|
816
|
+
throw new UnprocessableEntityException(
|
|
817
|
+
`Bloco de template não fechado: ${unbalancedBlocks
|
|
818
|
+
.map((name) => `{{#${name}}}`)
|
|
819
|
+
.join(', ')}. Isso descartaria o resto do prompt.`,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (missingRequired.length && !options.allowMissingVariables) {
|
|
824
|
+
throw new UnprocessableEntityException(
|
|
825
|
+
`Variáveis obrigatórias ausentes do conteúdo: ${missingRequired.join(', ')}. ` +
|
|
826
|
+
'Envie allow_missing_variables=true para salvar mesmo assim.',
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const warnings: string[] = [];
|
|
831
|
+
if (missingRequired.length) {
|
|
832
|
+
warnings.push(`missing-variables: ${missingRequired.join(', ')}`);
|
|
833
|
+
}
|
|
834
|
+
if (unknownPlaceholders.length) {
|
|
835
|
+
warnings.push(`unknown-variables: ${unknownPlaceholders.join(', ')}`);
|
|
836
|
+
}
|
|
837
|
+
warnings.push(...this.missingPhraseWarnings(definition, content));
|
|
838
|
+
|
|
839
|
+
return warnings;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Guard phrases are the runtime protection for security-sensitive prompts: if
|
|
844
|
+
* the stored content contains none of them, `AiPromptResolverService` discards
|
|
845
|
+
* it. Warn loudly here so the editor learns it before the first graded essay.
|
|
846
|
+
*/
|
|
847
|
+
private missingPhraseWarnings(
|
|
848
|
+
definition: AiPromptDefinition | undefined,
|
|
849
|
+
content: string,
|
|
850
|
+
): string[] {
|
|
851
|
+
const phrases = definition?.requiredPhrases;
|
|
852
|
+
if (!phrases?.length) return [];
|
|
853
|
+
|
|
854
|
+
const missing = phrases.filter((phrase) => !content.includes(phrase));
|
|
855
|
+
if (!missing.length) return [];
|
|
856
|
+
|
|
857
|
+
const hasAny = missing.length < phrases.length;
|
|
858
|
+
const warning = `security-phrases-removed: ${missing.join(', ')}`;
|
|
859
|
+
|
|
860
|
+
if (hasAny) {
|
|
861
|
+
this.logger.warn(`AI prompt "${definition!.slug}" lost safety phrases: ${missing.join(', ')}`);
|
|
862
|
+
return [warning];
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
this.logger.error(
|
|
866
|
+
`AI prompt "${definition!.slug}" lost ALL of its safety phrases; the resolver ` +
|
|
867
|
+
'will ignore the stored content and use the code fallback.',
|
|
868
|
+
);
|
|
869
|
+
return [`${warning} (todas — o prompt do banco será ignorado em runtime)`];
|
|
870
|
+
}
|
|
871
|
+
|
|
279
872
|
private async findById(id: number): Promise<AiInstructionRecord | null> {
|
|
280
|
-
const rows = await this.prisma.$queryRaw<AiInstructionRecord[]
|
|
281
|
-
SELECT
|
|
282
|
-
is_active, version, created_at, updated_at
|
|
873
|
+
const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
|
|
874
|
+
SELECT ${INSTRUCTION_COLUMNS}
|
|
283
875
|
FROM ai_instruction WHERE id = ${id} LIMIT 1
|
|
284
|
-
|
|
876
|
+
`);
|
|
877
|
+
return rows?.[0] || null;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
private async findBySlug(slug: string): Promise<AiInstructionRecord | null> {
|
|
881
|
+
const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
|
|
882
|
+
SELECT ${INSTRUCTION_COLUMNS}
|
|
883
|
+
FROM ai_instruction WHERE slug = ${slug} LIMIT 1
|
|
884
|
+
`);
|
|
285
885
|
return rows?.[0] || null;
|
|
286
886
|
}
|
|
287
887
|
|
|
288
888
|
private async findActiveBySlug(slug: string): Promise<AiInstructionRecord | null> {
|
|
289
|
-
const rows = await this.prisma.$queryRaw<AiInstructionRecord[]
|
|
290
|
-
SELECT
|
|
291
|
-
is_active, version, created_at, updated_at
|
|
889
|
+
const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
|
|
890
|
+
SELECT ${INSTRUCTION_COLUMNS}
|
|
292
891
|
FROM ai_instruction WHERE slug = ${slug} AND is_active = TRUE LIMIT 1
|
|
293
|
-
|
|
892
|
+
`);
|
|
294
893
|
return rows?.[0] || null;
|
|
295
894
|
}
|
|
296
895
|
|
|
@@ -299,27 +898,27 @@ export class AiInstructionService implements OnModuleInit {
|
|
|
299
898
|
moduleSlug?: string,
|
|
300
899
|
): Promise<AiInstructionRecord | null> {
|
|
301
900
|
if (moduleSlug) {
|
|
302
|
-
const rows = await this.prisma.$queryRaw<AiInstructionRecord[]
|
|
303
|
-
SELECT
|
|
304
|
-
is_active, version, created_at, updated_at
|
|
901
|
+
const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
|
|
902
|
+
SELECT ${INSTRUCTION_COLUMNS}
|
|
305
903
|
FROM ai_instruction
|
|
306
904
|
WHERE layer = CAST(${layer} AS ai_instruction_layer_enum)
|
|
307
905
|
AND module_slug = ${moduleSlug}
|
|
308
906
|
AND is_active = TRUE
|
|
907
|
+
ORDER BY id
|
|
309
908
|
LIMIT 1
|
|
310
|
-
|
|
909
|
+
`);
|
|
311
910
|
if (rows?.[0]) return rows[0];
|
|
312
911
|
}
|
|
313
912
|
|
|
314
|
-
const rows = await this.prisma.$queryRaw<AiInstructionRecord[]
|
|
315
|
-
SELECT
|
|
316
|
-
is_active, version, created_at, updated_at
|
|
913
|
+
const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
|
|
914
|
+
SELECT ${INSTRUCTION_COLUMNS}
|
|
317
915
|
FROM ai_instruction
|
|
318
916
|
WHERE layer = CAST(${layer} AS ai_instruction_layer_enum)
|
|
319
917
|
AND module_slug IS NULL
|
|
320
918
|
AND is_active = TRUE
|
|
919
|
+
ORDER BY id
|
|
321
920
|
LIMIT 1
|
|
322
|
-
|
|
921
|
+
`);
|
|
323
922
|
return rows?.[0] || null;
|
|
324
923
|
}
|
|
325
924
|
|