From fc40d3c949537cac863517eb9b8451028d377726 Mon Sep 17 00:00:00 2001 From: Typer_Body Date: Thu, 7 May 2026 23:33:54 +0800 Subject: [PATCH] ruff --- src/langbot/pkg/workflow/nodes/condition.py | 27 +- .../pkg/workflow/nodes/database_query.py | 1 - .../pkg/workflow/nodes/parameter_extractor.py | 1 - .../pkg/workflow/nodes/question_classifier.py | 1 - .../pkg/workflow/nodes/send_message.py | 3 - .../components/home-sidebar/HomeSidebar.tsx | 7 +- .../UnifiedBindingSelector.tsx | 74 +-- .../home/workflows/WorkflowDetailContent.tsx | 288 +++++++----- .../workflow-debugger/WorkflowDebugger.tsx | 425 ++++++++++++------ .../workflow-editor/NodePalette.tsx | 314 +++++++------ .../workflow-editor/PropertyPanel.tsx | 227 +++++++--- .../workflow-editor/WorkflowNodeComponent.tsx | 229 ++++++---- .../components/workflow-editor/index.ts | 5 +- .../node-configs/ai-configs.ts | 61 ++- .../node-configs/control-configs.ts | 128 ++++-- .../workflow-editor/node-configs/index.ts | 33 +- .../node-configs/integration-configs.ts | 317 +++++++++++-- .../node-configs/process-configs.ts | 87 +++- .../node-configs/trigger-configs.ts | 135 ++++-- .../workflow-editor/node-configs/types.ts | 37 +- .../workflow-editor/workflow-constants.ts | 240 +++++++--- .../workflow-editor/workflow-node-metadata.ts | 54 ++- .../workflow-form/WorkflowFormComponent.tsx | 37 +- .../home/workflows/store/useWorkflowStore.ts | 265 ++++++----- web/src/app/infra/entities/workflow/index.ts | 14 +- web/src/app/infra/http/BackendClient.ts | 10 +- web/src/i18n/I18nProvider.tsx | 4 +- web/src/i18n/locales/en-US.ts | 57 ++- web/src/i18n/locales/es-ES.ts | 48 +- web/src/i18n/locales/ja-JP.ts | 51 ++- web/src/i18n/locales/ru-RU.ts | 48 +- web/src/i18n/locales/th-TH.ts | 48 +- web/src/i18n/locales/vi-VN.ts | 48 +- web/src/i18n/locales/zh-Hant.ts | 48 +- 34 files changed, 2297 insertions(+), 1075 deletions(-) diff --git a/src/langbot/pkg/workflow/nodes/condition.py b/src/langbot/pkg/workflow/nodes/condition.py index e7e63aa1..b870d011 100644 --- a/src/langbot/pkg/workflow/nodes/condition.py +++ b/src/langbot/pkg/workflow/nodes/condition.py @@ -74,15 +74,24 @@ class ConditionNode(WorkflowNode): left_num = float(left) right_num = float(right) - if operator == "==": return left_num == right_num - elif operator == "!=": return left_num != right_num - elif operator == ">": return left_num > right_num - elif operator == "<": return left_num < right_num - elif operator == ">=": return left_num >= right_num - elif operator == "<=": return left_num <= right_num + if operator == "==": + return left_num == right_num + elif operator == "!=": + return left_num != right_num + elif operator == ">": + return left_num > right_num + elif operator == "<": + return left_num < right_num + elif operator == ">=": + return left_num >= right_num + elif operator == "<=": + return left_num <= right_num except ValueError: - if operator == "==": return left == right - elif operator == "!=": return left != right - elif operator in (">", "<", ">=", "<="): return False + if operator == "==": + return left == right + elif operator == "!=": + return left != right + elif operator in (">", "<", ">=", "<="): + return False return False diff --git a/src/langbot/pkg/workflow/nodes/database_query.py b/src/langbot/pkg/workflow/nodes/database_query.py index 72d9bb9f..4a3a8e1c 100644 --- a/src/langbot/pkg/workflow/nodes/database_query.py +++ b/src/langbot/pkg/workflow/nodes/database_query.py @@ -31,7 +31,6 @@ class DatabaseQueryNode(WorkflowNode): async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]: connection_type = self.get_config("connection_type", "postgresql") - connection_string = self.get_config("connection_string", "") query = self.get_config("query", "") query_type = self.get_config("query_type", "select") timeout = self.get_config("timeout", 30) diff --git a/src/langbot/pkg/workflow/nodes/parameter_extractor.py b/src/langbot/pkg/workflow/nodes/parameter_extractor.py index 68363182..8d551da3 100644 --- a/src/langbot/pkg/workflow/nodes/parameter_extractor.py +++ b/src/langbot/pkg/workflow/nodes/parameter_extractor.py @@ -30,7 +30,6 @@ class ParameterExtractorNode(WorkflowNode): config_schema: ClassVar[list[NodeConfig]] = [] async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]: - text = inputs.get("text", "") param_defs = self.get_config("parameters", []) extracted = {} diff --git a/src/langbot/pkg/workflow/nodes/question_classifier.py b/src/langbot/pkg/workflow/nodes/question_classifier.py index 6f6ecb3c..1346644f 100644 --- a/src/langbot/pkg/workflow/nodes/question_classifier.py +++ b/src/langbot/pkg/workflow/nodes/question_classifier.py @@ -30,7 +30,6 @@ class QuestionClassifierNode(WorkflowNode): config_schema: ClassVar[list[NodeConfig]] = [] async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]: - question = inputs.get("question", "") categories = self.get_config("categories", []) if categories: diff --git a/src/langbot/pkg/workflow/nodes/send_message.py b/src/langbot/pkg/workflow/nodes/send_message.py index d85c8a7c..460fad71 100644 --- a/src/langbot/pkg/workflow/nodes/send_message.py +++ b/src/langbot/pkg/workflow/nodes/send_message.py @@ -30,7 +30,4 @@ class SendMessageNode(WorkflowNode): config_schema: ClassVar[list[NodeConfig]] = [] async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]: - message = inputs.get("message", "") - target = inputs.get("target") or self.get_config("target_id", "") - return {"status": "sent", "message_id": f"msg_{context.execution_id}"} diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index bb346a36..0585baa3 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -161,7 +161,12 @@ function isEntityCategory(id: string): id is EntityCategoryId { // Map sidebar config IDs to SidebarDataContext keys const ENTITY_KEY_MAP: Record< EntityCategoryId, - 'bots' | 'pipelines' | 'workflows' | 'knowledgeBases' | 'plugins' | 'mcpServers' + | 'bots' + | 'pipelines' + | 'workflows' + | 'knowledgeBases' + | 'plugins' + | 'mcpServers' > = { bots: 'bots', pipelines: 'pipelines', diff --git a/web/src/app/home/components/unified-binding-selector/UnifiedBindingSelector.tsx b/web/src/app/home/components/unified-binding-selector/UnifiedBindingSelector.tsx index b4ad12f5..0dce1513 100644 --- a/web/src/app/home/components/unified-binding-selector/UnifiedBindingSelector.tsx +++ b/web/src/app/home/components/unified-binding-selector/UnifiedBindingSelector.tsx @@ -1,6 +1,9 @@ import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useSidebarData, SidebarEntityItem } from '../home-sidebar/SidebarDataContext'; +import { + useSidebarData, + SidebarEntityItem, +} from '../home-sidebar/SidebarDataContext'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; import { @@ -43,7 +46,8 @@ export default function UnifiedBindingSelector({ className, }: UnifiedBindingSelectorProps) { const { t } = useTranslation(); - const { pipelines, workflows, refreshPipelines, refreshWorkflows } = useSidebarData(); + const { pipelines, workflows, refreshPipelines, refreshWorkflows } = + useSidebarData(); const [open, setOpen] = useState(false); useEffect(() => { @@ -137,7 +141,9 @@ export default function UnifiedBindingSelector({ {/* Entity selection */}
@@ -171,7 +177,9 @@ export default function UnifiedBindingSelector({
@@ -186,37 +194,37 @@ export default function UnifiedBindingSelector({ )) ) + ) : workflows.length === 0 ? ( +
+ {t('bots.noWorkflowsFound')} +
) : ( - workflows.length === 0 ? ( -
- {t('bots.noWorkflowsFound')} -
- ) : ( - workflows.map((workflow) => ( - + )) )}
diff --git a/web/src/app/home/workflows/WorkflowDetailContent.tsx b/web/src/app/home/workflows/WorkflowDetailContent.tsx index 1cf0ce27..35830015 100644 --- a/web/src/app/home/workflows/WorkflowDetailContent.tsx +++ b/web/src/app/home/workflows/WorkflowDetailContent.tsx @@ -2,7 +2,13 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; @@ -12,7 +18,15 @@ import WorkflowExecutionsTab from './components/workflow-executions/WorkflowExec import WorkflowDebugDialog from './components/workflow-debug-dialog/WorkflowDebugDialog'; import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; import { useTranslation } from 'react-i18next'; -import { Settings, Play, BarChart3, GitBranch, Download, Upload, Bug } from 'lucide-react'; +import { + Settings, + Play, + BarChart3, + GitBranch, + Download, + Upload, + Bug, +} from 'lucide-react'; import { backendClient } from '@/app/infra/http'; import { Workflow } from '@/app/infra/entities/api'; import { useWorkflowStore } from './store/useWorkflowStore'; @@ -24,7 +38,7 @@ export default function WorkflowDetailContent({ id }: { id: string }) { const navigate = useNavigate(); const { t } = useTranslation(); const { refreshWorkflows, workflows, setDetailEntityName } = useSidebarData(); - + const { currentWorkflow, setCurrentWorkflow, @@ -43,7 +57,11 @@ export default function WorkflowDetailContent({ id }: { id: string }) { const [activeTab, setActiveTab] = useState('editor'); const [workflow, setWorkflow] = useState(null); const [createStep, setCreateStep] = useState<'basic' | 'editor'>('basic'); - const [basicInfo, setBasicInfo] = useState<{ name: string; description: string; emoji: string }>({ + const [basicInfo, setBasicInfo] = useState<{ + name: string; + description: string; + emoji: string; + }>({ name: '', description: '', emoji: '🔄', @@ -65,11 +83,14 @@ export default function WorkflowDetailContent({ id }: { id: string }) { // Load node types useEffect(() => { if (nodeTypes.length === 0) { - backendClient.getWorkflowNodeTypes().then((resp) => { - setNodeTypes(resp.node_types, resp.categories); - }).catch((err) => { - console.error('Failed to load node types:', err); - }); + backendClient + .getWorkflowNodeTypes() + .then((resp) => { + setNodeTypes(resp.node_types, resp.categories); + }) + .catch((err) => { + console.error('Failed to load node types:', err); + }); } }, [nodeTypes.length, setNodeTypes]); @@ -82,16 +103,23 @@ export default function WorkflowDetailContent({ id }: { id: string }) { } setLoading(true); - backendClient.getWorkflow(id).then((resp) => { - setWorkflow(resp.workflow); - setCurrentWorkflow(resp.workflow); - fromWorkflowDefinition(resp.workflow.nodes || [], resp.workflow.edges || []); - }).catch((err) => { - console.error('Failed to load workflow:', err); - toast.error(t('workflows.loadError')); - }).finally(() => { - setLoading(false); - }); + backendClient + .getWorkflow(id) + .then((resp) => { + setWorkflow(resp.workflow); + setCurrentWorkflow(resp.workflow); + fromWorkflowDefinition( + resp.workflow.nodes || [], + resp.workflow.edges || [], + ); + }) + .catch((err) => { + console.error('Failed to load workflow:', err); + toast.error(t('workflows.loadError')); + }) + .finally(() => { + setLoading(false); + }); return () => { reset(); @@ -105,7 +133,7 @@ export default function WorkflowDetailContent({ id }: { id: string }) { setSaving(true); try { const { nodes, edges } = toWorkflowDefinition(); - + if (isCreateMode) { const resp = await backendClient.createWorkflow({ name: basicInfo.name || t('workflows.newWorkflow'), @@ -138,12 +166,22 @@ export default function WorkflowDetailContent({ id }: { id: string }) { } finally { setSaving(false); } - }, [id, isCreateMode, workflow, isSaving, toWorkflowDefinition, refreshWorkflows, navigate, t, basicInfo]); + }, [ + id, + isCreateMode, + workflow, + isSaving, + toWorkflowDefinition, + refreshWorkflows, + navigate, + t, + basicInfo, + ]); // Export workflow handler const handleExport = useCallback(() => { const { nodes, edges } = toWorkflowDefinition(); - + const exportData = { name: workflow?.name || t('workflows.newWorkflow'), description: workflow?.description || '', @@ -155,8 +193,10 @@ export default function WorkflowDetailContent({ id }: { id: string }) { version: '1.0', exportedAt: new Date().toISOString(), }; - - const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { + type: 'application/json', + }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -165,84 +205,103 @@ export default function WorkflowDetailContent({ id }: { id: string }) { a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - + toast.success(t('workflows.exportSuccess')); }, [workflow, toWorkflowDefinition, t]); // Import workflow handler - const handleImport = useCallback((file: File) => { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const importData = JSON.parse(e.target?.result as string); - - // Validate imported data structure - if (!importData.nodes || !Array.isArray(importData.nodes)) { - throw new Error('Invalid workflow file: missing nodes'); - } - if (!importData.edges || !Array.isArray(importData.edges)) { - throw new Error('Invalid workflow file: missing edges'); - } - - // Validate each node has required fields - const nodeIds = new Set(); - for (const node of importData.nodes) { - if (!node.id || !node.type) { - throw new Error(`Invalid node: missing id or type`); + const handleImport = useCallback( + (file: File) => { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const importData = JSON.parse(e.target?.result as string); + + // Validate imported data structure + if (!importData.nodes || !Array.isArray(importData.nodes)) { + throw new Error('Invalid workflow file: missing nodes'); } - if (!node.position || typeof node.position.x !== 'number' || typeof node.position.y !== 'number') { - throw new Error(`Invalid node "${node.id}": missing or invalid position`); + if (!importData.edges || !Array.isArray(importData.edges)) { + throw new Error('Invalid workflow file: missing edges'); } - nodeIds.add(node.id); + + // Validate each node has required fields + const nodeIds = new Set(); + for (const node of importData.nodes) { + if (!node.id || !node.type) { + throw new Error(`Invalid node: missing id or type`); + } + if ( + !node.position || + typeof node.position.x !== 'number' || + typeof node.position.y !== 'number' + ) { + throw new Error( + `Invalid node "${node.id}": missing or invalid position`, + ); + } + nodeIds.add(node.id); + } + + // Validate each edge has required fields and references existing nodes + for (const edge of importData.edges) { + if (!edge.id || !edge.source || !edge.target) { + throw new Error(`Invalid edge: missing id, source, or target`); + } + if (!nodeIds.has(edge.source)) { + throw new Error( + `Edge "${edge.id}" references unknown source node "${edge.source}"`, + ); + } + if (!nodeIds.has(edge.target)) { + throw new Error( + `Edge "${edge.id}" references unknown target node "${edge.target}"`, + ); + } + } + + // Load nodes and edges into the store + fromWorkflowDefinition(importData.nodes, importData.edges); + + // Update workflow metadata if available + if ( + workflow && + (importData.name || importData.description || importData.emoji) + ) { + setWorkflow({ + ...workflow, + name: importData.name || workflow.name, + description: importData.description || workflow.description, + emoji: importData.emoji || workflow.emoji, + variables: importData.variables || workflow.variables, + settings: importData.settings || workflow.settings, + }); + } + + setDirty(true); + toast.success(t('workflows.importSuccess')); + } catch (error) { + console.error('Failed to import workflow:', error); + toast.error(t('workflows.importError')); } - - // Validate each edge has required fields and references existing nodes - for (const edge of importData.edges) { - if (!edge.id || !edge.source || !edge.target) { - throw new Error(`Invalid edge: missing id, source, or target`); - } - if (!nodeIds.has(edge.source)) { - throw new Error(`Edge "${edge.id}" references unknown source node "${edge.source}"`); - } - if (!nodeIds.has(edge.target)) { - throw new Error(`Edge "${edge.id}" references unknown target node "${edge.target}"`); - } - } - - // Load nodes and edges into the store - fromWorkflowDefinition(importData.nodes, importData.edges); - - // Update workflow metadata if available - if (workflow && (importData.name || importData.description || importData.emoji)) { - setWorkflow({ - ...workflow, - name: importData.name || workflow.name, - description: importData.description || workflow.description, - emoji: importData.emoji || workflow.emoji, - variables: importData.variables || workflow.variables, - settings: importData.settings || workflow.settings, - }); - } - - setDirty(true); - toast.success(t('workflows.importSuccess')); - } catch (error) { - console.error('Failed to import workflow:', error); - toast.error(t('workflows.importError')); - } - }; - reader.readAsText(file); - }, [workflow, fromWorkflowDefinition, setDirty, t]); + }; + reader.readAsText(file); + }, + [workflow, fromWorkflowDefinition, setDirty, t], + ); // Handle file input change - const handleFileChange = useCallback((e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - handleImport(file); - // Reset file input - e.target.value = ''; - } - }, [handleImport]); + const handleFileChange = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + handleImport(file); + // Reset file input + e.target.value = ''; + } + }, + [handleImport], + ); // Publish handler const handlePublish = useCallback(async () => { @@ -289,7 +348,10 @@ export default function WorkflowDetailContent({ id }: { id: string }) { style={{ display: 'none' }} ref={fileInputRef} /> - @@ -307,17 +369,26 @@ export default function WorkflowDetailContent({ id }: { id: string }) { {t('workflows.basicInfo')} - {t('workflows.basicInfoDesc')} + + {t('workflows.basicInfoDesc')} +
- setBasicInfo({ ...basicInfo, emoji })} /> + + setBasicInfo({ ...basicInfo, emoji }) + } + /> setBasicInfo({ ...basicInfo, name: e.target.value })} + onChange={(e) => + setBasicInfo({ ...basicInfo, name: e.target.value }) + } placeholder={t('workflows.namePlaceholder')} className="flex-1" /> @@ -325,11 +396,18 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
- +