Skip to content

Commit db4fd19

Browse files
authored
Feat:new component list operations (infiniflow#11276)
### What problem does this PR solve? issue: infiniflow#10427 change: new component list operations ### Type of change - [x] New Feature (non-breaking change which adds functionality)
1 parent 12db62b commit db4fd19

13 files changed

Lines changed: 384 additions & 2 deletions

File tree

agent/component/list_operations.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from abc import ABC
2+
import os
3+
from agent.component.base import ComponentBase, ComponentParamBase
4+
from api.utils.api_utils import timeout
5+
6+
class ListOperationsParam(ComponentParamBase):
7+
"""
8+
Define the List Operations component parameters.
9+
"""
10+
def __init__(self):
11+
super().__init__()
12+
self.query = ""
13+
self.operations = "topN"
14+
self.n=0
15+
self.sort_method = "asc"
16+
self.filter = {
17+
"operator": "=",
18+
"value": ""
19+
}
20+
self.outputs = {
21+
"result": {
22+
"value": [],
23+
"type": "Array of ?"
24+
},
25+
"first": {
26+
"value": "",
27+
"type": "?"
28+
},
29+
"last": {
30+
"value": "",
31+
"type": "?"
32+
}
33+
}
34+
35+
def check(self):
36+
self.check_empty(self.query, "query")
37+
self.check_valid_value(self.operations, "Support operations", ["topN","head","tail","filter","sort","drop_duplicates"])
38+
39+
def get_input_form(self) -> dict[str, dict]:
40+
return {}
41+
42+
43+
class ListOperations(ComponentBase,ABC):
44+
component_name = "ListOperations"
45+
46+
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
47+
def _invoke(self, **kwargs):
48+
self.input_objects=[]
49+
inputs = getattr(self._param, "query", None)
50+
self.inputs=self._canvas.get_variable_value(inputs)
51+
self.set_input_value(inputs, self.inputs)
52+
if self._param.operations == "topN":
53+
self._topN()
54+
elif self._param.operations == "head":
55+
self._head()
56+
elif self._param.operations == "tail":
57+
self._tail()
58+
elif self._param.operations == "filter":
59+
self._filter()
60+
elif self._param.operations == "sort":
61+
self._sort()
62+
elif self._param.operations == "drop_duplicates":
63+
self._drop_duplicates()
64+
65+
66+
def _coerce_n(self):
67+
try:
68+
return int(getattr(self._param, "n", 0))
69+
except Exception:
70+
return 0
71+
72+
def _set_outputs(self, outputs):
73+
self._param.outputs["result"]["value"] = outputs
74+
self._param.outputs["first"]["value"] = outputs[0] if outputs else None
75+
self._param.outputs["last"]["value"] = outputs[-1] if outputs else None
76+
77+
def _topN(self):
78+
n = self._coerce_n()
79+
if n < 1:
80+
outputs = []
81+
else:
82+
n = min(n, len(self.inputs))
83+
outputs = self.inputs[:n]
84+
self._set_outputs(outputs)
85+
86+
def _head(self):
87+
n = self._coerce_n()
88+
if 1 <= n <= len(self.inputs):
89+
outputs = [self.inputs[n - 1]]
90+
else:
91+
outputs = []
92+
self._set_outputs(outputs)
93+
94+
def _tail(self):
95+
n = self._coerce_n()
96+
if 1 <= n <= len(self.inputs):
97+
outputs = [self.inputs[-n]]
98+
else:
99+
outputs = []
100+
self._set_outputs(outputs)
101+
102+
def _filter(self):
103+
self._set_outputs([i for i in self.inputs if self._eval(self._norm(i),self._param.filter["operator"],self._param.filter["value"])])
104+
105+
def _norm(self,v):
106+
s = "" if v is None else str(v)
107+
return s
108+
109+
def _eval(self, v, operator, value):
110+
if operator == "=":
111+
return v == value
112+
elif operator == "≠":
113+
return v != value
114+
elif operator == "contains":
115+
return value in v
116+
elif operator == "start with":
117+
return v.startswith(value)
118+
elif operator == "end with":
119+
return v.endswith(value)
120+
else:
121+
return False
122+
123+
def _sort(self):
124+
if self._param.sort_method == "asc":
125+
self._set_outputs(sorted(self.inputs))
126+
elif self._param.sort_method == "desc":
127+
self._set_outputs(sorted(self.inputs, reverse=True))
128+
129+
def _drop_duplicates(self):
130+
seen = set()
131+
outs = []
132+
for item in self.inputs:
133+
k = self._hashable(item)
134+
if k in seen:
135+
continue
136+
seen.add(k)
137+
outs.append(item)
138+
self._set_outputs(outs)
139+
140+
def _hashable(self,x):
141+
if isinstance(x, dict):
142+
return tuple(sorted((k, self._hashable(v)) for k, v in x.items()))
143+
if isinstance(x, (list, tuple)):
144+
return tuple(self._hashable(v) for v in x)
145+
if isinstance(x, set):
146+
return tuple(sorted(self._hashable(v) for v in x))
147+
return x
148+
def thoughts(self) -> str:
149+
return "ListOperation in progress"

web/src/constants/agent.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export enum Operator {
109109
SearXNG = 'SearXNG',
110110
Placeholder = 'Placeholder',
111111
DataOperations = 'DataOperations',
112+
ListOperations = 'ListOperations',
112113
VariableAssigner = 'VariableAssigner',
113114
VariableAggregator = 'VariableAggregator',
114115
File = 'File', // pipeline

web/src/locales/en.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,8 @@ This delimiter is used to split the input text into several text pieces echo of
15911591
codeDescription: 'It allows developers to write custom Python logic.',
15921592
dataOperations: 'Data operations',
15931593
dataOperationsDescription: 'Perform various operations on a Data object.',
1594+
listOperations: 'List operations',
1595+
listOperationsDescription: 'Perform operations on a list.',
15941596
variableAssigner: 'Variable assigner',
15951597
variableAssignerDescription:
15961598
'This component performs operations on Data objects, including extracting, filtering, and editing keys and values in the Data.',
@@ -1806,6 +1808,19 @@ Important structured information may include: names, dates, locations, events, k
18061808
removeKeys: 'Remove keys',
18071809
renameKeys: 'Rename keys',
18081810
},
1811+
ListOperationsOptions: {
1812+
topN: 'Top N',
1813+
head: 'Head',
1814+
tail: 'Tail',
1815+
sort: 'Sort',
1816+
filter: 'Filter',
1817+
dropDuplicates: 'Drop duplicates',
1818+
},
1819+
sortMethod: 'Sort method',
1820+
SortMethodOptions: {
1821+
asc: 'Ascending',
1822+
desc: 'Descending',
1823+
},
18091824
},
18101825
llmTools: {
18111826
bad_calculator: {

web/src/locales/zh.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,6 +1508,8 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
15081508
codeDescription: '它允许开发人员编写自定义 Python 逻辑。',
15091509
dataOperations: '数据操作',
15101510
dataOperationsDescription: '对数据对象执行各种操作。',
1511+
listOperations: '列表操作',
1512+
listOperationsDescription: '对列表对象执行各种操作。',
15111513
variableAssigner: '变量赋值器',
15121514
variableAssignerDescription:
15131515
'此组件对数据对象执行操作,包括提取、筛选和编辑数据中的键和值。',
@@ -1679,6 +1681,19 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
16791681
removeKeys: '删除键',
16801682
renameKeys: '重命名键',
16811683
},
1684+
ListOperationsOptions: {
1685+
topN: '取前N项',
1686+
head: '取前第N项',
1687+
tail: '取后第N项',
1688+
sort: '排序',
1689+
filter: '筛选',
1690+
dropDuplicates: '去重',
1691+
},
1692+
sortMethod: '排序方式',
1693+
SortMethodOptions: {
1694+
asc: '升序',
1695+
desc: '降序',
1696+
},
16821697
},
16831698
footer: {
16841699
profile: 'All rights reserved @ React',

web/src/pages/agent/canvas/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { FileNode } from './node/file-node';
6161
import { InvokeNode } from './node/invoke-node';
6262
import { IterationNode, IterationStartNode } from './node/iteration-node';
6363
import { KeywordNode } from './node/keyword-node';
64+
import { ListOperationsNode } from './node/list-operations-node';
6465
import { MessageNode } from './node/message-node';
6566
import NoteNode from './node/note-node';
6667
import ParserNode from './node/parser-node';
@@ -101,6 +102,7 @@ export const nodeTypes: NodeTypes = {
101102
splitterNode: SplitterNode,
102103
contextNode: ExtractorNode,
103104
dataOperationsNode: DataOperationsNode,
105+
listOperationsNode: ListOperationsNode,
104106
variableAssignerNode: VariableAssignerNode,
105107
variableAggregatorNode: VariableAggregatorNode,
106108
};

web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export function AccordionOperators({
7979
Operator.Code,
8080
Operator.StringTransform,
8181
Operator.DataOperations,
82+
Operator.ListOperations,
8283
// Operator.VariableAssigner,
8384
Operator.VariableAggregator,
8485
]}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { BaseNode } from '@/interfaces/database/agent';
2+
import { NodeProps } from '@xyflow/react';
3+
import { camelCase } from 'lodash';
4+
import { useTranslation } from 'react-i18next';
5+
import { RagNode } from '.';
6+
import { ListOperationsFormSchemaType } from '../../form/list-operations-form';
7+
import { LabelCard } from './card';
8+
9+
export function ListOperationsNode({
10+
...props
11+
}: NodeProps<BaseNode<ListOperationsFormSchemaType>>) {
12+
const { data } = props;
13+
const { t } = useTranslation();
14+
15+
return (
16+
<RagNode {...props}>
17+
<LabelCard>
18+
{t(`flow.ListOperationsOptions.${camelCase(data.form?.operations)}`)}
19+
</LabelCard>
20+
</RagNode>
21+
);
22+
}

web/src/pages/agent/constant/index.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,35 @@ export const initialDataOperationsValues = {
595595
},
596596
},
597597
};
598+
export enum SortMethod {
599+
Asc = 'asc',
600+
Desc = 'desc',
601+
}
602+
603+
export enum ListOperations {
604+
TopN = 'topN',
605+
Head = 'head',
606+
Tail = 'tail',
607+
Filter = 'filter',
608+
Sort = 'sort',
609+
DropDuplicates = 'drop_duplicates',
610+
}
611+
612+
export const initialListOperationsValues = {
613+
query: '',
614+
operations: ListOperations.TopN,
615+
outputs: {
616+
result: {
617+
type: 'Array<?>',
618+
},
619+
first: {
620+
type: '?',
621+
},
622+
last: {
623+
type: '?',
624+
},
625+
},
626+
};
598627

