diff --git a/src/langbot/pkg/pipeline/pipelinemgr.py b/src/langbot/pkg/pipeline/pipelinemgr.py
index e59a5f15..c9c5def2 100644
--- a/src/langbot/pkg/pipeline/pipelinemgr.py
+++ b/src/langbot/pkg/pipeline/pipelinemgr.py
@@ -247,8 +247,8 @@ class RuntimePipeline:
await self._check_output(query, result)
if result.result_type == pipeline_entities.ResultType.INTERRUPT:
- self.ap.logger.warning(
- f'Stage {stage_container.inst_name} interrupted query {query.query_id}, message: {str(query.message_chain)[:100]}'
+ self.ap.logger.debug(
+ f'Stage {stage_container.inst_name} interrupted query {query.query_id}'
)
break
elif result.result_type == pipeline_entities.ResultType.CONTINUE:
@@ -263,8 +263,8 @@ class RuntimePipeline:
await self._check_output(query, sub_result)
if sub_result.result_type == pipeline_entities.ResultType.INTERRUPT:
- self.ap.logger.warning(
- f'Stage {stage_container.inst_name} interrupted query {query.query_id}, message: {str(query.message_chain)[:100]}'
+ self.ap.logger.debug(
+ f'Stage {stage_container.inst_name} interrupted query {query.query_id}'
)
break
elif sub_result.result_type == pipeline_entities.ResultType.CONTINUE:
@@ -327,8 +327,8 @@ class RuntimePipeline:
event_ctx = await self.ap.plugin_connector.emit_event(event_obj, bound_plugins)
if event_ctx.is_prevented_default():
- self.ap.logger.warning(
- f'MessageReceived event prevented default for query {query.query_id}, pipeline={pipeline_name}, message: {str(query.message_chain)[:100]}'
+ self.ap.logger.debug(
+ f'MessageReceived event prevented default for query {query.query_id}, pipeline={pipeline_name}'
)
return
diff --git a/src/langbot/pkg/pipeline/process/handlers/chat.py b/src/langbot/pkg/pipeline/process/handlers/chat.py
index 0f2e6a90..203a3612 100644
--- a/src/langbot/pkg/pipeline/process/handlers/chat.py
+++ b/src/langbot/pkg/pipeline/process/handlers/chat.py
@@ -61,8 +61,8 @@ class ChatMessageHandler(handler.MessageHandler):
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
else:
- self.ap.logger.warning(
- f'NormalMessageReceived event prevented default for query {query.query_id} without reply, message: {str(query.message_chain)[:100]}'
+ self.ap.logger.debug(
+ f'NormalMessageReceived event prevented default for query {query.query_id} without reply'
)
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
else:
diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx
index aa803d24..611a58bd 100644
--- a/web/src/app/home/bots/components/bot-form/BotForm.tsx
+++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx
@@ -13,13 +13,10 @@ import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
import { UUID } from 'uuidjs';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { httpClient } from '@/app/infra/http/HttpClient';
-import {
- Bot,
- PipelineRoutingRule,
- RoutingRuleOperator,
-} from '@/app/infra/entities/api';
+import { Bot } from '@/app/infra/entities/api';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
-import { ExternalLink, Plus, Trash2 } from 'lucide-react';
+import { ExternalLink } from 'lucide-react';
+import RoutingRulesEditor from './RoutingRulesEditor';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
@@ -492,303 +489,10 @@ export default function BotForm({
/>
{/* Pipeline Routing Rules */}
-
-
-
-
{t('bots.routingRules')}
-
- {t('bots.routingRulesDescription')}
-
-
-
-
-
- {(form.watch('pipeline_routing_rules') || []).map(
- (rule, index) => {
- // Determine which operators are available for the current type
- const operatorsForType: {
- value: RoutingRuleOperator;
- labelKey: string;
- }[] =
- rule.type === 'launcher_type'
- ? [
- { value: 'eq', labelKey: 'bots.operatorEq' },
- { value: 'neq', labelKey: 'bots.operatorNeq' },
- ]
- : rule.type === 'launcher_id'
- ? [
- { value: 'eq', labelKey: 'bots.operatorEq' },
- { value: 'neq', labelKey: 'bots.operatorNeq' },
- {
- value: 'contains',
- labelKey: 'bots.operatorContains',
- },
- {
- value: 'not_contains',
- labelKey: 'bots.operatorNotContains',
- },
- {
- value: 'regex',
- labelKey: 'bots.operatorRegex',
- },
- ]
- : [
- { value: 'eq', labelKey: 'bots.operatorEq' },
- { value: 'neq', labelKey: 'bots.operatorNeq' },
- {
- value: 'contains',
- labelKey: 'bots.operatorContains',
- },
- {
- value: 'not_contains',
- labelKey: 'bots.operatorNotContains',
- },
- {
- value: 'starts_with',
- labelKey: 'bots.operatorStartsWith',
- },
- {
- value: 'regex',
- labelKey: 'bots.operatorRegex',
- },
- ];
-
- return (
-
- {/* Field selector */}
-
-
- {/* Operator selector */}
-
-
- {/* Value input */}
- {rule.type === 'launcher_type' ? (
-
- ) : (
-
{
- const rules = [
- ...(form.getValues('pipeline_routing_rules') ||
- []),
- ];
- rules[index] = {
- ...rules[index],
- value: e.target.value,
- };
- form.setValue('pipeline_routing_rules', rules, {
- shouldDirty: true,
- });
- }}
- />
- )}
-
-
- →
-
-
- {/* Pipeline selector */}
-
-
-
-
- );
- },
- )}
-
+
)}
diff --git a/web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx b/web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx
new file mode 100644
index 00000000..7d2ada5d
--- /dev/null
+++ b/web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx
@@ -0,0 +1,258 @@
+'use client';
+
+import { useTranslation } from 'react-i18next';
+import { UseFormReturn } from 'react-hook-form';
+import {
+ PipelineRoutingRule,
+ RoutingRuleOperator,
+} from '@/app/infra/entities/api';
+import { Plus, Trash2 } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { FormLabel } from '@/components/ui/form';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+
+interface PipelineOption {
+ value: string;
+ label: string;
+ emoji?: string;
+}
+
+interface RoutingRulesEditorProps {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ form: UseFormReturn;
+ pipelineNameList: PipelineOption[];
+}
+
+const OPERATORS_BY_TYPE: Record<
+ PipelineRoutingRule['type'],
+ { value: RoutingRuleOperator; labelKey: string }[]
+> = {
+ launcher_type: [
+ { value: 'eq', labelKey: 'bots.operatorEq' },
+ { value: 'neq', labelKey: 'bots.operatorNeq' },
+ ],
+ launcher_id: [
+ { value: 'eq', labelKey: 'bots.operatorEq' },
+ { value: 'neq', labelKey: 'bots.operatorNeq' },
+ { value: 'contains', labelKey: 'bots.operatorContains' },
+ { value: 'not_contains', labelKey: 'bots.operatorNotContains' },
+ { value: 'regex', labelKey: 'bots.operatorRegex' },
+ ],
+ message_content: [
+ { value: 'eq', labelKey: 'bots.operatorEq' },
+ { value: 'neq', labelKey: 'bots.operatorNeq' },
+ { value: 'contains', labelKey: 'bots.operatorContains' },
+ { value: 'not_contains', labelKey: 'bots.operatorNotContains' },
+ { value: 'starts_with', labelKey: 'bots.operatorStartsWith' },
+ { value: 'regex', labelKey: 'bots.operatorRegex' },
+ ],
+};
+
+function getValuePlaceholder(
+ t: (key: string) => string,
+ rule: PipelineRoutingRule,
+): string {
+ if (rule.type === 'launcher_id') return t('bots.ruleValueLauncherIdPlaceholder');
+ if (rule.operator === 'regex') return t('bots.ruleValueRegexpPlaceholder');
+ return t('bots.ruleValueMessagePlaceholder');
+}
+
+export default function RoutingRulesEditor({
+ form,
+ pipelineNameList,
+}: RoutingRulesEditorProps) {
+ const { t } = useTranslation();
+
+ const rules: PipelineRoutingRule[] =
+ form.watch('pipeline_routing_rules') || [];
+
+ const updateRules = (newRules: PipelineRoutingRule[]) => {
+ form.setValue('pipeline_routing_rules', newRules, { shouldDirty: true });
+ };
+
+ const addRule = () => {
+ updateRules([
+ ...rules,
+ {
+ type: 'launcher_type',
+ operator: 'eq',
+ value: '',
+ pipeline_uuid: '',
+ },
+ ]);
+ };
+
+ const updateRule = (index: number, patch: Partial) => {
+ const updated = [...rules];
+ updated[index] = { ...updated[index], ...patch };
+ updateRules(updated);
+ };
+
+ const removeRule = (index: number) => {
+ const updated = [...rules];
+ updated.splice(index, 1);
+ updateRules(updated);
+ };
+
+ return (
+
+
+
+
{t('bots.routingRules')}
+
+ {t('bots.routingRulesDescription')}
+
+
+
+
+
+ {rules.map((rule, index) => {
+ const operatorsForType = OPERATORS_BY_TYPE[rule.type] || OPERATORS_BY_TYPE.message_content;
+
+ return (
+
+ {/* Field selector */}
+
+
+ {/* Operator selector */}
+
+
+ {/* Value input */}
+ {rule.type === 'launcher_type' ? (
+
+ ) : (
+
updateRule(index, { value: e.target.value })}
+ />
+ )}
+
+
→
+
+ {/* Pipeline selector */}
+
+
+
+
+ );
+ })}
+
+ );
+}