### 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)tags/v0.19.0
| @@ -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 }; | |||
| `, | |||
| }; | |||
| @@ -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<TForm extends any> = { | |||
| 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<ICodeForm>; | |||
| export type RAGFlowNodeType = | |||
| | IBeginNode | |||
| @@ -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...🕞', | |||
| }, | |||
| }, | |||
| @@ -364,7 +364,8 @@ export default { | |||
| {knowledge} | |||
| 上記がナレッジベースです。`, | |||
| systemMessage: '入力してください!', | |||
| systemTip: 'LLMが質問に答える際に従う指示を設定します。モデルがネイティブで推論をサポートしている場合、推論を停止するためにプロンプトに //no_thinking を追加できます。', | |||
| systemTip: | |||
| 'LLMが質問に答える際に従う指示を設定します。モデルがネイティブで推論をサポートしている場合、推論を停止するためにプロンプトに //no_thinking を追加できます。', | |||
| topN: 'トップN', | |||
| topNTip: `類似度スコアがしきい値を超えるチャンクのうち、上位N件のみがLLMに供給されます。`, | |||
| variable: '変数', | |||
| @@ -1158,6 +1158,9 @@ export default { | |||
| promptMessage: '提示詞是必填項', | |||
| promptTip: | |||
| '系統提示為大型模型提供任務描述、規定回覆方式,以及設定其他各種要求。系統提示通常與 key(變數)合用,透過變數設定大型模型的輸入資料。你可以透過斜線或 (x) 按鈕顯示可用的 key。', | |||
| code: '程式碼', | |||
| codeDescription: '它允許開發人員編寫自訂 Python 邏輯。', | |||
| inputVariables: '輸入變數', | |||
| runningHintText: '正在運行...🕞', | |||
| }, | |||
| footer: { | |||
| @@ -1217,6 +1217,10 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 | |||
| '系统提示为大模型提供任务描述、规定回复方式,以及设置其他各种要求。系统提示通常与 key (变量)合用,通过变量设置大模型的输入数据。你可以通过斜杠或者 (x) 按钮显示可用的 key。', | |||
| knowledgeBasesTip: '选择关联的知识库,或者在下方选择包含知识库ID的变量。', | |||
| knowledgeBaseVars: '知识库变量', | |||
| code: '代码', | |||
| codeDescription: '它允许开发人员编写自定义 Python 逻辑。', | |||
| inputVariables: '输入变量', | |||
| addVariable: '新增变量', | |||
| runningHintText: '正在运行中...🕞', | |||
| }, | |||
| footer: { | |||
| @@ -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 = [ | |||
| @@ -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 = () => <div></div>; | |||
| @@ -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 ( | |||
| <FormCollapse title={t('flow.inputVariables')}> | |||
| <Form.List name={name}> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| {fields.map(({ key, name, ...restField }) => ( | |||
| <div key={key} className="flex items-center gap-2 pb-4"> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'name']} | |||
| className="m-0 flex-1" | |||
| > | |||
| <Input /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'component_id']} | |||
| className="m-0 flex-1" | |||
| > | |||
| <Select | |||
| placeholder={t('common.pleaseSelect')} | |||
| options={valueOptions} | |||
| ></Select> | |||
| </Form.Item> | |||
| <MinusCircleOutlined onClick={() => remove(name)} /> | |||
| </div> | |||
| ))} | |||
| <Form.Item> | |||
| <Button | |||
| type="dashed" | |||
| onClick={() => add()} | |||
| block | |||
| icon={<PlusOutlined />} | |||
| > | |||
| {t('flow.addVariable')} | |||
| </Button> | |||
| </Form.Item> | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| </FormCollapse> | |||
| ); | |||
| }; | |||
| @@ -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 ( | |||
| <FormCollapse title={t('flow.output')}> | |||
| <Form.List name={name}> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| {fields.map(({ key, name, ...restField }) => ( | |||
| <div key={key} className="flex items-center gap-2 pb-4"> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'first']} | |||
| className="m-0 flex-1" | |||
| > | |||
| <Input /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'last']} | |||
| className="m-0 flex-1" | |||
| > | |||
| <Select | |||
| placeholder={t('common.pleaseSelect')} | |||
| options={options} | |||
| ></Select> | |||
| </Form.Item> | |||
| <MinusCircleOutlined onClick={() => remove(name)} /> | |||
| </div> | |||
| ))} | |||
| <Form.Item> | |||
| <Button | |||
| type="dashed" | |||
| onClick={() => add()} | |||
| block | |||
| icon={<PlusOutlined />} | |||
| > | |||
| {t('flow.addVariable')} | |||
| </Button> | |||
| </Form.Item> | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| </FormCollapse> | |||
| ); | |||
| }; | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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 ( | |||
| <Form | |||
| name="basic" | |||
| autoComplete="off" | |||
| form={form} | |||
| onValuesChange={onValuesChange} | |||
| layout={'vertical'} | |||
| > | |||
| <DynamicInputVariable node={node}></DynamicInputVariable> | |||
| <Form.Item | |||
| name={'script'} | |||
| label={ | |||
| <Form.Item name={'lang'} className={styles.languageItem}> | |||
| <Select | |||
| defaultValue={'python'} | |||
| popupMatchSelectWidth={false} | |||
| options={options} | |||
| /> | |||
| </Form.Item> | |||
| } | |||
| className="bg-gray-100 rounded dark:bg-gray-800" | |||
| > | |||
| <Editor | |||
| height={200} | |||
| theme="vs-dark" | |||
| language={formData.lang} | |||
| options={{ | |||
| minimap: { enabled: false }, | |||
| automaticLayout: true, | |||
| }} | |||
| /> | |||
| </Form.Item> | |||
| </Form> | |||
| ); | |||
| }; | |||
| export default CodeForm; | |||
| @@ -22,7 +22,7 @@ const getVariableName = (type: string) => | |||
| type === VariableType.Reference ? 'component_id' : 'value'; | |||
| const DynamicVariableForm = ({ name: formName, node }: IProps) => { | |||
| formName = formName || 'query'; | |||
| const nextFormName = formName || 'query'; | |||
| const { t } = useTranslation(); | |||
| const valueOptions = useBuildComponentIdSelectOptions( | |||
| node?.id, | |||
| @@ -38,15 +38,15 @@ const DynamicVariableForm = ({ name: formName, node }: IProps) => { | |||
| const handleTypeChange = useCallback( | |||
| (name: number) => () => { | |||
| setTimeout(() => { | |||
| form.setFieldValue([formName, name, 'component_id'], undefined); | |||
| form.setFieldValue([formName, name, 'value'], undefined); | |||
| form.setFieldValue([nextFormName, name, 'component_id'], undefined); | |||
| form.setFieldValue([nextFormName, name, 'value'], undefined); | |||
| }, 0); | |||
| }, | |||
| [form], | |||
| [form, nextFormName], | |||
| ); | |||
| return ( | |||
| <Form.List name={formName}> | |||
| <Form.List name={nextFormName}> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| {fields.map(({ key, name, ...restField }) => ( | |||
| @@ -63,7 +63,7 @@ const DynamicVariableForm = ({ name: formName, node }: IProps) => { | |||
| </Form.Item> | |||
| <Form.Item noStyle dependencies={[name, 'type']}> | |||
| {({ getFieldValue }) => { | |||
| const type = getFieldValue([formName, name, 'type']); | |||
| const type = getFieldValue([nextFormName, name, 'type']); | |||
| return ( | |||
| <Form.Item | |||
| {...restField} | |||
| @@ -39,6 +39,7 @@ import { | |||
| initialBeginValues, | |||
| initialBingValues, | |||
| initialCategorizeValues, | |||
| initialCodeValues, | |||
| initialConcentratorValues, | |||
| initialCrawlerValues, | |||
| initialDeepLValues, | |||
| @@ -139,6 +140,7 @@ export const useInitializeOperatorParams = () => { | |||
| [Operator.Email]: initialEmailValues, | |||
| [Operator.Iteration]: initialIterationValues, | |||
| [Operator.IterationStart]: initialIterationValues, | |||
| [Operator.Code]: initialCodeValues, | |||
| }; | |||
| }, [llmId]); | |||