diff --git a/api/src/helper/helper.service.ts b/api/src/helper/helper.service.ts index db0a8800..336815bb 100644 --- a/api/src/helper/helper.service.ts +++ b/api/src/helper/helper.service.ts @@ -161,8 +161,13 @@ export class HelperService { } const settings = await this.settingService.getSettings(); + const defaultHelperKey = `default_${type}_helper`; + if (!(defaultHelperKey in settings.chatbot_settings)) { + throw new Error(`Default ${type.toUpperCase()} helper setting not found`); + } + const defaultHelperName = settings.chatbot_settings[ - `default_${type}_helper` as any + defaultHelperKey ] as HelperName; const defaultHelper = this.get(type, defaultHelperName); diff --git a/api/src/helper/lib/base-flow-escape-helper.ts b/api/src/helper/lib/base-flow-escape-helper.ts new file mode 100644 index 00000000..5c93aea9 --- /dev/null +++ b/api/src/helper/lib/base-flow-escape-helper.ts @@ -0,0 +1,52 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import EventWrapper from '@/channel/lib/EventWrapper'; +import { BlockStub } from '@/chat/schemas/block.schema'; +import { LoggerService } from '@/logger/logger.service'; +import { SettingService } from '@/setting/services/setting.service'; + +import { HelperService } from '../helper.service'; +import { FlowEscape, HelperName, HelperType } from '../types'; + +import BaseHelper from './base-helper'; + +export default abstract class BaseFlowEscapeHelper< + N extends HelperName = HelperName, +> extends BaseHelper { + protected readonly type: HelperType = HelperType.FLOW_ESCAPE; + + constructor( + name: N, + settingService: SettingService, + helperService: HelperService, + logger: LoggerService, + ) { + super(name, settingService, helperService, logger); + } + + /** + * Checks if the helper can handle the flow escape for the given block message. + * + * @param _blockMessage - The block message to check. + * @returns - Whether the helper can handle the flow escape for the given block message. + */ + abstract canHandleFlowEscape(_blockMessage: T): boolean; + + /** + * Adjudicates the flow escape event. + * + * @param _event - The event wrapper containing the event data. + * @param _block - The block associated with the event. + * @returns - A promise that resolves to a FlowEscape.AdjudicationResult. + */ + abstract adjudicate( + _event: EventWrapper, + _block: T, + ): Promise; +} diff --git a/api/src/helper/types.ts b/api/src/helper/types.ts index 658c95c3..48652e50 100644 --- a/api/src/helper/types.ts +++ b/api/src/helper/types.ts @@ -9,6 +9,7 @@ import { ExtensionSetting } from '@/setting/schemas/types'; import { HyphenToUnderscore } from '@/utils/types/extension'; +import BaseFlowEscapeHelper from './lib/base-flow-escape-helper'; import BaseHelper from './lib/base-helper'; import BaseLlmHelper from './lib/base-llm-helper'; import BaseNlpHelper from './lib/base-nlp-helper'; @@ -93,9 +94,31 @@ export namespace LLM { } } +export namespace FlowEscape { + export enum Action { + REPROMPT = 're_prompt', + COERCE = 'coerce_to_option', + NEW_CTX = 'new_context', + } + + export type AdjudicationResult = + | { + action: Action.COERCE; + coercedOption: string; + } + | { + action: Action.REPROMPT; + repromptMessage?: string; + } + | { + action: Action.NEW_CTX; + }; +} + export enum HelperType { NLU = 'nlu', LLM = 'llm', + FLOW_ESCAPE = 'flow_escape', STORAGE = 'storage', UTIL = 'util', } @@ -105,6 +128,7 @@ export type HelperName = `${string}-helper`; interface HelperTypeMap { [HelperType.NLU]: BaseNlpHelper; [HelperType.LLM]: BaseLlmHelper; + [HelperType.FLOW_ESCAPE]: BaseFlowEscapeHelper; [HelperType.STORAGE]: BaseStorageHelper; [HelperType.UTIL]: BaseHelper; } diff --git a/api/src/setting/seeds/setting.seed-model.ts b/api/src/setting/seeds/setting.seed-model.ts index c65d9be3..f26b82aa 100644 --- a/api/src/setting/seeds/setting.seed-model.ts +++ b/api/src/setting/seeds/setting.seed-model.ts @@ -50,6 +50,20 @@ export const DEFAULT_SETTINGS = [ }, weight: 3, }, + { + group: 'chatbot_settings', + label: 'default_flow_escape_helper', + value: '', + type: SettingType.select, + config: { + multiple: false, + allowCreate: false, + entity: 'Helper', + idKey: 'name', + labelKey: 'name', + }, + weight: 3, + }, { group: 'chatbot_settings', label: 'default_storage_helper', diff --git a/frontend/public/locales/en/chatbot_settings.json b/frontend/public/locales/en/chatbot_settings.json index bde69e7b..b5acf678 100644 --- a/frontend/public/locales/en/chatbot_settings.json +++ b/frontend/public/locales/en/chatbot_settings.json @@ -9,7 +9,8 @@ "default_nlu_helper": "Default NLU Helper", "default_llm_helper": "Default LLM Helper", "default_storage_helper": "Default Storage Helper", - "default_nlu_penalty_factor": "NLU Penalty Factor" + "default_nlu_penalty_factor": "NLU Penalty Factor", + "default_flow_escape_helper": "Default Flow Escape Helper" }, "help": { "global_fallback": "Global fallback allows you to send custom messages when user entry does not match any of the block messages.", @@ -17,6 +18,7 @@ "default_nlu_helper": "The NLU helper is responsible for processing and understanding user inputs, including tasks like intent prediction, language detection, and entity recognition.", "default_llm_helper": "The LLM helper leverages advanced generative AI to perform tasks such as text generation, chat completion, and complex query responses.", "default_storage_helper": "The storage helper defines where to store attachment files. By default, the default local storage helper stores them locally, but you can choose to use Minio or any other storage solution.", - "default_nlu_penalty_factor": "The NLU penalty factor is a coefficient (between 0 and 1) applied exclusively to NLU-based entity matching. It reduces the score contribution of patterns that match broadly (e.g. using wildcard values like Any) rather than specific entity values. This helps the engine prioritize blocks triggered by more precise NLU matches, without affecting other matching strategies such as text, regex, or interaction triggers." + "default_nlu_penalty_factor": "The NLU penalty factor is a coefficient (between 0 and 1) applied exclusively to NLU-based entity matching. It reduces the score contribution of patterns that match broadly (e.g. using wildcard values like Any) rather than specific entity values. This helps the engine prioritize blocks triggered by more precise NLU matches, without affecting other matching strategies such as text, regex, or interaction triggers.", + "default_flow_escape_helper": "The Flow Escape helper is used when the user’s message does not match any option in a flow. It assists the chatbot in deciding whether to re-prompt, provide an explanation, or end the conversation." } } diff --git a/frontend/public/locales/fr/chatbot_settings.json b/frontend/public/locales/fr/chatbot_settings.json index 9d44b07c..f370ac4d 100644 --- a/frontend/public/locales/fr/chatbot_settings.json +++ b/frontend/public/locales/fr/chatbot_settings.json @@ -9,7 +9,8 @@ "default_nlu_helper": "Utilitaire NLU par défaut", "default_llm_helper": "Utilitaire LLM par défaut", "default_storage_helper": "Utilitaire de stockage par défaut", - "default_nlu_penalty_factor": "Facteur de pénalité NLU" + "default_nlu_penalty_factor": "Facteur de pénalité NLU", + "default_flow_escape_helper": "Utilitaire de secours de flux par défaut" }, "help": { "global_fallback": "La réponse de secours globale vous permet d'envoyer des messages personnalisés lorsque l'entrée de l'utilisateur ne correspond à aucun des messages des blocs.", @@ -17,6 +18,7 @@ "default_nlu_helper": "Utilitaire du traitement et de la compréhension des entrées des utilisateurs, incluant des tâches telles que la prédiction d'intention, la détection de langue et la reconnaissance d'entités.", "default_llm_helper": "Utilitaire responsable de l'intelligence artificielle générative avancée pour effectuer des tâches telles que la génération de texte, la complétion de chat et les réponses à des requêtes complexes.", "default_storage_helper": "Utilitaire de stockage définit l'emplacement où stocker les fichiers joints. Par défaut, le stockage local les conserve localement, mais vous pouvez choisir d'utiliser Minio ou toute autre solution de stockage.", - "default_nlu_penalty_factor": "Le facteur de pénalité NLU est un coefficient (entre 0 et 1) appliqué exclusivement aux correspondances d'entités basées sur NLU. Il réduit la contribution au score des motifs qui correspondent de manière générale (par exemple, en utilisant des valeurs génériques comme Any) plutôt que des valeurs d'entité spécifiques. Cela permet au chatbot de donner la priorité aux blocs déclenchés par des correspondances NLU plus précises, sans affecter d'autres stratégies de correspondance telles que le texte, les expressions regex ou les déclencheurs d'interaction." + "default_nlu_penalty_factor": "Le facteur de pénalité NLU est un coefficient (entre 0 et 1) appliqué exclusivement aux correspondances d'entités basées sur NLU. Il réduit la contribution au score des motifs qui correspondent de manière générale (par exemple, en utilisant des valeurs génériques comme Any) plutôt que des valeurs d'entité spécifiques. Cela permet au chatbot de donner la priorité aux blocs déclenchés par des correspondances NLU plus précises, sans affecter d'autres stratégies de correspondance telles que le texte, les expressions regex ou les déclencheurs d'interaction.", + "default_flow_escape_helper": "L’utilitaire de secours de flux est utilisé lorsque le message de l’utilisateur ne correspond à aucune option dans un scénario. Il aide le chatbot à décider s’il faut reformuler la question, fournir une explication ou mettre fin à la conversation." } } diff --git a/frontend/src/components/settings/SettingInput.tsx b/frontend/src/components/settings/SettingInput.tsx index 51fc181f..bd8c854d 100644 --- a/frontend/src/components/settings/SettingInput.tsx +++ b/frontend/src/components/settings/SettingInput.tsx @@ -20,8 +20,8 @@ import { PasswordInput } from "@/app-components/inputs/PasswordInput"; import { useTranslate } from "@/hooks/useTranslate"; import { EntityType, Format } from "@/services/types"; import { AttachmentResourceRef } from "@/types/attachment.types"; +import { IEntityMapTypes } from "@/types/base.types"; import { IBlock } from "@/types/block.types"; -import { IHelper } from "@/types/helper.types"; import { ISetting, SettingType } from "@/types/setting.types"; import { MIME_TYPES } from "@/utils/attachment"; @@ -32,6 +32,12 @@ interface RenderSettingInputProps { isDisabled?: (setting: ISetting) => boolean; } +const DEFAULT_HELPER_ENTITIES: Record = { + ["default_nlu_helper"]: EntityType.NLU_HELPER, + ["default_llm_helper"]: EntityType.LLM_HELPER, + ["default_flow_escape_helper"]: EntityType.FLOW_ESCAPE_HELPER, + ["default_storage_helper"]: EntityType.STORAGE_HELPER, +}; const SettingInput: React.FC = ({ setting, field, @@ -125,54 +131,25 @@ const SettingInput: React.FC = ({ {...rest} /> ); - } else if (setting.label === "default_nlu_helper") { + } else if ( + setting.label.startsWith("default_") && + setting.label.endsWith("_helper") + ) { const { onChange, ...rest } = field; return ( - + searchFields={["name"]} - entity={EntityType.NLU_HELPER} + entity={DEFAULT_HELPER_ENTITIES[setting.label]} format={Format.BASIC} - labelKey="name" - idKey="name" - label={t("label.default_nlu_helper")} - helperText={t("help.default_nlu_helper")} - multiple={false} - onChange={(_e, selected, ..._) => onChange(selected?.name)} - {...rest} - /> - ); - } else if (setting.label === "default_llm_helper") { - const { onChange, ...rest } = field; - - return ( - - searchFields={["name"]} - entity={EntityType.LLM_HELPER} - format={Format.BASIC} - labelKey="name" - idKey="name" - label={t("label.default_llm_helper")} - helperText={t("help.default_llm_helper")} - multiple={false} - onChange={(_e, selected, ..._) => onChange(selected?.name)} - {...rest} - /> - ); - } else if (setting.label === "default_storage_helper") { - const { onChange, ...rest } = field; - - return ( - - searchFields={["name"]} - entity={EntityType.STORAGE_HELPER} - format={Format.BASIC} - labelKey="name" - idKey="name" - label={t("label.default_storage_helper")} - helperText={t("help.default_storage_helper")} - multiple={false} - onChange={(_e, selected, ..._) => onChange(selected?.name)} + labelKey={setting.config?.labelKey || "name"} + idKey={setting.config?.idKey || "name"} + label={label} + helperText={helperText} + multiple={!!setting.config?.multiple} + onChange={(_e, selected, ..._) => + onChange(selected?.[setting.config?.idKey || "name"]) + } {...rest} /> ); diff --git a/frontend/src/services/api.class.ts b/frontend/src/services/api.class.ts index 509185ed..6e77e07e 100644 --- a/frontend/src/services/api.class.ts +++ b/frontend/src/services/api.class.ts @@ -74,6 +74,7 @@ export const ROUTES = { [EntityType.HELPER]: "/helper", [EntityType.NLU_HELPER]: "/helper/nlu", [EntityType.LLM_HELPER]: "/helper/llm", + [EntityType.FLOW_ESCAPE_HELPER]: "helper/flow_escape", [EntityType.STORAGE_HELPER]: "/helper/storage", } as const; diff --git a/frontend/src/services/entities.ts b/frontend/src/services/entities.ts index 7984adff..cfdbd2aa 100644 --- a/frontend/src/services/entities.ts +++ b/frontend/src/services/entities.ts @@ -304,6 +304,14 @@ export const LlmHelperEntity = new schema.Entity( }, ); +export const FlowEscapeHelperEntity = new schema.Entity( + EntityType.FLOW_ESCAPE_HELPER, + undefined, + { + idAttribute: ({ name }) => name, + }, +); + export const StorageHelperEntity = new schema.Entity( EntityType.STORAGE_HELPER, undefined, @@ -341,5 +349,6 @@ export const ENTITY_MAP = { [EntityType.HELPER]: HelperEntity, [EntityType.NLU_HELPER]: NluHelperEntity, [EntityType.LLM_HELPER]: LlmHelperEntity, + [EntityType.FLOW_ESCAPE_HELPER]: FlowEscapeHelperEntity, [EntityType.STORAGE_HELPER]: StorageHelperEntity, } as const; diff --git a/frontend/src/services/types.ts b/frontend/src/services/types.ts index af357e02..c3e6ed5e 100644 --- a/frontend/src/services/types.ts +++ b/frontend/src/services/types.ts @@ -38,6 +38,7 @@ export enum EntityType { HELPER = "Helper", NLU_HELPER = "NluHelper", LLM_HELPER = "LlmHelper", + FLOW_ESCAPE_HELPER = "FlowEscapeHelper", STORAGE_HELPER = "StorageHelper", } diff --git a/frontend/src/types/base.types.ts b/frontend/src/types/base.types.ts index 510189a3..895f3d61 100644 --- a/frontend/src/types/base.types.ts +++ b/frontend/src/types/base.types.ts @@ -116,6 +116,7 @@ export const POPULATE_BY_TYPE = { [EntityType.HELPER]: [], [EntityType.NLU_HELPER]: [], [EntityType.LLM_HELPER]: [], + [EntityType.FLOW_ESCAPE_HELPER]: [], [EntityType.STORAGE_HELPER]: [], } as const; @@ -208,6 +209,7 @@ export interface IEntityMapTypes { [EntityType.HELPER]: IEntityTypes; [EntityType.NLU_HELPER]: IEntityTypes; [EntityType.LLM_HELPER]: IEntityTypes; + [EntityType.FLOW_ESCAPE_HELPER]: IEntityTypes; [EntityType.STORAGE_HELPER]: IEntityTypes; }