From 008e55a65eb538a5729017f3f1c069a3f9c457a3 Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 16 May 2025 09:53:00 +0800 Subject: [PATCH] Feat: Add the JS code (or other) executor component to Agent. #4977 (#7677) ### What problem does this PR solve? Feat: Add the JS code (or other) executor component to Agent. #4977 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/constants/agent.ts | 26 +++++++ web/src/interfaces/database/flow.ts | 7 ++ web/src/locales/en.ts | 3 + web/src/locales/ja.ts | 3 +- web/src/locales/zh-traditional.ts | 3 + web/src/locales/zh.ts | 4 ++ web/src/pages/flow/constant.tsx | 23 +++++++ web/src/pages/flow/flow-drawer/index.tsx | 2 + .../form/code-form/dynamic-input-variable.tsx | 66 ++++++++++++++++++ .../code-form/dynamic-output-variable.tsx | 66 ++++++++++++++++++ web/src/pages/flow/form/code-form/index.less | 16 +++++ web/src/pages/flow/form/code-form/index.tsx | 67 +++++++++++++++++++ .../components/dynamic-input-variable.tsx | 12 ++-- web/src/pages/flow/hooks.tsx | 2 + 14 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 web/src/constants/agent.ts create mode 100644 web/src/pages/flow/form/code-form/dynamic-input-variable.tsx create mode 100644 web/src/pages/flow/form/code-form/dynamic-output-variable.tsx create mode 100644 web/src/pages/flow/form/code-form/index.less create mode 100644 web/src/pages/flow/form/code-form/index.tsx diff --git a/web/src/constants/agent.ts b/web/src/constants/agent.ts new file mode 100644 index 000000000..32d5e9ddc --- /dev/null +++ b/web/src/constants/agent.ts @@ -0,0 +1,26 @@ +export enum ProgrammingLanguage { + Python = 'python', + Javascript = 'javascript', +} + +export const CodeTemplateStrMap = { + [ProgrammingLanguage.Python]: ` + def main(arg1: str, arg2: str) -> dict: + return { + "result": arg1 + arg2, + } + `, + [ProgrammingLanguage.Javascript]: ` + const axios = require('axios'); + async function main(args) { + try { + const response = await axios.get('https://github.com/infiniflow/ragflow'); + console.log('Body:', response.data); + } catch (error) { + console.error('Error:', error.message); + } + } + + module.exports = { main }; + `, +}; diff --git a/web/src/interfaces/database/flow.ts b/web/src/interfaces/database/flow.ts index 940f48599..8d324c373 100644 --- a/web/src/interfaces/database/flow.ts +++ b/web/src/interfaces/database/flow.ts @@ -119,6 +119,12 @@ export interface IRetrievalForm { kb_ids: string[]; } +export interface ICodeForm { + inputs?: Array<{ name?: string; component_id?: string }>; + lang: string; + script?: string; +} + export type BaseNodeData = { label: string; // operator type name: string; // operator name @@ -145,6 +151,7 @@ export type IEmailNode = BaseNode; export type IIterationNode = BaseNode; export type IIterationStartNode = BaseNode; export type IKeywordNode = BaseNode; +export type ICodeNode = BaseNode; export type RAGFlowNodeType = | IBeginNode diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 8a01cb5e1..c7c4614c1 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1262,6 +1262,9 @@ This delimiter is used to split the input text into several text pieces echo of knowledgeBasesTip: 'Select the knowledge bases to associate with this chat assistant, or choose variables containing knowledge base IDs below.', knowledgeBaseVars: 'Knowledge base variables', + code: 'Code', + codeDescription: 'It allows developers to write custom Python logic.', + inputVariables: 'Input variables', runningHintText: 'is running...🕞', }, }, diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index 892e4430f..aa57438ec 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -364,7 +364,8 @@ export default { {knowledge} 上記がナレッジベースです。`, systemMessage: '入力してください!', - systemTip: 'LLMが質問に答える際に従う指示を設定します。モデルがネイティブで推論をサポートしている場合、推論を停止するためにプロンプトに //no_thinking を追加できます。', + systemTip: + 'LLMが質問に答える際に従う指示を設定します。モデルがネイティブで推論をサポートしている場合、推論を停止するためにプロンプトに //no_thinking を追加できます。', topN: 'トップN', topNTip: `類似度スコアがしきい値を超えるチャンクのうち、上位N件のみがLLMに供給されます。`, variable: '変数', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index 923a94b66..219386d29 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -1158,6 +1158,9 @@ export default { promptMessage: '提示詞是必填項', promptTip: '系統提示為大型模型提供任務描述、規定回覆方式,以及設定其他各種要求。系統提示通常與 key(變數)合用,透過變數設定大型模型的輸入資料。你可以透過斜線或 (x) 按鈕顯示可用的 key。', + code: '程式碼', + codeDescription: '它允許開發人員編寫自訂 Python 邏輯。', + inputVariables: '輸入變數', runningHintText: '正在運行...🕞', }, footer: { diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 6b4988af7..01e1e80bd 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1217,6 +1217,10 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 '系统提示为大模型提供任务描述、规定回复方式,以及设置其他各种要求。系统提示通常与 key (变量)合用,通过变量设置大模型的输入数据。你可以通过斜杠或者 (x) 按钮显示可用的 key。', knowledgeBasesTip: '选择关联的知识库,或者在下方选择包含知识库ID的变量。', knowledgeBaseVars: '知识库变量', + code: '代码', + codeDescription: '它允许开发人员编写自定义 Python 逻辑。', + inputVariables: '输入变量', + addVariable: '新增变量', runningHintText: '正在运行中...🕞', }, footer: { diff --git a/web/src/pages/flow/constant.tsx b/web/src/pages/flow/constant.tsx index 5e8097afc..77ce94532 100644 --- a/web/src/pages/flow/constant.tsx +++ b/web/src/pages/flow/constant.tsx @@ -27,6 +27,7 @@ import { ReactComponent as TemplateIcon } from '@/assets/svg/template.svg'; import { ReactComponent as TuShareIcon } from '@/assets/svg/tushare.svg'; import { ReactComponent as WenCaiIcon } from '@/assets/svg/wencai.svg'; import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.svg'; +import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; // 邮件功能 @@ -56,6 +57,7 @@ import upperFirst from 'lodash/upperFirst'; import { CirclePower, CloudUpload, + CodeXml, Database, IterationCcw, ListOrdered, @@ -104,6 +106,7 @@ export enum Operator { Email = 'Email', Iteration = 'Iteration', IterationStart = 'IterationItem', + Code = 'Code', } export const CommonOperatorList = Object.values(Operator).filter( @@ -147,6 +150,7 @@ export const operatorIconMap = { [Operator.Email]: EmailIcon, [Operator.Iteration]: IterationCcw, [Operator.IterationStart]: CirclePower, + [Operator.Code]: CodeXml, }; export const operatorMap: Record< @@ -285,6 +289,7 @@ export const operatorMap: Record< [Operator.Email]: { backgroundColor: '#e6f7ff' }, [Operator.Iteration]: { backgroundColor: '#e6f7ff' }, [Operator.IterationStart]: { backgroundColor: '#e6f7ff' }, + [Operator.Code]: { backgroundColor: '#4c5458' }, }; export const componentMenuList = [ @@ -322,6 +327,9 @@ export const componentMenuList = [ { name: Operator.Iteration, }, + { + name: Operator.Code, + }, { name: Operator.Note, }, @@ -633,6 +641,19 @@ export const initialIterationValues = { }; export const initialIterationStartValues = {}; +export const initialCodeValues = { + lang: 'python', + script: CodeTemplateStrMap[ProgrammingLanguage.Python], + arguments: [ + { + name: 'arg1', + }, + { + name: 'arg2', + }, + ], +}; + export const CategorizeAnchorPointPositions = [ { top: 1, right: 34 }, { top: 8, right: 18 }, @@ -714,6 +735,7 @@ export const RestrictedUpstreamMap = { [Operator.Email]: [Operator.Begin], [Operator.Iteration]: [Operator.Begin], [Operator.IterationStart]: [Operator.Begin], + [Operator.Code]: [Operator.Begin], }; export const NodeMap = { @@ -753,6 +775,7 @@ export const NodeMap = { [Operator.Email]: 'emailNode', [Operator.Iteration]: 'group', [Operator.IterationStart]: 'iterationStartNode', + [Operator.Code]: 'ragNode', }; export const LanguageOptions = [ diff --git a/web/src/pages/flow/flow-drawer/index.tsx b/web/src/pages/flow/flow-drawer/index.tsx index 40afa61f7..e9778f1b5 100644 --- a/web/src/pages/flow/flow-drawer/index.tsx +++ b/web/src/pages/flow/flow-drawer/index.tsx @@ -14,6 +14,7 @@ import BaiduForm from '../form/baidu-form'; import BeginForm from '../form/begin-form'; import BingForm from '../form/bing-form'; import CategorizeForm from '../form/categorize-form'; +import CodeForm from '../form/code-form'; import CrawlerForm from '../form/crawler-form'; import DeepLForm from '../form/deepl-form'; import DuckDuckGoForm from '../form/duckduckgo-form'; @@ -97,6 +98,7 @@ const FormMap = { [Operator.Email]: EmailForm, [Operator.Iteration]: IterationForm, [Operator.IterationStart]: () => <>, + [Operator.Code]: CodeForm, }; const EmptyContent = () =>
; diff --git a/web/src/pages/flow/form/code-form/dynamic-input-variable.tsx b/web/src/pages/flow/form/code-form/dynamic-input-variable.tsx new file mode 100644 index 000000000..8fc55c0be --- /dev/null +++ b/web/src/pages/flow/form/code-form/dynamic-input-variable.tsx @@ -0,0 +1,66 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Form, Input, Select } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; +import { FormCollapse } from '../components/dynamic-input-variable'; + +type DynamicInputVariableProps = { + name?: string; + node?: RAGFlowNodeType; +}; + +export const DynamicInputVariable = ({ + name = 'arguments', + node, +}: DynamicInputVariableProps) => { + const { t } = useTranslation(); + + const valueOptions = useBuildComponentIdSelectOptions( + node?.id, + node?.parentId, + ); + + return ( + + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( +
+ + + + + + + remove(name)} /> +
+ ))} + + + + + )} +
+
+ ); +}; diff --git a/web/src/pages/flow/form/code-form/dynamic-output-variable.tsx b/web/src/pages/flow/form/code-form/dynamic-output-variable.tsx new file mode 100644 index 000000000..0e7e3a03c --- /dev/null +++ b/web/src/pages/flow/form/code-form/dynamic-output-variable.tsx @@ -0,0 +1,66 @@ +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Form, Input, Select } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { FormCollapse } from '../components/dynamic-input-variable'; + +type DynamicOutputVariableProps = { + name?: string; +}; + +const options = [ + 'String', + 'Number', + 'Boolean', + 'Array[String]', + 'Array[Number]', + 'Object', +].map((x) => ({ label: x, value: x })); + +export const DynamicOutputVariable = ({ + name = 'output', +}: DynamicOutputVariableProps) => { + const { t } = useTranslation(); + + return ( + + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( +
+ + + + + + + remove(name)} /> +
+ ))} + + + + + )} +
+
+ ); +}; diff --git a/web/src/pages/flow/form/code-form/index.less b/web/src/pages/flow/form/code-form/index.less new file mode 100644 index 000000000..cde868bb6 --- /dev/null +++ b/web/src/pages/flow/form/code-form/index.less @@ -0,0 +1,16 @@ +.languageItem { + margin: 0; + :global(.ant-select-selector) { + background: transparent !important; + border: none !important; + box-shadow: none !important; + } + :global(.ant-select-selector:hover) { + border: none !important; + box-shadow: none !important; + } + :global(.ant-select-focused .ant-select-selector) { + border: none !important; + box-shadow: none !important; + } +} diff --git a/web/src/pages/flow/form/code-form/index.tsx b/web/src/pages/flow/form/code-form/index.tsx new file mode 100644 index 000000000..73e575cbb --- /dev/null +++ b/web/src/pages/flow/form/code-form/index.tsx @@ -0,0 +1,67 @@ +import Editor, { loader } from '@monaco-editor/react'; +import { Form, Select } from 'antd'; +import { IOperatorForm } from '../../interface'; +import { DynamicInputVariable } from './dynamic-input-variable'; + +import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; +import { ICodeForm } from '@/interfaces/database/flow'; +import { useEffect } from 'react'; +import styles from './index.less'; + +loader.config({ paths: { vs: '/vs' } }); + +const options = [ + ProgrammingLanguage.Python, + ProgrammingLanguage.Javascript, +].map((x) => ({ value: x, label: x })); + +const CodeForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const formData = node?.data.form as ICodeForm; + + useEffect(() => { + setTimeout(() => { + // TODO: Direct operation zustand is more elegant + form?.setFieldValue( + 'script', + CodeTemplateStrMap[formData.lang as ProgrammingLanguage], + ); + }, 0); + }, [form, formData.lang]); + + return ( +
+ + +