Compare commits

..

10 Commits

Author SHA1 Message Date
db4fd19c82 Feat:new component list operations (#11276)
### What problem does this PR solve?
issue:
https://github.com/infiniflow/ragflow/issues/10427
change:
new component list operations

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-11-14 16:33:20 +08:00
12db62b9c7 Refactor: improve mineru_parser get property logic (#11268)
### What problem does this PR solve?

improve mineru_parser get property logic

### Type of change

- [x] Refactoring
2025-11-14 16:32:35 +08:00
b5f2cf16bc Fix: check task executor alive and display status (#11270)
### What problem does this PR solve?

Correctly check task executor alive and display status.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-11-14 15:52:28 +08:00
e27ff8d3d4 Fix: rerank algorithm (#11266)
### What problem does this PR solve?

Fix: rerank algorithm #11234

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-11-14 13:59:54 +08:00
5f59418aba Remove leftover account and password from the code (#11248)
Remove legacy accounts and passwords.

### What problem does this PR solve?

Remove leftover account and password in
agent/templates/sql_assistant.json

### Type of change

- [x] Other (please describe):
2025-11-14 13:59:03 +08:00
87e69868c0 Fixes: Added session variable types and modified configuration (#11269)
### What problem does this PR solve?

Fixes: Added session variable types and modified configuration

- Added more types of session variables
- Modified the embedding model switching logic in the knowledge base
configuration

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-11-14 13:56:56 +08:00
72c20022f6 Refactor service config fetching in admin server (#11267)
### What problem does this PR solve?

As title

### Type of change

- [x] Refactoring

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2025-11-14 12:32:08 +08:00
3f2472f1b9 Skip checking python comments 2025-11-14 11:59:15 +08:00
1d4d67daf8 Fix check_comment_ascii.py 2025-11-14 11:45:32 +08:00
7538e218a5 Fix check_comment_ascii.py 2025-11-14 11:32:55 +08:00
40 changed files with 1153 additions and 260 deletions

View File

@ -96,7 +96,7 @@ jobs:
args: "check" args: "check"
- name: Check comments of changed Python files - name: Check comments of changed Python files
if: ${{ !cancelled() && !failure() }} if: ${{ false }}
run: | run: |
if [[ ${{ github.event_name }} == 'pull_request_target' ]]; then if [[ ${{ github.event_name }} == 'pull_request_target' ]]; then
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} \ CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} \
@ -110,7 +110,7 @@ jobs:
for file in "${files[@]}"; do for file in "${files[@]}"; do
if [ -f "$file" ]; then if [ -f "$file" ]; then
if python3 check_comment_ascii.py $file"; then if python3 check_comment_ascii.py "$file"; then
echo "✅ $file" echo "✅ $file"
else else
echo "❌ $file" echo "❌ $file"

View File

@ -4,7 +4,7 @@
Admin Service is a dedicated management component designed to monitor, maintain, and administrate the RAGFlow system. It provides comprehensive tools for ensuring system stability, performing operational tasks, and managing users and permissions efficiently. Admin Service is a dedicated management component designed to monitor, maintain, and administrate the RAGFlow system. It provides comprehensive tools for ensuring system stability, performing operational tasks, and managing users and permissions efficiently.
The service offers real-time monitoring of critical components, including the RAGFlow server, Task Executor processes, and dependent services such as MySQL, Elasticsearch, Redis, and MinIO. It automatically checks their health status, resource usage, and uptime, and performs restarts in case of failures to minimize downtime. The service offers real-time monitoring of critical components, including the RAGFlow server, Task Executor processes, and dependent services such as MySQL, Infinity, Elasticsearch, Redis, and MinIO. It automatically checks their health status, resource usage, and uptime, and performs restarts in case of failures to minimize downtime.
For user and system management, it supports listing, creating, modifying, and deleting users and their associated resources like knowledge bases and Agents. For user and system management, it supports listing, creating, modifying, and deleting users and their associated resources like knowledge bases and Agents.

View File

@ -393,7 +393,9 @@ class AdminCLI(Cmd):
print(f"Can't access {self.host}, port: {self.port}") print(f"Can't access {self.host}, port: {self.port}")
def _format_service_detail_table(self, data): def _format_service_detail_table(self, data):
if not any([isinstance(v, list) for v in data.values()]): if isinstance(data, list):
return data
if not all([isinstance(v, list) for v in data.values()]):
# normal table # normal table
return data return data
# handle task_executor heartbeats map, for example {'name': [{'done': 2, 'now': timestamp1}, {'done': 3, 'now': timestamp2}] # handle task_executor heartbeats map, for example {'name': [{'done': 2, 'now': timestamp1}, {'done': 3, 'now': timestamp2}]
@ -404,7 +406,7 @@ class AdminCLI(Cmd):
task_executor_list.append({ task_executor_list.append({
"task_executor_name": k, "task_executor_name": k,
**heartbeats[0], **heartbeats[0],
}) } if heartbeats else {"task_executor_name": k})
return task_executor_list return task_executor_list
def _print_table_simple(self, data): def _print_table_simple(self, data):
@ -415,7 +417,8 @@ class AdminCLI(Cmd):
# handle single row data # handle single row data
data = [data] data = [data]
columns = list(data[0].keys()) columns = list(set().union(*(d.keys() for d in data)))
columns.sort()
col_widths = {} col_widths = {}
def get_string_width(text): def get_string_width(text):

View File

@ -169,7 +169,7 @@ def login_verify(f):
username = auth.parameters['username'] username = auth.parameters['username']
password = auth.parameters['password'] password = auth.parameters['password']
try: try:
if check_admin(username, password) is False: if not check_admin(username, password):
return jsonify({ return jsonify({
"code": 500, "code": 500,
"message": "Access denied", "message": "Access denied",

View File

@ -25,8 +25,21 @@ from common.config_utils import read_config
from urllib.parse import urlparse from urllib.parse import urlparse
class BaseConfig(BaseModel):
id: int
name: str
host: str
port: int
service_type: str
detail_func_name: str
def to_dict(self) -> dict[str, Any]:
return {'id': self.id, 'name': self.name, 'host': self.host, 'port': self.port,
'service_type': self.service_type}
class ServiceConfigs: class ServiceConfigs:
configs = dict configs = list[BaseConfig]
def __init__(self): def __init__(self):
self.configs = [] self.configs = []
@ -45,19 +58,6 @@ class ServiceType(Enum):
FILE_STORE = "file_store" FILE_STORE = "file_store"
class BaseConfig(BaseModel):
id: int
name: str
host: str
port: int
service_type: str
detail_func_name: str
def to_dict(self) -> dict[str, Any]:
return {'id': self.id, 'name': self.name, 'host': self.host, 'port': self.port,
'service_type': self.service_type}
class MetaConfig(BaseConfig): class MetaConfig(BaseConfig):
meta_type: str meta_type: str
@ -227,7 +227,7 @@ def load_configurations(config_path: str) -> list[BaseConfig]:
ragflow_count = 0 ragflow_count = 0
id_count = 0 id_count = 0
for k, v in raw_configs.items(): for k, v in raw_configs.items():
match (k): match k:
case "ragflow": case "ragflow":
name: str = f'ragflow_{ragflow_count}' name: str = f'ragflow_{ragflow_count}'
host: str = v['host'] host: str = v['host']

View File

@ -13,8 +13,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
import logging
import re import re
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from common.constants import ActiveEnum from common.constants import ActiveEnum
@ -190,7 +189,8 @@ class ServiceMgr:
config_dict['status'] = service_detail['status'] config_dict['status'] = service_detail['status']
else: else:
config_dict['status'] = 'timeout' config_dict['status'] = 'timeout'
except Exception: except Exception as e:
logging.warning(f"Can't get service details, error: {e}")
config_dict['status'] = 'timeout' config_dict['status'] = 'timeout'
if not config_dict['host']: if not config_dict['host']:
config_dict['host'] = '-' config_dict['host'] = '-'
@ -205,17 +205,13 @@ class ServiceMgr:
@staticmethod @staticmethod
def get_service_details(service_id: int): def get_service_details(service_id: int):
service_id = int(service_id) service_idx = int(service_id)
configs = SERVICE_CONFIGS.configs configs = SERVICE_CONFIGS.configs
service_config_mapping = { if service_idx < 0 or service_idx >= len(configs):
c.id: { raise AdminException(f"invalid service_index: {service_idx}")
'name': c.name,
'detail_func_name': c.detail_func_name service_config = configs[service_idx]
} for c in configs service_info = {'name': service_config.name, 'detail_func_name': service_config.detail_func_name}
}
service_info = service_config_mapping.get(service_id, {})
if not service_info:
raise AdminException(f"invalid service_id: {service_id}")
detail_func = getattr(health_utils, service_info.get('detail_func_name')) detail_func = getattr(health_utils, service_info.get('detail_func_name'))
res = detail_func() res = detail_func()

View File

@ -0,0 +1,149 @@
from abc import ABC
import os
from agent.component.base import ComponentBase, ComponentParamBase
from api.utils.api_utils import timeout
class ListOperationsParam(ComponentParamBase):
"""
Define the List Operations component parameters.
"""
def __init__(self):
super().__init__()
self.query = ""
self.operations = "topN"
self.n=0
self.sort_method = "asc"
self.filter = {
"operator": "=",
"value": ""
}
self.outputs = {
"result": {
"value": [],
"type": "Array of ?"
},
"first": {
"value": "",
"type": "?"
},
"last": {
"value": "",
"type": "?"
}
}
def check(self):
self.check_empty(self.query, "query")
self.check_valid_value(self.operations, "Support operations", ["topN","head","tail","filter","sort","drop_duplicates"])
def get_input_form(self) -> dict[str, dict]:
return {}
class ListOperations(ComponentBase,ABC):
component_name = "ListOperations"
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
def _invoke(self, **kwargs):
self.input_objects=[]
inputs = getattr(self._param, "query", None)
self.inputs=self._canvas.get_variable_value(inputs)
self.set_input_value(inputs, self.inputs)
if self._param.operations == "topN":
self._topN()
elif self._param.operations == "head":
self._head()
elif self._param.operations == "tail":
self._tail()
elif self._param.operations == "filter":
self._filter()
elif self._param.operations == "sort":
self._sort()
elif self._param.operations == "drop_duplicates":
self._drop_duplicates()
def _coerce_n(self):
try:
return int(getattr(self._param, "n", 0))
except Exception:
return 0
def _set_outputs(self, outputs):
self._param.outputs["result"]["value"] = outputs
self._param.outputs["first"]["value"] = outputs[0] if outputs else None
self._param.outputs["last"]["value"] = outputs[-1] if outputs else None
def _topN(self):
n = self._coerce_n()
if n < 1:
outputs = []
else:
n = min(n, len(self.inputs))
outputs = self.inputs[:n]
self._set_outputs(outputs)
def _head(self):
n = self._coerce_n()
if 1 <= n <= len(self.inputs):
outputs = [self.inputs[n - 1]]
else:
outputs = []
self._set_outputs(outputs)
def _tail(self):
n = self._coerce_n()
if 1 <= n <= len(self.inputs):
outputs = [self.inputs[-n]]
else:
outputs = []
self._set_outputs(outputs)
def _filter(self):
self._set_outputs([i for i in self.inputs if self._eval(self._norm(i),self._param.filter["operator"],self._param.filter["value"])])
def _norm(self,v):
s = "" if v is None else str(v)
return s
def _eval(self, v, operator, value):
if operator == "=":
return v == value
elif operator == "":
return v != value
elif operator == "contains":
return value in v
elif operator == "start with":
return v.startswith(value)
elif operator == "end with":
return v.endswith(value)
else:
return False
def _sort(self):
if self._param.sort_method == "asc":
self._set_outputs(sorted(self.inputs))
elif self._param.sort_method == "desc":
self._set_outputs(sorted(self.inputs, reverse=True))
def _drop_duplicates(self):
seen = set()
outs = []
for item in self.inputs:
k = self._hashable(item)
if k in seen:
continue
seen.add(k)
outs.append(item)
self._set_outputs(outs)
def _hashable(self,x):
if isinstance(x, dict):
return tuple(sorted((k, self._hashable(v)) for k, v in x.items()))
if isinstance(x, (list, tuple)):
return tuple(self._hashable(v) for v in x)
if isinstance(x, set):
return tuple(sorted(self._hashable(v) for v in x))
return x
def thoughts(self) -> str:
return "ListOperation in progress"

View File

@ -83,10 +83,10 @@
"value": [] "value": []
} }
}, },
"password": "20010812Yy!", "password": "",
"port": 3306, "port": 3306,
"sql": "{Agent:WickedGoatsDivide@content}", "sql": "{Agent:WickedGoatsDivide@content}",
"username": "13637682833@163.com" "username": ""
} }
}, },
"upstream": [ "upstream": [
@ -527,10 +527,10 @@
"value": [] "value": []
} }
}, },
"password": "20010812Yy!", "password": "",
"port": 3306, "port": 3306,
"sql": "{Agent:WickedGoatsDivide@content}", "sql": "{Agent:WickedGoatsDivide@content}",
"username": "13637682833@163.com" "username": ""
}, },
"label": "ExeSQL", "label": "ExeSQL",
"name": "ExeSQL" "name": "ExeSQL"

