### 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
| 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 }; | |||||
| `, | |||||
| }; |
| kb_ids: string[]; | kb_ids: string[]; | ||||
| } | } | ||||
| export interface ICodeForm { | |||||
| inputs?: Array<{ name?: string; component_id?: string }>; | |||||
| lang: string; | |||||
| script?: string; | |||||
| } | |||||
| export type BaseNodeData<TForm extends any> = { | export type BaseNodeData<TForm extends any> = { | ||||
| label: string; // operator type | label: string; // operator type | ||||
| name: string; // operator name | name: string; // operator name | ||||
| export type IIterationNode = BaseNode; | export type IIterationNode = BaseNode; | ||||
| export type IIterationStartNode = BaseNode; | export type IIterationStartNode = BaseNode; | ||||
| export type IKeywordNode = BaseNode; | export type IKeywordNode = BaseNode; | ||||
| export type ICodeNode = BaseNode<ICodeForm>; | |||||
| export type RAGFlowNodeType = | export type RAGFlowNodeType = | ||||
| | IBeginNode | | IBeginNode |
| knowledgeBasesTip: | knowledgeBasesTip: | ||||
| 'Select the knowledge bases to associate with this chat assistant, or choose variables containing knowledge base IDs below.', | 'Select the knowledge bases to associate with this chat assistant, or choose variables containing knowledge base IDs below.', | ||||
| knowledgeBaseVars: 'Knowledge base variables', | knowledgeBaseVars: 'Knowledge base variables', | ||||
| code: 'Code', | |||||
| codeDescription: 'It allows developers to write custom Python logic.', | |||||
| inputVariables: 'Input variables', | |||||
| runningHintText: 'is running...🕞', | runningHintText: 'is running...🕞', | ||||
| }, | }, | ||||
| }, | }, |
| {knowledge} | {knowledge} | ||||
| 上記がナレッジベースです。`, | 上記がナレッジベースです。`, | ||||
| systemMessage: '入力してください!', | systemMessage: '入力してください!', | ||||
| systemTip: 'LLMが質問に答える際に従う指示を設定します。モデルがネイティブで推論をサポートしている場合、推論を停止するためにプロンプトに //no_thinking を追加できます。', | |||||
| systemTip: | |||||
| 'LLMが質問に答える際に従う指示を設定します。モデルがネイティブで推論をサポートしている場合、推論を停止するためにプロンプトに //no_thinking を追加できます。', | |||||
| topN: 'トップN', | topN: 'トップN', | ||||
| topNTip: `類似度スコアがしきい値を超えるチャンクのうち、上位N件のみがLLMに供給されます。`, | topNTip: `類似度スコアがしきい値を超えるチャンクのうち、上位N件のみがLLMに供給されます。`, | ||||
| variable: '変数', | variable: '変数', |
| promptMessage: '提示詞是必填項', | promptMessage: '提示詞是必填項', | ||||
| promptTip: | promptTip: | ||||
| '系統提示為大型模型提供任務描述、規定回覆方式,以及設定其他各種要求。系統提示通常與 key(變數)合用,透過變數設定大型模型的輸入資料。你可以透過斜線或 (x) 按鈕顯示可用的 key。', | '系統提示為大型模型提供任務描述、規定回覆方式,以及設定其他各種要求。系統提示通常與 key(變數)合用,透過變數設定大型模型的輸入資料。你可以透過斜線或 (x) 按鈕顯示可用的 key。', | ||||
| code: '程式碼', | |||||
| codeDescription: '它允許開發人員編寫自訂 Python 邏輯。', | |||||
| inputVariables: '輸入變數', | |||||
| runningHintText: '正在運行...🕞', | runningHintText: '正在運行...🕞', | ||||
| }, | }, | ||||
| footer: { | footer: { |
| '系统提示为大模型提供任务描述、规定回复方式,以及设置其他各种要求。系统提示通常与 key (变量)合用,通过变量设置大模型的输入数据。你可以通过斜杠或者 (x) 按钮显示可用的 key。', | '系统提示为大模型提供任务描述、规定回复方式,以及设置其他各种要求。系统提示通常与 key (变量)合用,通过变量设置大模型的输入数据。你可以通过斜杠或者 (x) 按钮显示可用的 key。', | ||||
| knowledgeBasesTip: '选择关联的知识库,或者在下方选择包含知识库ID的变量。', | knowledgeBasesTip: '选择关联的知识库,或者在下方选择包含知识库ID的变量。', | ||||
| knowledgeBaseVars: '知识库变量', | knowledgeBaseVars: '知识库变量', | ||||
| code: '代码', | |||||
| codeDescription: '它允许开发人员编写自定义 Python 逻辑。', | |||||
| inputVariables: '输入变量', | |||||
| addVariable: '新增变量', | |||||
| runningHintText: '正在运行中...🕞', | runningHintText: '正在运行中...🕞', | ||||
| }, | }, | ||||
| footer: { | footer: { |
| import { ReactComponent as TuShareIcon } from '@/assets/svg/tushare.svg'; | import { ReactComponent as TuShareIcon } from '@/assets/svg/tushare.svg'; | ||||
| import { ReactComponent as WenCaiIcon } from '@/assets/svg/wencai.svg'; | import { ReactComponent as WenCaiIcon } from '@/assets/svg/wencai.svg'; | ||||
| import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.svg'; | import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.svg'; | ||||
| import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; | |||||
| // 邮件功能 | // 邮件功能 | ||||
| import { | import { | ||||
| CirclePower, | CirclePower, | ||||
| CloudUpload, | CloudUpload, | ||||
| CodeXml, | |||||
| Database, | Database, | ||||
| IterationCcw, | IterationCcw, | ||||
| ListOrdered, | ListOrdered, | ||||
| Email = 'Email', | Email = 'Email', | ||||
| Iteration = 'Iteration', | Iteration = 'Iteration', | ||||
| IterationStart = 'IterationItem', | IterationStart = 'IterationItem', | ||||
| Code = 'Code', | |||||
| } | } | ||||
| export const CommonOperatorList = Object.values(Operator).filter( | export const CommonOperatorList = Object.values(Operator).filter( | ||||
| [Operator.Email]: EmailIcon, | [Operator.Email]: EmailIcon, | ||||
| [Operator.Iteration]: IterationCcw, | [Operator.Iteration]: IterationCcw, | ||||
| [Operator.IterationStart]: CirclePower, | [Operator.IterationStart]: CirclePower, | ||||
| [Operator.Code]: CodeXml, | |||||
| }; | }; | ||||
| export const operatorMap: Record< | export const operatorMap: Record< | ||||
| [Operator.Email]: { backgroundColor: '#e6f7ff' }, | [Operator.Email]: { backgroundColor: '#e6f7ff' }, | ||||
| [Operator.Iteration]: { backgroundColor: '#e6f7ff' }, | [Operator.Iteration]: { backgroundColor: '#e6f7ff' }, | ||||
| [Operator.IterationStart]: { backgroundColor: '#e6f7ff' }, | [Operator.IterationStart]: { backgroundColor: '#e6f7ff' }, | ||||
| [Operator.Code]: { backgroundColor: '#4c5458' }, | |||||
| }; | }; | ||||
| export const componentMenuList = [ | export const componentMenuList = [ | ||||
| { | { | ||||
| name: Operator.Iteration, | name: Operator.Iteration, | ||||
| }, | }, | ||||
| { | |||||
| name: Operator.Code, | |||||
| }, | |||||
| { | { | ||||
| name: Operator.Note, | name: Operator.Note, | ||||
| }, | }, | ||||
| }; | }; | ||||
| export const initialIterationStartValues = {}; | export const initialIterationStartValues = {}; | ||||
| export const initialCodeValues = { | |||||
| lang: 'python', | |||||
| script: CodeTemplateStrMap[ProgrammingLanguage.Python], | |||||
| arguments: [ | |||||
| { | |||||
| name: 'arg1', | |||||
| }, | |||||
| { | |||||
| name: 'arg2', | |||||
| }, | |||||
| ], | |||||
| }; | |||||
| export const CategorizeAnchorPointPositions = [ | export const CategorizeAnchorPointPositions = [ | ||||
| { top: 1, right: 34 }, | { top: 1, right: 34 }, | ||||
| { top: 8, right: 18 }, | { top: 8, right: 18 }, | ||||
| [Operator.Email]: [Operator.Begin], | [Operator.Email]: [Operator.Begin], | ||||
| [Operator.Iteration]: [Operator.Begin], | [Operator.Iteration]: [Operator.Begin], | ||||
| [Operator.IterationStart]: [Operator.Begin], | [Operator.IterationStart]: [Operator.Begin], | ||||
| [Operator.Code]: [Operator.Begin], | |||||
| }; | }; | ||||
| export const NodeMap = { | export const NodeMap = { | ||||
| [Operator.Email]: 'emailNode', | [Operator.Email]: 'emailNode', | ||||
| [Operator.Iteration]: 'group', | [Operator.Iteration]: 'group', | ||||
| [Operator.IterationStart]: 'iterationStartNode', | [Operator.IterationStart]: 'iterationStartNode', | ||||
| [Operator.Code]: 'ragNode', | |||||
| }; | }; | ||||
| export const LanguageOptions = [ | export const LanguageOptions = [ |
| import BeginForm from '../form/begin-form'; | import BeginForm from '../form/begin-form'; | ||||
| import BingForm from '../form/bing-form'; | import BingForm from '../form/bing-form'; | ||||
| import CategorizeForm from '../form/categorize-form'; | import CategorizeForm from '../form/categorize-form'; | ||||
| import CodeForm from '../form/code-form'; | |||||
| import CrawlerForm from '../form/crawler-form'; | import CrawlerForm from '../form/crawler-form'; | ||||
| import DeepLForm from '../form/deepl-form'; | import DeepLForm from '../form/deepl-form'; | ||||
| import DuckDuckGoForm from '../form/duckduckgo-form'; | import DuckDuckGoForm from '../form/duckduckgo-form'; | ||||
| [Operator.Email]: EmailForm, | [Operator.Email]: EmailForm, | ||||
| [Operator.Iteration]: IterationForm, | [Operator.Iteration]: IterationForm, | ||||
| [Operator.IterationStart]: () => <></>, | [Operator.IterationStart]: () => <></>, | ||||
| [Operator.Code]: CodeForm, | |||||
| }; | }; | ||||
| const EmptyContent = () => <div></div>; | const EmptyContent = () => <div></div>; |
| 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> | |||||
| ); | |||||
| }; |
| 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> | |||||
| ); | |||||
| }; |
| .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; | |||||
| } | |||||
| } |
| 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; |
| type === VariableType.Reference ? 'component_id' : 'value'; | type === VariableType.Reference ? 'component_id' : 'value'; | ||||
| const DynamicVariableForm = ({ name: formName, node }: IProps) => { | const DynamicVariableForm = ({ name: formName, node }: IProps) => { | ||||
| formName = formName || 'query'; | |||||
| const nextFormName = formName || 'query'; | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const valueOptions = useBuildComponentIdSelectOptions( | const valueOptions = useBuildComponentIdSelectOptions( | ||||
| node?.id, | node?.id, | ||||
| const handleTypeChange = useCallback( | const handleTypeChange = useCallback( | ||||
| (name: number) => () => { | (name: number) => () => { | ||||
| setTimeout(() => { | 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); | }, 0); | ||||
| }, | }, | ||||
| [form], | |||||
| [form, nextFormName], | |||||
| ); | ); | ||||
| return ( | return ( | ||||
| <Form.List name={formName}> | |||||
| <Form.List name={nextFormName}> | |||||
| {(fields, { add, remove }) => ( | {(fields, { add, remove }) => ( | ||||
| <> | <> | ||||
| {fields.map(({ key, name, ...restField }) => ( | {fields.map(({ key, name, ...restField }) => ( | ||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item noStyle dependencies={[name, 'type']}> | <Form.Item noStyle dependencies={[name, 'type']}> | ||||
| {({ getFieldValue }) => { | {({ getFieldValue }) => { | ||||
| const type = getFieldValue([formName, name, 'type']); | |||||
| const type = getFieldValue([nextFormName, name, 'type']); | |||||
| return ( | return ( | ||||
| <Form.Item | <Form.Item | ||||
| {...restField} | {...restField} |
| initialBeginValues, | initialBeginValues, | ||||
| initialBingValues, | initialBingValues, | ||||
| initialCategorizeValues, | initialCategorizeValues, | ||||
| initialCodeValues, | |||||
| initialConcentratorValues, | initialConcentratorValues, | ||||
| initialCrawlerValues, | initialCrawlerValues, | ||||
| initialDeepLValues, | initialDeepLValues, | ||||
| [Operator.Email]: initialEmailValues, | [Operator.Email]: initialEmailValues, | ||||
| [Operator.Iteration]: initialIterationValues, | [Operator.Iteration]: initialIterationValues, | ||||
| [Operator.IterationStart]: initialIterationValues, | [Operator.IterationStart]: initialIterationValues, | ||||
| [Operator.Code]: initialCodeValues, | |||||
| }; | }; | ||||
| }, [llmId]); | }, [llmId]); | ||||