599628
export const initialVariableAssignerValues = {};
600629

@@ -673,6 +702,7 @@ export const RestrictedUpstreamMap = {
673702
[Operator.Tool]: [Operator.Begin],
674703
[Operator.Placeholder]: [Operator.Begin],
675704
[Operator.DataOperations]: [Operator.Begin],
705+
[Operator.ListOperations]: [Operator.Begin],
676706
[Operator.Parser]: [Operator.Begin], // pipeline
677707
[Operator.Splitter]: [Operator.Begin],
678708
[Operator.HierarchicalMerger]: [Operator.Begin],
@@ -729,6 +759,7 @@ export const NodeMap = {
729759
[Operator.HierarchicalMerger]: 'splitterNode',
730760
[Operator.Extractor]: 'contextNode',
731761
[Operator.DataOperations]: 'dataOperationsNode',
762+
[Operator.ListOperations]: 'listOperationsNode',
732763
[Operator.VariableAssigner]: 'variableAssignerNode',
733764
[Operator.VariableAggregator]: 'variableAggregatorNode',
734765
};

web/src/pages/agent/form-sheet/form-config-map.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import IterationForm from '../form/iteration-form';
2121
import IterationStartForm from '../form/iteration-start-from';
2222
import Jin10Form from '../form/jin10-form';
2323
import KeywordExtractForm from '../form/keyword-extract-form';
24+
import ListOperationsForm from '../form/list-operations-form';
2425
import MessageForm from '../form/message-form';
2526
import ParserForm from '../form/parser-form';
2627
import PubMedForm from '../form/pubmed-form';
@@ -184,6 +185,9 @@ export const FormConfigMap = {
184185
[Operator.DataOperations]: {
185186
component: DataOperationsForm,
186187
},
188+
[Operator.ListOperations]: {
189+
component: ListOperationsForm,
190+
},
187191
[Operator.VariableAssigner]: {
188192
component: VariableAssignerForm,
189193
},

0 commit comments

Comments
 (0)