View File

@ -173,7 +173,8 @@ def check_task_executor_alive():
heartbeats = [json.loads(heartbeat) for heartbeat in heartbeats] heartbeats = [json.loads(heartbeat) for heartbeat in heartbeats]
task_executor_heartbeats[task_executor_id] = heartbeats task_executor_heartbeats[task_executor_id] = heartbeats
if task_executor_heartbeats: if task_executor_heartbeats:
return {"status": "alive", "message": task_executor_heartbeats} status = "alive" if any(task_executor_heartbeats.values()) else "timeout"
return {"status": status, "message": task_executor_heartbeats}
else: else:
return {"status": "timeout", "message": "Not found any task executor."} return {"status": "timeout", "message": "Not found any task executor."}
except Exception as e: except Exception as e:

View File

@ -1,16 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
Check whether given python files contain non-ASCII comments.
How to check the whole git repo:
```
$ git ls-files -z -- '*.py' | xargs -0 python3 check_comment_ascii.py
```
"""
import sys import sys
import tokenize import tokenize
import ast import ast
import pathlib import pathlib
import re import re
ASCII = re.compile(r"^[ -~]*\Z") # Only printable ASCII ASCII = re.compile(r"^[\n -~]*\Z") # Printable ASCII + newline
def check(src: str, name: str) -> int: def check(src: str, name: str) -> int:
""" """
I'm a docstring docstring line 1
docstring line 2
""" """
ok = 1 ok = 1
# A common comment begins with `#` # A common comment begins with `#`

View File

@ -434,7 +434,7 @@ class MinerUParser(RAGFlowPdfParser):
if not section.strip(): if not section.strip():
section = "FAILED TO PARSE TABLE" section = "FAILED TO PARSE TABLE"
case MinerUContentType.IMAGE: case MinerUContentType.IMAGE:
section = "".join(output["image_caption"]) + "\n" + "".join(output["image_footnote"]) section = "".join(output.get(["image_caption"],[])) + "\n" + "".join(output.get(["image_footnote"],[]))
case MinerUContentType.EQUATION: case MinerUContentType.EQUATION:
section = output["text"] section = output["text"]
case MinerUContentType.CODE: case MinerUContentType.CODE:

View File

@ -347,7 +347,7 @@ class Dealer:
## For rank feature(tag_fea) scores. ## For rank feature(tag_fea) scores.
rank_fea = self._rank_feature_scores(rank_feature, sres) rank_fea = self._rank_feature_scores(rank_feature, sres)
return tkweight * (np.array(tksim)+rank_fea) + vtweight * vtsim, tksim, vtsim return tkweight * np.array(tksim) + vtweight * vtsim + rank_fea, tksim, vtsim
def hybrid_similarity(self, ans_embd, ins_embd, ans, inst): def hybrid_similarity(self, ans_embd, ins_embd, ans, inst):
return self.qryr.hybrid_similarity(ans_embd, return self.qryr.hybrid_similarity(ans_embd,

View File

@ -110,7 +110,7 @@ class RedisDB:
info = self.REDIS.info() info = self.REDIS.info()
return { return {
'redis_version': info["redis_version"], 'redis_version': info["redis_version"],
'server_mode': info["server_mode"], 'server_mode': info["server_mode"] if "server_mode" in info else info.get("redis_mode", ""),
'used_memory': info["used_memory_human"], 'used_memory': info["used_memory_human"],
'total_system_memory': info["total_system_memory_human"], 'total_system_memory': info["total_system_memory_human"],
'mem_fragmentation_ratio': info["mem_fragmentation_ratio"], 'mem_fragmentation_ratio': info["mem_fragmentation_ratio"],

View File

@ -61,6 +61,12 @@ export interface FormFieldConfig {
horizontal?: boolean; horizontal?: boolean;
onChange?: (value: any) => void; onChange?: (value: any) => void;
tooltip?: React.ReactNode; tooltip?: React.ReactNode;
customValidate?: (
value: any,
formValues: any,
) => string | boolean | Promise<string | boolean>;
dependencies?: string[];
schema?: ZodSchema;
} }
// Component props interface // Component props interface
@ -94,36 +100,40 @@ const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
let fieldSchema: ZodSchema; let fieldSchema: ZodSchema;
// Create base validation schema based on field type // Create base validation schema based on field type
switch (field.type) { if (field.schema) {
case FormFieldType.Email: fieldSchema = field.schema;
fieldSchema = z.string().email('Please enter a valid email address'); } else {
break; switch (field.type) {
case FormFieldType.Number: case FormFieldType.Email:
fieldSchema = z.coerce.number(); fieldSchema = z.string().email('Please enter a valid email address');
if (field.validation?.min !== undefined) { break;
fieldSchema = (fieldSchema as z.ZodNumber).min( case FormFieldType.Number:
field.validation.min, fieldSchema = z.coerce.number();
field.validation.message || if (field.validation?.min !== undefined) {
`Value cannot be less than ${field.validation.min}`, fieldSchema = (fieldSchema as z.ZodNumber).min(
); field.validation.min,
} field.validation.message ||
if (field.validation?.max !== undefined) { `Value cannot be less than ${field.validation.min}`,
fieldSchema = (fieldSchema as z.ZodNumber).max( );
field.validation.max, }
field.validation.message || if (field.validation?.max !== undefined) {
`Value cannot be greater than ${field.validation.max}`, fieldSchema = (fieldSchema as z.ZodNumber).max(
); field.validation.max,
} field.validation.message ||
break; `Value cannot be greater than ${field.validation.max}`,
case FormFieldType.Checkbox: );
fieldSchema = z.boolean(); }
break; break;
case FormFieldType.Tag: case FormFieldType.Checkbox:
fieldSchema = z.array(z.string()); fieldSchema = z.boolean();
break; break;
default: case FormFieldType.Tag:
fieldSchema = z.string(); fieldSchema = z.array(z.string());
break; break;
default:
fieldSchema = z.string();
break;
}
} }
// Handle required fields // Handle required fields
@ -300,10 +310,90 @@ const DynamicForm = {
// Initialize form // Initialize form
const form = useForm<T>({ const form = useForm<T>({
resolver: zodResolver(schema), resolver: async (data, context, options) => {
const zodResult = await zodResolver(schema)(data, context, options);
let combinedErrors = { ...zodResult.errors };
const fieldErrors: Record<string, { type: string; message: string }> =
{};
for (const field of fields) {
if (field.customValidate && data[field.name] !== undefined) {
try {
const result = await field.customValidate(
data[field.name],
data,
);
if (typeof result === 'string') {
fieldErrors[field.name] = {
type: 'custom',
message: result,
};
} else if (result === false) {
fieldErrors[field.name] = {
type: 'custom',
message:
field.validation?.message || `${field.label} is invalid`,
};
}
} catch (error) {
fieldErrors[field.name] = {
type: 'custom',
message:
error instanceof Error
? error.message
: 'Validation failed',
};
}
}
}
combinedErrors = {
...combinedErrors,
...fieldErrors,
} as any;
console.log('combinedErrors', combinedErrors);
return {
values: Object.keys(combinedErrors).length ? {} : data,
errors: combinedErrors,
} as any;
},
defaultValues, defaultValues,
}); });
useEffect(() => {
const dependencyMap: Record<string, string[]> = {};
fields.forEach((field) => {
if (field.dependencies && field.dependencies.length > 0) {
field.dependencies.forEach((dep) => {
if (!dependencyMap[dep]) {
dependencyMap[dep] = [];
}
dependencyMap[dep].push(field.name);
});
}
});
const subscriptions = Object.keys(dependencyMap).map((depField) => {
return form.watch((values: any, { name }) => {
if (name === depField && dependencyMap[depField]) {
dependencyMap[depField].forEach((dependentField) => {
form.trigger(dependentField as any);
});
}
});
});
return () => {
subscriptions.forEach((sub) => {
if (sub.unsubscribe) {
sub.unsubscribe();
}
});
};
}, [fields, form]);
// Expose form methods via ref // Expose form methods via ref
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
submit: () => form.handleSubmit(onSubmit)(), submit: () => form.handleSubmit(onSubmit)(),

View File

@ -51,6 +51,7 @@ export interface SegmentedProps
direction?: 'ltr' | 'rtl'; direction?: 'ltr' | 'rtl';
motionName?: string; motionName?: string;
activeClassName?: string; activeClassName?: string;
itemClassName?: string;
rounded?: keyof typeof segmentedVariants.round; rounded?: keyof typeof segmentedVariants.round;
sizeType?: keyof typeof segmentedVariants.size; sizeType?: keyof typeof segmentedVariants.size;
buttonSize?: keyof typeof segmentedVariants.buttonSize; buttonSize?: keyof typeof segmentedVariants.buttonSize;
@ -62,6 +63,7 @@ export function Segmented({
onChange, onChange,
className, className,
activeClassName, activeClassName,
itemClassName,
rounded = 'default', rounded = 'default',
sizeType = 'default', sizeType = 'default',
buttonSize = 'default', buttonSize = 'default',
@ -92,12 +94,13 @@ export function Segmented({
<div <div
key={actualValue} key={actualValue}
className={cn( className={cn(
'inline-flex items-center text-base font-normal cursor-pointer', 'inline-flex items-center text-base font-normal cursor-pointer',
segmentedVariants.round[rounded], segmentedVariants.round[rounded],
segmentedVariants.buttonSize[buttonSize], segmentedVariants.buttonSize[buttonSize],
{ {
'text-text-primary bg-bg-base': selectedValue === actualValue, 'text-text-primary bg-bg-base': selectedValue === actualValue,
}, },
itemClassName,
activeClassName && selectedValue === actualValue activeClassName && selectedValue === actualValue
? activeClassName ? activeClassName
: '', : '',

View File

@ -109,6 +109,7 @@ export enum Operator {
SearXNG = 'SearXNG', SearXNG = 'SearXNG',
Placeholder = 'Placeholder', Placeholder = 'Placeholder',
DataOperations = 'DataOperations', DataOperations = 'DataOperations',
ListOperations = 'ListOperations',
VariableAssigner = 'VariableAssigner', VariableAssigner = 'VariableAssigner',
VariableAggregator = 'VariableAggregator', VariableAggregator = 'VariableAggregator',
File = 'File', // pipeline File = 'File', // pipeline

View File

@ -1009,6 +1009,7 @@ Example: general/v2/`,
pleaseUploadAtLeastOneFile: 'Please upload at least one file', pleaseUploadAtLeastOneFile: 'Please upload at least one file',
}, },
flow: { flow: {
formatTypeError: 'Format or type error',
variableNameMessage: variableNameMessage:
'Variable name can only contain letters and underscores', 'Variable name can only contain letters and underscores',
variableDescription: 'Variable Description', variableDescription: 'Variable Description',
@ -1590,6 +1591,8 @@ This delimiter is used to split the input text into several text pieces echo of
codeDescription: 'It allows developers to write custom Python logic.', codeDescription: 'It allows developers to write custom Python logic.',
dataOperations: 'Data operations', dataOperations: 'Data operations',
dataOperationsDescription: 'Perform various operations on a Data object.', dataOperationsDescription: 'Perform various operations on a Data object.',
listOperations: 'List operations',
listOperationsDescription: 'Perform operations on a list.',
variableAssigner: 'Variable assigner', variableAssigner: 'Variable assigner',
variableAssignerDescription: variableAssignerDescription:
'This component performs operations on Data objects, including extracting, filtering, and editing keys and values in the Data.', 'This component performs operations on Data objects, including extracting, filtering, and editing keys and values in the Data.',
@ -1805,6 +1808,19 @@ Important structured information may include: names, dates, locations, events, k
removeKeys: 'Remove keys', removeKeys: 'Remove keys',
renameKeys: 'Rename keys', renameKeys: 'Rename keys',
}, },
ListOperationsOptions: {
topN: 'Top N',
head: 'Head',
tail: 'Tail',
sort: 'Sort',
filter: 'Filter',
dropDuplicates: 'Drop duplicates',
},
sortMethod: 'Sort method',
SortMethodOptions: {
asc: 'Ascending',
desc: 'Descending',
},
}, },
llmTools: { llmTools: {
bad_calculator: { bad_calculator: {

View File

@ -956,6 +956,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
pleaseUploadAtLeastOneFile: '请上传至少一个文件', pleaseUploadAtLeastOneFile: '请上传至少一个文件',
}, },
flow: { flow: {
formatTypeError: '格式或类型错误',
variableNameMessage: '名称只能包含字母和下划线', variableNameMessage: '名称只能包含字母和下划线',
variableDescription: '变量的描述', variableDescription: '变量的描述',
defaultValue: '默认值', defaultValue: '默认值',
@ -1507,6 +1508,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
codeDescription: '它允许开发人员编写自定义 Python 逻辑。', codeDescription: '它允许开发人员编写自定义 Python 逻辑。',
dataOperations: '数据操作', dataOperations: '数据操作',
dataOperationsDescription: '对数据对象执行各种操作。', dataOperationsDescription: '对数据对象执行各种操作。',
listOperations: '列表操作',
listOperationsDescription: '对列表对象执行各种操作。',
variableAssigner: '变量赋值器', variableAssigner: '变量赋值器',
variableAssignerDescription: variableAssignerDescription:
'此组件对数据对象执行操作,包括提取、筛选和编辑数据中的键和值。', '此组件对数据对象执行操作,包括提取、筛选和编辑数据中的键和值。',
@ -1678,6 +1681,19 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
removeKeys: '删除键', removeKeys: '删除键',
renameKeys: '重命名键', renameKeys: '重命名键',
}, },
ListOperationsOptions: {
topN: '取前N项',
head: '取前第N项',
tail: '取后第N项',
sort: '排序',
filter: '筛选',
dropDuplicates: '去重',
},
sortMethod: '排序方式',
SortMethodOptions: {
asc: '升序',
desc: '降序',
},
}, },
footer: { footer: {
profile: 'All rights reserved @ React', profile: 'All rights reserved @ React',

View File

@ -61,6 +61,7 @@ import { FileNode } from './node/file-node';
import { InvokeNode } from './node/invoke-node'; import { InvokeNode } from './node/invoke-node';
import { IterationNode, IterationStartNode } from './node/iteration-node'; import { IterationNode, IterationStartNode } from './node/iteration-node';
import { KeywordNode } from './node/keyword-node'; import { KeywordNode } from './node/keyword-node';
import { ListOperationsNode } from './node/list-operations-node';
import { MessageNode } from './node/message-node'; import { MessageNode } from './node/message-node';
import NoteNode from './node/note-node'; import NoteNode from './node/note-node';
import ParserNode from './node/parser-node'; import ParserNode from './node/parser-node';
@ -101,6 +102,7 @@ export const nodeTypes: NodeTypes = {
splitterNode: SplitterNode, splitterNode: SplitterNode,
contextNode: ExtractorNode, contextNode: ExtractorNode,
dataOperationsNode: DataOperationsNode, dataOperationsNode: DataOperationsNode,
listOperationsNode: ListOperationsNode,
variableAssignerNode: VariableAssignerNode, variableAssignerNode: VariableAssignerNode,
variableAggregatorNode: VariableAggregatorNode, variableAggregatorNode: VariableAggregatorNode,
}; };

View File

@ -79,6 +79,7 @@ export function AccordionOperators({
Operator.Code, Operator.Code,
Operator.StringTransform, Operator.StringTransform,
Operator.DataOperations, Operator.DataOperations,
Operator.ListOperations,
// Operator.VariableAssigner, // Operator.VariableAssigner,
Operator.VariableAggregator, Operator.VariableAggregator,
]} ]}

View File

@ -0,0 +1,22 @@
import { BaseNode } from '@/interfaces/database/agent';
import { NodeProps } from '@xyflow/react';
import { camelCase } from 'lodash';
import { useTranslation } from 'react-i18next';
import { RagNode } from '.';
import { ListOperationsFormSchemaType } from '../../form/list-operations-form';
import { LabelCard } from './card';
export function ListOperationsNode({
...props
}: NodeProps<BaseNode<ListOperationsFormSchemaType>>) {
const { data } = props;
const { t } = useTranslation();
return (
<RagNode {...props}>
<LabelCard>
{t(`flow.ListOperationsOptions.${camelCase(data.form?.operations)}`)}
</LabelCard>
</RagNode>
);
}

View File

@ -595,6 +595,35 @@ export const initialDataOperationsValues = {
}, },
}, },
}; };
export enum SortMethod {
Asc = 'asc',
Desc = 'desc',
}
export enum ListOperations {
TopN = 'topN',
Head = 'head',
Tail = 'tail',
Filter = 'filter',
Sort = 'sort',
DropDuplicates = 'drop_duplicates',
}
export const initialListOperationsValues = {
query: '',
operations: ListOperations.TopN,
outputs: {
result: {
type: 'Array<?>',
},
first: {
type: '?',
},
last: {
type: '?',
},
},
};
export const initialVariableAssignerValues = {}; export const initialVariableAssignerValues = {};
@ -673,6 +702,7 @@ export const RestrictedUpstreamMap = {
[Operator.Tool]: [Operator.Begin], [Operator.Tool]: [Operator.Begin],
[Operator.Placeholder]: [Operator.Begin], [Operator.Placeholder]: [Operator.Begin],
[Operator.DataOperations]: [Operator.Begin], [Operator.DataOperations]: [Operator.Begin],
[Operator.ListOperations]: [Operator.Begin],
[Operator.Parser]: [Operator.Begin], // pipeline [Operator.Parser]: [Operator.Begin], // pipeline
[Operator.Splitter]: [Operator.Begin], [Operator.Splitter]: [Operator.Begin],
[Operator.HierarchicalMerger]: [Operator.Begin], [Operator.HierarchicalMerger]: [Operator.Begin],
@ -729,6 +759,7 @@ export const NodeMap = {
[Operator.HierarchicalMerger]: 'splitterNode', [Operator.HierarchicalMerger]: 'splitterNode',
[Operator.Extractor]: 'contextNode', [Operator.Extractor]: 'contextNode',
[Operator.DataOperations]: 'dataOperationsNode', [Operator.DataOperations]: 'dataOperationsNode',
[Operator.ListOperations]: 'listOperationsNode',
[Operator.VariableAssigner]: 'variableAssignerNode', [Operator.VariableAssigner]: 'variableAssignerNode',
[Operator.VariableAggregator]: 'variableAggregatorNode', [Operator.VariableAggregator]: 'variableAggregatorNode',
}; };

View File

@ -21,6 +21,7 @@ import IterationForm from '../form/iteration-form';
import IterationStartForm from '../form/iteration-start-from'; import IterationStartForm from '../form/iteration-start-from';
import Jin10Form from '../form/jin10-form'; import Jin10Form from '../form/jin10-form';
import KeywordExtractForm from '../form/keyword-extract-form'; import KeywordExtractForm from '../form/keyword-extract-form';
import ListOperationsForm from '../form/list-operations-form';
import MessageForm from '../form/message-form'; import MessageForm from '../form/message-form';
import ParserForm from '../form/parser-form'; import ParserForm from '../form/parser-form';
import PubMedForm from '../form/pubmed-form'; import PubMedForm from '../form/pubmed-form';
@ -184,6 +185,9 @@ export const FormConfigMap = {
[Operator.DataOperations]: { [Operator.DataOperations]: {
component: DataOperationsForm, component: DataOperationsForm,
}, },
[Operator.ListOperations]: {
component: ListOperationsForm,
},
[Operator.VariableAssigner]: { [Operator.VariableAssigner]: {
component: VariableAssignerForm, component: VariableAssignerForm,
}, },

View File

@ -0,0 +1,140 @@
import NumberInput from '@/components/originui/number-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Separator } from '@/components/ui/separator';
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
DataOperationsOperatorOptions,
JsonSchemaDataType,
ListOperations,
SortMethod,
initialListOperationsValues,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output, OutputSchema } from '../components/output';
import { PromptEditor } from '../components/prompt-editor';
import { QueryVariable } from '../components/query-variable';
export const RetrievalPartialSchema = {
query: z.string(),
operations: z.string(),
n: z.number().int().min(0).optional(),
sort_method: z.string().optional(),
filter: z
.object({
value: z.string().optional(),
operator: z.string().optional(),
})
.optional(),
...OutputSchema,
};
export const FormSchema = z.object(RetrievalPartialSchema);
export type ListOperationsFormSchemaType = z.infer<typeof FormSchema>;
const outputList = buildOutputList(initialListOperationsValues.outputs);
function ListOperationsForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const defaultValues = useFormValues(initialListOperationsValues, node);
const form = useForm<ListOperationsFormSchemaType>({
defaultValues: defaultValues,
mode: 'onChange',
resolver: zodResolver(FormSchema),
shouldUnregister: true,
});
const operations = useWatch({ control: form.control, name: 'operations' });
const ListOperationsOptions = buildOptions(
ListOperations,
t,
`flow.ListOperationsOptions`,
true,
);
const SortMethodOptions = buildOptions(
SortMethod,
t,
`flow.SortMethodOptions`,
true,
);
const operatorOptions = useBuildSwitchOperatorOptions(
DataOperationsOperatorOptions,
);
useWatchFormChange(node?.id, form, true);
return (
<Form {...form}>
<FormWrapper>
<QueryVariable
name="query"
className="flex-1"
types={[JsonSchemaDataType.Array]}
></QueryVariable>
<Separator />
<RAGFlowFormItem name="operations" label={t('flow.operations')}>
<SelectWithSearch options={ListOperationsOptions} />
</RAGFlowFormItem>
{[
ListOperations.TopN,
ListOperations.Head,
ListOperations.Tail,
].includes(operations as ListOperations) && (
<FormField
control={form.control}
name="n"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flowNum')}</FormLabel>
<FormControl>
<NumberInput {...field} className="w-full"></NumberInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{[ListOperations.Sort].includes(operations as ListOperations) && (
<RAGFlowFormItem name="sort_method" label={t('flow.sortMethod')}>
<SelectWithSearch options={SortMethodOptions} />
</RAGFlowFormItem>
)}
{[ListOperations.Filter].includes(operations as ListOperations) && (
<div className="flex items-center gap-2">
<RAGFlowFormItem name="filter.operator" className="flex-1">
<SelectWithSearch options={operatorOptions}></SelectWithSearch>
</RAGFlowFormItem>
<Separator className="w-2" />
<RAGFlowFormItem name="filter.value" className="flex-1">
<PromptEditor showToolbar={false} multiLine={false} />
</RAGFlowFormItem>
</div>
)}
<Output list={outputList} isFormRequired></Output>
</FormWrapper>
</Form>
);
}
export default memo(ListOperationsForm);

View File

@ -0,0 +1,134 @@
import {
DynamicForm,
DynamicFormRef,
FormFieldConfig,
} from '@/components/dynamic-form';
import { Modal } from '@/components/ui/modal/modal';
import { t } from 'i18next';
import { useEffect, useRef } from 'react';
import { FieldValues } from 'react-hook-form';
import { TypeMaps, TypesWithArray } from '../constant';
import { useHandleForm } from '../hooks/use-form';
import { useObjectFields } from '../hooks/use-object-fields';
export const AddVariableModal = (props: {
fields?: FormFieldConfig[];
setFields: (value: any) => void;
visible?: boolean;
hideModal: () => void;
defaultValues?: FieldValues;
setDefaultValues?: (value: FieldValues) => void;
}) => {
const {
fields,
setFields,
visible,
hideModal,
defaultValues,
setDefaultValues,
} = props;
const { handleSubmit: submitForm, loading } = useHandleForm();
const { handleCustomValidate, handleCustomSchema, handleRender } =
useObjectFields();
const formRef = useRef<DynamicFormRef>(null);
const handleFieldUpdate = (
fieldName: string,
updatedField: Partial<FormFieldConfig>,
) => {
setFields((prevFields: any) =>
prevFields.map((field: any) =>
field.name === fieldName ? { ...field, ...updatedField } : field,
),
);
};
useEffect(() => {
const typeField = fields?.find((item) => item.name === 'type');
if (typeField) {
typeField.onChange = (value) => {
handleFieldUpdate('value', {
type: TypeMaps[value as keyof typeof TypeMaps],
render: handleRender(value),
customValidate: handleCustomValidate(value),
schema: handleCustomSchema(value),
});
const values = formRef.current?.getValues();
// setTimeout(() => {
switch (value) {
case TypesWithArray.Boolean:
setDefaultValues?.({ ...values, value: false });
break;
case TypesWithArray.Number:
setDefaultValues?.({ ...values, value: 0 });
break;
case TypesWithArray.Object:
setDefaultValues?.({ ...values, value: {} });
break;
case TypesWithArray.ArrayString:
setDefaultValues?.({ ...values, value: [''] });
break;
case TypesWithArray.ArrayNumber:
setDefaultValues?.({ ...values, value: [''] });
break;
case TypesWithArray.ArrayBoolean:
setDefaultValues?.({ ...values, value: [false] });
break;
case TypesWithArray.ArrayObject:
setDefaultValues?.({ ...values, value: [] });
break;
default:
setDefaultValues?.({ ...values, value: '' });
break;
}
// }, 0);
};
}
}, [fields]);
const handleSubmit = async (fieldValue: FieldValues) => {
await submitForm(fieldValue);
hideModal();
};
return (
<Modal
title={t('flow.add') + t('flow.conversationVariable')}
open={visible || false}
onCancel={hideModal}
showfooter={false}
>
<DynamicForm.Root
ref={formRef}
fields={fields || []}
onSubmit={(data) => {
console.log(data);
}}
defaultValues={defaultValues}
onFieldUpdate={handleFieldUpdate}
>
<div className="flex items-center justify-end w-full gap-2">
<DynamicForm.CancelButton
handleCancel={() => {
hideModal?.();
}}
/>
<DynamicForm.SavingButton
submitLoading={loading || false}
buttonText={t('common.ok')}
submitFunc={(values: FieldValues) => {
handleSubmit(values);
// console.log(values);
// console.log(nodes, edges);
// handleOk(values);
}}
/>
</div>
</DynamicForm.Root>
</Modal>
);
};

View File

@ -13,14 +13,14 @@ export enum TypesWithArray {
String = 'string', String = 'string',
Number = 'number', Number = 'number',
Boolean = 'boolean', Boolean = 'boolean',
// Object = 'object', Object = 'object',
// ArrayString = 'array<string>', ArrayString = 'array<string>',
// ArrayNumber = 'array<number>', ArrayNumber = 'array<number>',
// ArrayBoolean = 'array<boolean>', ArrayBoolean = 'array<boolean>',
// ArrayObject = 'array<object>', ArrayObject = 'array<object>',
} }
export const GobalFormFields = [ export const GlobalFormFields = [
{ {
label: t('flow.name'), label: t('flow.name'),
name: 'name', name: 'name',
@ -50,11 +50,11 @@ export const GobalFormFields = [
label: t('flow.description'), label: t('flow.description'),
name: 'description', name: 'description',
placeholder: t('flow.variableDescription'), placeholder: t('flow.variableDescription'),
type: 'textarea', type: FormFieldType.Textarea,
}, },
] as FormFieldConfig[]; ] as FormFieldConfig[];
export const GobalVariableFormDefaultValues = { export const GlobalVariableFormDefaultValues = {
name: '', name: '',
type: TypesWithArray.String, type: TypesWithArray.String,
value: '', value: '',
@ -65,9 +65,9 @@ export const TypeMaps = {
[TypesWithArray.String]: FormFieldType.Textarea, [TypesWithArray.String]: FormFieldType.Textarea,
[TypesWithArray.Number]: FormFieldType.Number, [TypesWithArray.Number]: FormFieldType.Number,
[TypesWithArray.Boolean]: FormFieldType.Checkbox, [TypesWithArray.Boolean]: FormFieldType.Checkbox,
// [TypesWithArray.Object]: FormFieldType.Textarea, [TypesWithArray.Object]: FormFieldType.Textarea,
// [TypesWithArray.ArrayString]: FormFieldType.Textarea, [TypesWithArray.ArrayString]: FormFieldType.Textarea,
// [TypesWithArray.ArrayNumber]: FormFieldType.Textarea, [TypesWithArray.ArrayNumber]: FormFieldType.Textarea,
// [TypesWithArray.ArrayBoolean]: FormFieldType.Textarea, [TypesWithArray.ArrayBoolean]: FormFieldType.Textarea,
// [TypesWithArray.ArrayObject]: FormFieldType.Textarea, [TypesWithArray.ArrayObject]: FormFieldType.Textarea,
}; };

View File

@ -0,0 +1,41 @@
import { useFetchAgent } from '@/hooks/use-agent-request';
import { GlobalVariableType } from '@/interfaces/database/agent';
import { useCallback } from 'react';
import { FieldValues } from 'react-hook-form';
import { useSaveGraph } from '../../hooks/use-save-graph';
import { TypesWithArray } from '../constant';
export const useHandleForm = () => {
const { data, refetch } = useFetchAgent();
const { saveGraph, loading } = useSaveGraph();
const handleObjectData = (value: any) => {
try {
return JSON.parse(value);
} catch (error) {
return value;
}
};
const handleSubmit = useCallback(async (fieldValue: FieldValues) => {
const param = {
...(data.dsl?.variables || {}),
[fieldValue.name]: {
...fieldValue,
value:
fieldValue.type === TypesWithArray.Object ||
fieldValue.type === TypesWithArray.ArrayObject
? handleObjectData(fieldValue.value)
: fieldValue.value,
},
} as Record<string, GlobalVariableType>;
const res = await saveGraph(undefined, {
globalVariables: param,
});
if (res.code === 0) {
refetch();
}
}, []);
return { handleSubmit, loading };
};

View File

@ -0,0 +1,246 @@
import { BlockButton, Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Segmented } from '@/components/ui/segmented';
import { Editor } from '@monaco-editor/react';
import { t } from 'i18next';
import { Trash2, X } from 'lucide-react';
import { useCallback } from 'react';
import { FieldValues } from 'react-hook-form';
import { z } from 'zod';
import { TypesWithArray } from '../constant';
export const useObjectFields = () => {
const booleanRender = useCallback(
(field: FieldValues, className?: string) => {
const fieldValue = field.value ? true : false;
return (
<Segmented
options={
[
{ value: true, label: 'True' },
{ value: false, label: 'False' },
] as any
}
sizeType="sm"
value={fieldValue}
onChange={field.onChange}
className={className}
itemClassName="justify-center flex-1"
></Segmented>
);
},
[],
);
const objectRender = useCallback((field: FieldValues) => {
const fieldValue =
typeof field.value === 'object'
? JSON.stringify(field.value, null, 2)
: JSON.stringify({}, null, 2);
console.log('object-render-field', field, fieldValue);
return (
<Editor
height={200}
defaultLanguage="json"
theme="vs-dark"
value={fieldValue}
onChange={field.onChange}
/>
);
}, []);
const objectValidate = useCallback((value: any) => {
try {
if (!JSON.parse(value)) {
throw new Error(t('knowledgeDetails.formatTypeError'));
}
return true;
} catch (e) {
throw new Error(t('knowledgeDetails.formatTypeError'));
}
}, []);
const arrayStringRender = useCallback((field: FieldValues, type = 'text') => {
const values = Array.isArray(field.value)
? field.value
: [type === 'number' ? 0 : ''];
return (
<>
{values?.map((item: any, index: number) => (
<div key={index} className="flex gap-1 items-center">
<Input
type={type}
value={item}
onChange={(e) => {
const newValues = [...values];
newValues[index] = e.target.value;
field.onChange(newValues);
}}
/>
<Button
variant={'secondary'}
onClick={() => {
const newValues = [...values];
newValues.splice(index, 1);
field.onChange(newValues);
}}
>
<Trash2 />
</Button>
</div>
))}
<BlockButton
type="button"
onClick={() => {
field.onChange([...field.value, '']);
}}
>
{t('flow.add')}
</BlockButton>
</>
);
}, []);
const arrayBooleanRender = useCallback(
(field: FieldValues) => {
// const values = field.value || [false];
const values = Array.isArray(field.value) ? field.value : [false];
return (
<div className="flex items-center gap-1 flex-wrap ">
{values?.map((item: any, index: number) => (
<div
key={index}
className="flex gap-1 items-center bg-bg-card rounded-lg border-[0.5px] border-border-button"
>
{booleanRender(
{
value: item,
onChange: (value) => {
values[index] = !!value;
field.onChange(values);
},
},
'bg-transparent',
)}
<Button
variant={'transparent'}
className="border-none py-0 px-1"
onClick={() => {
const newValues = [...values];
newValues.splice(index, 1);
field.onChange(newValues);
}}
>
<X />
</Button>
</div>
))}
<BlockButton
className="w-auto"
type="button"
onClick={() => {
field.onChange([...field.value, false]);
}}
>
{t('flow.add')}
</BlockButton>
</div>
);
},
[booleanRender],
);
const arrayNumberRender = useCallback(
(field: FieldValues) => {
return arrayStringRender(field, 'number');
},
[arrayStringRender],
);
const arrayValidate = useCallback((value: any, type: string = 'string') => {
if (!Array.isArray(value) || !value.every((item) => typeof item === type)) {
throw new Error(t('flow.formatTypeError'));
}
return true;
}, []);
const arrayStringValidate = useCallback(
(value: any) => {
return arrayValidate(value, 'string');
},
[arrayValidate],
);
const arrayNumberValidate = useCallback(
(value: any) => {
return arrayValidate(value, 'number');
},
[arrayValidate],
);
const arrayBooleanValidate = useCallback(
(value: any) => {
return arrayValidate(value, 'boolean');
},
[arrayValidate],
);
const handleRender = (value: TypesWithArray) => {
switch (value) {
case TypesWithArray.Boolean:
return booleanRender;
case TypesWithArray.Object:
case TypesWithArray.ArrayObject:
return objectRender;
case TypesWithArray.ArrayString:
return arrayStringRender;
case TypesWithArray.ArrayNumber:
return arrayNumberRender;
case TypesWithArray.ArrayBoolean:
return arrayBooleanRender;
default:
return undefined;
}
};
const handleCustomValidate = (value: TypesWithArray) => {
switch (value) {
case TypesWithArray.Object:
case TypesWithArray.ArrayObject:
return objectValidate;
case TypesWithArray.ArrayString:
return arrayStringValidate;
case TypesWithArray.ArrayNumber:
return arrayNumberValidate;
case TypesWithArray.ArrayBoolean:
return arrayBooleanValidate;
default:
return undefined;
}
};
const handleCustomSchema = (value: TypesWithArray) => {
switch (value) {
case TypesWithArray.ArrayString:
return z.array(z.string());
case TypesWithArray.ArrayNumber:
return z.array(z.number());
case TypesWithArray.ArrayBoolean:
return z.array(z.boolean());
default:
return undefined;
}
};
return {
objectRender,
objectValidate,
arrayStringRender,
arrayStringValidate,
arrayNumberRender,
booleanRender,
arrayBooleanRender,
arrayNumberValidate,
arrayBooleanValidate,
handleRender,
handleCustomValidate,
handleCustomSchema,
};
};

View File

@ -1,12 +1,6 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { import { FormFieldConfig } from '@/components/dynamic-form';
DynamicForm,
DynamicFormRef,
FormFieldConfig,
FormFieldType,
} from '@/components/dynamic-form';
import { BlockButton, Button } from '@/components/ui/button'; import { BlockButton, Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal';
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
@ -19,117 +13,65 @@ import { GlobalVariableType } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { t } from 'i18next'; import { t } from 'i18next';
import { Trash2 } from 'lucide-react'; import { Trash2 } from 'lucide-react';
import { useEffect, useRef, useState } from 'react'; import { useState } from 'react';
import { FieldValues } from 'react-hook-form'; import { FieldValues } from 'react-hook-form';
import { useSaveGraph } from '../hooks/use-save-graph'; import { useSaveGraph } from '../hooks/use-save-graph';
import { AddVariableModal } from './component/add-variable-modal';
import { import {
GobalFormFields, GlobalFormFields,
GobalVariableFormDefaultValues, GlobalVariableFormDefaultValues,
TypeMaps, TypeMaps,
TypesWithArray, TypesWithArray,
} from './contant'; } from './constant';
import { useObjectFields } from './hooks/use-object-fields';
export type IGobalParamModalProps = { export type IGlobalParamModalProps = {
data: any; data: any;
hideModal: (open: boolean) => void; hideModal: (open: boolean) => void;
}; };
export const GobalParamSheet = (props: IGobalParamModalProps) => { export const GlobalParamSheet = (props: IGlobalParamModalProps) => {
const { hideModal } = props; const { hideModal } = props;
const { data, refetch } = useFetchAgent(); const { data, refetch } = useFetchAgent();
const [fields, setFields] = useState<FormFieldConfig[]>(GobalFormFields);
const { visible, showModal, hideModal: hideAddModal } = useSetModalState(); const { visible, showModal, hideModal: hideAddModal } = useSetModalState();
const [fields, setFields] = useState<FormFieldConfig[]>(GlobalFormFields);
const [defaultValues, setDefaultValues] = useState<FieldValues>( const [defaultValues, setDefaultValues] = useState<FieldValues>(
GobalVariableFormDefaultValues, GlobalVariableFormDefaultValues,
); );
const formRef = useRef<DynamicFormRef>(null); const { handleCustomValidate, handleCustomSchema, handleRender } =
useObjectFields();
const { saveGraph } = useSaveGraph();
const handleFieldUpdate = ( const handleDeleteGlobalVariable = async (key: string) => {
fieldName: string,
updatedField: Partial<FormFieldConfig>,
) => {
setFields((prevFields) =>
prevFields.map((field) =>
field.name === fieldName ? { ...field, ...updatedField } : field,
),
);
};
useEffect(() => {
const typefileld = fields.find((item) => item.name === 'type');
if (typefileld) {
typefileld.onChange = (value) => {
// setWatchType(value);
handleFieldUpdate('value', {
type: TypeMaps[value as keyof typeof TypeMaps],
});
const values = formRef.current?.getValues();
setTimeout(() => {
switch (value) {
case TypesWithArray.Boolean:
setDefaultValues({ ...values, value: false });
break;
case TypesWithArray.Number:
setDefaultValues({ ...values, value: 0 });
break;
default:
setDefaultValues({ ...values, value: '' });
}
}, 0);
};
}
}, [fields]);
const { saveGraph, loading } = useSaveGraph();
const handleSubmit = async (value: FieldValues) => {
const param = {
...(data.dsl?.variables || {}),
[value.name]: value,
} as Record<string, GlobalVariableType>;
const res = await saveGraph(undefined, {
gobalVariables: param,
});
if (res.code === 0) {
refetch();
}
hideAddModal();
};
const handleDeleteGobalVariable = async (key: string) => {
const param = { const param = {
...(data.dsl?.variables || {}), ...(data.dsl?.variables || {}),
} as Record<string, GlobalVariableType>; } as Record<string, GlobalVariableType>;
delete param[key]; delete param[key];
const res = await saveGraph(undefined, { const res = await saveGraph(undefined, {
gobalVariables: param, globalVariables: param,
}); });
console.log('delete gobal variable-->', res);
if (res.code === 0) { if (res.code === 0) {
refetch(); refetch();
} }
}; };
const handleEditGobalVariable = (item: FieldValues) => { const handleEditGlobalVariable = (item: FieldValues) => {
fields.forEach((field) => { const newFields = fields.map((field) => {
if (field.name === 'value') { let newField = field;
switch (item.type) { newField.render = undefined;
// [TypesWithArray.String]: FormFieldType.Textarea, newField.schema = undefined;
// [TypesWithArray.Number]: FormFieldType.Number, newField.customValidate = undefined;
// [TypesWithArray.Boolean]: FormFieldType.Checkbox, if (newField.name === 'value') {
case TypesWithArray.Boolean: newField = {
field.type = FormFieldType.Checkbox; ...newField,
break; type: TypeMaps[item.type as keyof typeof TypeMaps],
case TypesWithArray.Number: render: handleRender(item.type),
field.type = FormFieldType.Number; customValidate: handleCustomValidate(item.type),
break; schema: handleCustomSchema(item.type),
default: };
field.type = FormFieldType.Textarea;
}
} }
return newField;
}); });
setFields(newFields);
setDefaultValues(item); setDefaultValues(item);
showModal(); showModal();
}; };
@ -149,8 +91,8 @@ export const GobalParamSheet = (props: IGobalParamModalProps) => {
<div className="px-5 pb-5"> <div className="px-5 pb-5">
<BlockButton <BlockButton
onClick={() => { onClick={() => {
setFields(GobalFormFields); setFields(GlobalFormFields);
setDefaultValues(GobalVariableFormDefaultValues); setDefaultValues(GlobalVariableFormDefaultValues);
showModal(); showModal();
}} }}
> >
@ -167,7 +109,7 @@ export const GobalParamSheet = (props: IGobalParamModalProps) => {
key={key} key={key}
className="flex items-center gap-3 min-h-14 justify-between px-5 py-3 border border-border-default rounded-lg hover:bg-bg-card group" className="flex items-center gap-3 min-h-14 justify-between px-5 py-3 border border-border-default rounded-lg hover:bg-bg-card group"
onClick={() => { onClick={() => {
handleEditGobalVariable(item); handleEditGlobalVariable(item);
}} }}
> >
<div className="flex flex-col"> <div className="flex flex-col">
@ -177,13 +119,23 @@ export const GobalParamSheet = (props: IGobalParamModalProps) => {
{item.type} {item.type}
</span> </span>
</div> </div>
<div> {![
<span className="text-text-primary">{item.value}</span> TypesWithArray.Object,
</div> TypesWithArray.ArrayObject,
TypesWithArray.ArrayString,
TypesWithArray.ArrayNumber,
TypesWithArray.ArrayBoolean,
].includes(item.type as TypesWithArray) && (
<div>
<span className="text-text-primary">
{item.value}
</span>
</div>
)}
</div> </div>
<div> <div>
<ConfirmDeleteDialog <ConfirmDeleteDialog
onOk={() => handleDeleteGobalVariable(key)} onOk={() => handleDeleteGlobalVariable(key)}
> >
<Button <Button
variant={'secondary'} variant={'secondary'}
@ -201,40 +153,14 @@ export const GobalParamSheet = (props: IGobalParamModalProps) => {
})} })}
</div> </div>
</SheetContent> </SheetContent>
<Modal <AddVariableModal
title={t('flow.add') + t('flow.conversationVariable')} visible={visible}
open={visible} hideModal={hideAddModal}
onCancel={hideAddModal} fields={fields}
showfooter={false} setFields={setFields}
> defaultValues={defaultValues}
<DynamicForm.Root setDefaultValues={setDefaultValues}
ref={formRef} />
fields={fields}
onSubmit={(data) => {
console.log(data);
}}
defaultValues={defaultValues}
onFieldUpdate={handleFieldUpdate}
>
<div className="flex items-center justify-end w-full gap-2">
<DynamicForm.CancelButton
handleCancel={() => {
hideAddModal?.();
}}
/>
<DynamicForm.SavingButton
submitLoading={loading || false}
buttonText={t('common.ok')}
submitFunc={(values: FieldValues) => {
handleSubmit(values);
// console.log(values);
// console.log(nodes, edges);
// handleOk(values);
}}
/>
</div>
</DynamicForm.Root>
</Modal>
</Sheet> </Sheet>
</> </>
); );

View File

@ -31,6 +31,7 @@ import {
initialIterationValues, initialIterationValues,
initialJin10Values, initialJin10Values,
initialKeywordExtractValues, initialKeywordExtractValues,
initialListOperationsValues,
initialMessageValues, initialMessageValues,
initialNoteValues, initialNoteValues,
initialParserValues, initialParserValues,
@ -129,6 +130,7 @@ export const useInitializeOperatorParams = () => {
prompts: t('flow.prompts.user.summary'), prompts: t('flow.prompts.user.summary'),
}, },
[Operator.DataOperations]: initialDataOperationsValues, [Operator.DataOperations]: initialDataOperationsValues,
[Operator.ListOperations]: initialListOperationsValues,
[Operator.VariableAssigner]: initialVariableAssignerValues, [Operator.VariableAssigner]: initialVariableAssignerValues,
[Operator.VariableAggregator]: initialVariableAggregatorValues, [Operator.VariableAggregator]: initialVariableAggregatorValues,
}; };

View File

@ -4,7 +4,7 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Operator } from '../constant'; import { Operator } from '../constant';
import useGraphStore from '../store'; import useGraphStore from '../store';
import { buildDslComponentsByGraph, buildDslGobalVariables } from '../utils'; import { buildDslComponentsByGraph, buildDslGlobalVariables } from '../utils';
export const useBuildDslData = () => { export const useBuildDslData = () => {
const { data } = useFetchAgent(); const { data } = useFetchAgent();
@ -13,7 +13,7 @@ export const useBuildDslData = () => {
const buildDslData = useCallback( const buildDslData = useCallback(
( (
currentNodes?: RAGFlowNodeType[], currentNodes?: RAGFlowNodeType[],
otherParam?: { gobalVariables: Record<string, GlobalVariableType> }, otherParam?: { globalVariables: Record<string, GlobalVariableType> },
) => { ) => {
const nodesToProcess = currentNodes ?? nodes; const nodesToProcess = currentNodes ?? nodes;
@ -41,13 +41,13 @@ export const useBuildDslData = () => {
data.dsl.components, data.dsl.components,
); );
const gobalVariables = buildDslGobalVariables( const globalVariables = buildDslGlobalVariables(
data.dsl, data.dsl,
otherParam?.gobalVariables, otherParam?.globalVariables,
); );
return { return {
...data.dsl, ...data.dsl,
...gobalVariables, ...globalVariables,
graph: { nodes: filteredNodes, edges: filteredEdges }, graph: { nodes: filteredNodes, edges: filteredEdges },
components: dslComponents, components: dslComponents,
}; };

View File

@ -21,7 +21,7 @@ export const useSaveGraph = (showMessage: boolean = true) => {
const saveGraph = useCallback( const saveGraph = useCallback(
async ( async (
currentNodes?: RAGFlowNodeType[], currentNodes?: RAGFlowNodeType[],
otherParam?: { gobalVariables: Record<string, GlobalVariableType> }, otherParam?: { globalVariables: Record<string, GlobalVariableType> },
) => { ) => {
return setAgent({ return setAgent({
id, id,

View File

@ -39,7 +39,7 @@ import { useParams } from 'umi';
import AgentCanvas from './canvas'; import AgentCanvas from './canvas';
import { DropdownProvider } from './canvas/context'; import { DropdownProvider } from './canvas/context';
import { Operator } from './constant'; import { Operator } from './constant';
import { GobalParamSheet } from './gobal-variable-sheet'; import { GlobalParamSheet } from './gobal-variable-sheet';
import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow'; import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow';
import { useHandleExportJsonFile } from './hooks/use-export-json'; import { useHandleExportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data'; import { useFetchDataOnMount } from './hooks/use-fetch-data';
@ -126,9 +126,9 @@ export default function Agent() {
} = useSetModalState(); } = useSetModalState();
const { const {
visible: gobalParamSheetVisible, visible: globalParamSheetVisible,
showModal: showGobalParamSheet, showModal: showGlobalParamSheet,
hideModal: hideGobalParamSheet, hideModal: hideGlobalParamSheet,
} = useSetModalState(); } = useSetModalState();
const { const {
@ -216,7 +216,7 @@ export default function Agent() {
</ButtonLoading> </ButtonLoading>
<ButtonLoading <ButtonLoading
variant={'secondary'} variant={'secondary'}
onClick={() => showGobalParamSheet()} onClick={() => showGlobalParamSheet()}
loading={loading} loading={loading}
> >
<MessageSquareCode /> {t('flow.conversationVariable')} <MessageSquareCode /> {t('flow.conversationVariable')}
@ -314,11 +314,11 @@ export default function Agent() {
loading={pipelineRunning} loading={pipelineRunning}
></PipelineRunSheet> ></PipelineRunSheet>
)} )}
{gobalParamSheetVisible && ( {globalParamSheetVisible && (
<GobalParamSheet <GlobalParamSheet
data={{}} data={{}}
hideModal={hideGobalParamSheet} hideModal={hideGlobalParamSheet}
></GobalParamSheet> ></GlobalParamSheet>
)} )}
</section> </section>
); );

View File

@ -14,7 +14,7 @@ import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.s
import { IconFont } from '@/components/icon-font'; import { IconFont } from '@/components/icon-font';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Equal, FileCode, HousePlus, Variable } from 'lucide-react'; import { Columns3, Equal, FileCode, HousePlus, Variable } from 'lucide-react';
import { Operator } from './constant'; import { Operator } from './constant';
interface IProps { interface IProps {
@ -57,6 +57,7 @@ export const SVGIconMap = {
}; };
export const LucideIconMap = { export const LucideIconMap = {
[Operator.DataOperations]: FileCode, [Operator.DataOperations]: FileCode,
[Operator.ListOperations]: Columns3,
[Operator.VariableAssigner]: Equal, [Operator.VariableAssigner]: Equal,
[Operator.VariableAggregator]: Variable, [Operator.VariableAggregator]: Variable,
}; };

View File

@ -328,7 +328,6 @@ export const buildDslComponentsByGraph = (
case Operator.DataOperations: case Operator.DataOperations:
params = transformDataOperationsParams(params); params = transformDataOperationsParams(params);
break; break;
default: default:
break; break;
} }
@ -348,30 +347,30 @@ export const buildDslComponentsByGraph = (
return components; return components;
}; };
export const buildDslGobalVariables = ( export const buildDslGlobalVariables = (
dsl: DSL, dsl: DSL,
gobalVariables?: Record<string, GlobalVariableType>, globalVariables?: Record<string, GlobalVariableType>,
) => { ) => {
if (!gobalVariables) { if (!globalVariables) {
return { globals: dsl.globals, variables: dsl.variables || {} }; return { globals: dsl.globals, variables: dsl.variables || {} };
} }
let gobalVariablesTemp: Record<string, any> = {}; let globalVariablesTemp: Record<string, any> = {};
let gobalSystem: Record<string, any> = {}; let globalSystem: Record<string, any> = {};
Object.keys(dsl.globals)?.forEach((key) => { Object.keys(dsl.globals)?.forEach((key) => {
if (key.indexOf('sys') > -1) { if (key.indexOf('sys') > -1) {
gobalSystem[key] = dsl.globals[key]; globalSystem[key] = dsl.globals[key];
} }
}); });
Object.keys(gobalVariables).forEach((key) => { Object.keys(globalVariables).forEach((key) => {
gobalVariablesTemp['env.' + key] = gobalVariables[key].value; globalVariablesTemp['env.' + key] = globalVariables[key].value;
}); });
const gobalVariablesResult = { const globalVariablesResult = {
...gobalSystem, ...globalSystem,
...gobalVariablesTemp, ...globalVariablesTemp,
}; };
return { globals: gobalVariablesResult, variables: gobalVariables }; return { globals: globalVariablesResult, variables: globalVariables };
}; };
export const receiveMessageError = (res: any) => export const receiveMessageError = (res: any) =>

View File

@ -7,11 +7,14 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Radio } from '@/components/ui/radio'; import { Radio } from '@/components/ui/radio';
import { Spin } from '@/components/ui/spin';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useMemo, useState } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { import {
useHandleKbEmbedding,
useHasParsedDocument, useHasParsedDocument,
useSelectChunkMethodList, useSelectChunkMethodList,
useSelectEmbeddingModelOptions, useSelectEmbeddingModelOptions,
@ -62,11 +65,17 @@ export function ChunkMethodItem(props: IProps) {
/> />
); );
} }
export function EmbeddingModelItem({ line = 1, isEdit = true }: IProps) { export function EmbeddingModelItem({ line = 1, isEdit }: IProps) {
const { t } = useTranslate('knowledgeConfiguration'); const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext(); const form = useFormContext();
const embeddingModelOptions = useSelectEmbeddingModelOptions(); const embeddingModelOptions = useSelectEmbeddingModelOptions();
const { handleChange } = useHandleKbEmbedding();
const disabled = useHasParsedDocument(isEdit); const disabled = useHasParsedDocument(isEdit);
const oldValue = useMemo(() => {
const embdStr = form.getValues('embd_id');
return embdStr || '';
}, [form]);
const [loading, setLoading] = useState(false);
return ( return (
<> <>
<FormField <FormField
@ -93,14 +102,33 @@ export function EmbeddingModelItem({ line = 1, isEdit = true }: IProps) {
className={cn('text-muted-foreground', { 'w-3/4': line === 1 })} className={cn('text-muted-foreground', { 'w-3/4': line === 1 })}
> >
<FormControl> <FormControl>
<SelectWithSearch <Spin
onChange={field.onChange} spinning={loading}
value={field.value} className={cn(' rounded-lg after:bg-bg-base', {
options={embeddingModelOptions} 'opacity-20': loading,
disabled={isEdit ? disabled : false} })}
placeholder={t('embeddingModelPlaceholder')} >
triggerClassName="!bg-bg-base" <SelectWithSearch
/> onChange={async (value) => {
field.onChange(value);
if (isEdit && disabled) {
setLoading(true);
const res = await handleChange({
embed_id: value,
callback: field.onChange,
});
if (res.code !== 0) {
field.onChange(oldValue);
}
setLoading(false);
}
}}
value={field.value}
options={embeddingModelOptions}
placeholder={t('embeddingModelPlaceholder')}
triggerClassName="!bg-bg-base"
/>
</Spin>
</FormControl> </FormControl>
</div> </div>
</div> </div>

View File

@ -88,7 +88,7 @@ export function GeneralForm() {
}} }}
/> />
<PermissionFormField></PermissionFormField> <PermissionFormField></PermissionFormField>
<EmbeddingModelItem></EmbeddingModelItem> <EmbeddingModelItem isEdit={true}></EmbeddingModelItem>
<PageRankFormField></PageRankFormField> <PageRankFormField></PageRankFormField>
<TagItems></TagItems> <TagItems></TagItems>

View File

@ -4,10 +4,12 @@ import { useSetModalState } from '@/hooks/common-hooks';
import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks'; import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { useSelectParserList } from '@/hooks/user-setting-hooks'; import { useSelectParserList } from '@/hooks/user-setting-hooks';
import kbService from '@/services/knowledge-service';
import { useIsFetching } from '@tanstack/react-query'; import { useIsFetching } from '@tanstack/react-query';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { UseFormReturn } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form';
import { useParams, useSearchParams } from 'umi';
import { z } from 'zod'; import { z } from 'zod';
import { formSchema } from './form-schema'; import { formSchema } from './form-schema';
@ -98,3 +100,22 @@ export const useRenameKnowledgeTag = () => {
showTagRenameModal: handleShowTagRenameModal, showTagRenameModal: handleShowTagRenameModal,
}; };
}; };
export const useHandleKbEmbedding = () => {
const { id } = useParams();
const [searchParams] = useSearchParams();
const knowledgeBaseId = searchParams.get('id') || id;
const handleChange = useCallback(
async ({ embed_id }: { embed_id: string }) => {
const res = await kbService.checkEmbedding({
kb_id: knowledgeBaseId,
embd_id: embed_id,
});
return res.data;
},
[knowledgeBaseId],
);
return {
handleChange,
};
};

View File

@ -47,6 +47,7 @@ const {
traceGraphRag, traceGraphRag,
runRaptor, runRaptor,
traceRaptor, traceRaptor,
check_embedding,
} = api; } = api;
const methods = { const methods = {
@ -214,6 +215,11 @@ const methods = {
url: api.pipelineRerun, url: api.pipelineRerun,
method: 'post', method: 'post',
}, },
checkEmbedding: {
url: check_embedding,
method: 'post',
},
}; };
const kbService = registerServer<keyof typeof methods>(methods, request); const kbService = registerServer<keyof typeof methods>(methods, request);

View File

@ -49,6 +49,8 @@ export default {
llm_tools: `${api_host}/plugin/llm_tools`, llm_tools: `${api_host}/plugin/llm_tools`,
// knowledge base // knowledge base
check_embedding: `${api_host}/kb/check_embedding`,
kb_list: `${api_host}/kb/list`, kb_list: `${api_host}/kb/list`,
create_kb: `${api_host}/kb/create`, create_kb: `${api_host}/kb/create`,
update_kb: `${api_host}/kb/update`, update_kb: `${api_host}/kb/update`,