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)
This commit is contained in:
balibabu 2025-05-16 09:53:00 +08:00 committed by GitHub
parent 772992812a
commit 008e55a65e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 293 additions and 7 deletions

View File

@ -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 };
`,
};

View File

@ -119,6 +119,12 @@ export interface IRetrievalForm {
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
@ -145,6 +151,7 @@ export type IEmailNode = BaseNode;
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

View File

@ -1262,6 +1262,9 @@ This delimiter is used to split the input text into several text pieces echo of
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...🕞',
}, },
}, },

View File

@ -364,7 +364,8 @@ export default {
{knowledge} {knowledge}
`, `,
systemMessage: '入力してください!', systemMessage: '入力してください!',
systemTip: 'LLMが質問に答える際に従う指示を設定します。モデルがネイティブで推論をサポートしている場合、推論を停止するためにプロンプトに //no_thinking を追加できます。', systemTip:
'LLMが質問に答える際に従う指示を設定します。モデルがネイティブで推論をサポートしている場合、推論を停止するためにプロンプトに //no_thinking を追加できます。',
topN: 'トップN', topN: 'トップN',
topNTip: `類似度スコアがしきい値を超えるチャンクのうち、上位N件のみがLLMに供給されます。`, topNTip: `類似度スコアがしきい値を超えるチャンクのうち、上位N件のみがLLMに供給されます。`,
variable: '変数', variable: '変数',

View File

@ -1158,6 +1158,9 @@ export default {
promptMessage: '提示詞是必填項', promptMessage: '提示詞是必填項',
promptTip: promptTip:
'系統提示為大型模型提供任務描述、規定回覆方式,以及設定其他各種要求。系統提示通常與 key變數合用透過變數設定大型模型的輸入資料。你可以透過斜線或 (x) 按鈕顯示可用的 key。', '系統提示為大型模型提供任務描述、規定回覆方式,以及設定其他各種要求。系統提示通常與 key變數合用透過變數設定大型模型的輸入資料。你可以透過斜線或 (x) 按鈕顯示可用的 key。',
code: '程式碼',
codeDescription: '它允許開發人員編寫自訂 Python 邏輯。',
inputVariables: '輸入變數',
runningHintText: '正在運行...🕞', runningHintText: '正在運行...🕞',
}, },
footer: { footer: {

View File

@ -1217,6 +1217,10 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
'系统提示为大模型提供任务描述、规定回复方式,以及设置其他各种要求。系统提示通常与 key (变量)合用,通过变量设置大模型的输入数据。你可以通过斜杠或者 (x) 按钮显示可用的 key。', '系统提示为大模型提供任务描述、规定回复方式,以及设置其他各种要求。系统提示通常与 key (变量)合用,通过变量设置大模型的输入数据。你可以通过斜杠或者 (x) 按钮显示可用的 key。',
knowledgeBasesTip: '选择关联的知识库或者在下方选择包含知识库ID的变量。', knowledgeBasesTip: '选择关联的知识库或者在下方选择包含知识库ID的变量。',
knowledgeBaseVars: '知识库变量', knowledgeBaseVars: '知识库变量',
code: '代码',
codeDescription: '它允许开发人员编写自定义 Python 逻辑。',
inputVariables: '输入变量',
addVariable: '新增变量',
runningHintText: '正在运行中...🕞', runningHintText: '正在运行中...🕞',
}, },
footer: { footer: {

View File

@ -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 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';
// 邮件功能 // 邮件功能
@ -56,6 +57,7 @@ import upperFirst from 'lodash/upperFirst';
import { import {
CirclePower, CirclePower,
CloudUpload, CloudUpload,
CodeXml,
Database, Database,
IterationCcw, IterationCcw,
ListOrdered, ListOrdered,
@ -104,6 +106,7 @@ export enum Operator {
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(
@ -147,6 +150,7 @@ export const operatorIconMap = {
[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<
@ -285,6 +289,7 @@ 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 = [
@ -322,6 +327,9 @@ export const componentMenuList = [
{ {
name: Operator.Iteration, name: Operator.Iteration,
}, },
{
name: Operator.Code,
},
{ {
name: Operator.Note, name: Operator.Note,
}, },
@ -633,6 +641,19 @@ export const initialIterationValues = {
}; };
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 },
@ -714,6 +735,7 @@ export const RestrictedUpstreamMap = {
[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 = {
@ -753,6 +775,7 @@ 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 = [

View File

@ -14,6 +14,7 @@ import BaiduForm from '../form/baidu-form';
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';
@ -97,6 +98,7 @@ const FormMap = {
[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>;

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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;
}
}

View File

@ -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;

View File

@ -22,7 +22,7 @@ const getVariableName = (type: string) =>
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,
@ -38,15 +38,15 @@ const DynamicVariableForm = ({ name: formName, node }: IProps) => {
const handleTypeChange = useCallback( const handleTypeChange = useCallback(
(name: number) => () => { (name: number) => () => {
setTimeout(() => { setTimeout(() => {
form.setFieldValue([formName, name, 'component_id'], undefined); form.setFieldValue([nextFormName, name, 'component_id'], undefined);
form.setFieldValue([formName, name, 'value'], 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 }) => (
@ -63,7 +63,7 @@ const DynamicVariableForm = ({ name: formName, node }: IProps) => {
</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}

View File

@ -39,6 +39,7 @@ import {
initialBeginValues, initialBeginValues,
initialBingValues, initialBingValues,
initialCategorizeValues, initialCategorizeValues,
initialCodeValues,
initialConcentratorValues, initialConcentratorValues,
initialCrawlerValues, initialCrawlerValues,
initialDeepLValues, initialDeepLValues,
@ -139,6 +140,7 @@ export const useInitializeOperatorParams = () => {
[Operator.Email]: initialEmailValues, [Operator.Email]: initialEmailValues,
[Operator.Iteration]: initialIterationValues, [Operator.Iteration]: initialIterationValues,
[Operator.IterationStart]: initialIterationValues, [Operator.IterationStart]: initialIterationValues,
[Operator.Code]: initialCodeValues,
}; };
}, [llmId]); }, [llmId]);