Compare commits

...

11 Commits

Author SHA1 Message Date
7fef285af5 Revert "Bump infinity to 0.6.12 (#12140)" (#12161)
This reverts commit 0588fe79b9.
2025-12-24 15:24:12 +08:00
b1efb905e5 Fix: metadata_obj issue. (#12146)
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-12-24 13:40:34 +08:00
6400bf87ba Fix: LLM tool does not exist in multiple retrieval case (#12143)
### What problem does this PR solve?

 Fix LLM tool does not exist in multiple retrieval case

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-12-24 13:26:48 +08:00
f239bc02d3 Feat: Support Markdown Rendering for tips in user-fill-up Component #11825 (#12147)
### What problem does this PR solve?

Feat: Support Markdown Rendering for tips in user-fill-up Component
#11825

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-12-24 13:25:56 +08:00
5776fa73a7 refactor: improve memory service date time consistency (#12144)
### What problem does this PR solve?

 improve memory service date time consistency

### Type of change

- [x] Refactoring
2025-12-24 11:00:31 +08:00
fc6af1998b Doc: Added an HTTP request component reference (#12141)
### Type of change

- [x] Documentation Update
2025-12-24 09:35:32 +08:00
0588fe79b9 Bump infinity to 0.6.12 (#12140)
### What problem does this PR solve?

As title

### Type of change

- [x] Refactoring

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2025-12-24 09:34:54 +08:00
f545265f93 Fix:remove duplicate tool_meta (#12139)
### What problem does this PR solve?
pr:#12117
change:remove duplicate tool_meta

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-12-24 09:34:08 +08:00
c987d33649 Feat: deduplicate metadata lists during updates (#12125)
### What problem does this PR solve?

Deduplicate metadata lists during updates.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-12-24 09:32:55 +08:00
d72debf0db Fix: Add prompts when merging or deleting metadata. (#12138)
### What problem does this PR solve?

Fix: Add prompts when merging or deleting metadata.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
2025-12-24 09:32:41 +08:00
c33134ea2c Fix: table tag on chunks. (#12126)
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-12-24 09:32:19 +08:00
23 changed files with 496 additions and 140 deletions

View File

@ -86,8 +86,9 @@ class Agent(LLM, ToolBase):
self.tools = {}
for idx, cpn in enumerate(self._param.tools):
cpn = self._load_tool_obj(cpn)
name = cpn.get_meta()["function"]["name"]
self.tools[f"{name}_{idx}"] = cpn
original_name = cpn.get_meta()["function"]["name"]
indexed_name = f"{original_name}_{idx}"
self.tools[indexed_name] = cpn
self.chat_mdl = LLMBundle(self._canvas.get_tenant_id(), TenantLLMService.llm_id2llm_type(self._param.llm_id), self._param.llm_id,
max_retries=self._param.max_retries,
@ -95,7 +96,12 @@ class Agent(LLM, ToolBase):
max_rounds=self._param.max_rounds,
verbose_tool_use=True
)
self.tool_meta = [v.get_meta() for _,v in self.tools.items()]
self.tool_meta = []
for indexed_name, tool_obj in self.tools.items():
original_meta = tool_obj.get_meta()
indexed_meta = deepcopy(original_meta)
indexed_meta["function"]["name"] = indexed_name
self.tool_meta.append(indexed_meta)
for mcp in self._param.mcp:
_, mcp_server = MCPServerService.get_by_id(mcp["mcp_id"])
@ -109,7 +115,8 @@ class Agent(LLM, ToolBase):
def _load_tool_obj(self, cpn: dict) -> object:
from agent.component import component_class
param = component_class(cpn["component_name"] + "Param")()
tool_name = cpn["component_name"]
param = component_class(tool_name + "Param")()
param.update(cpn["params"])
try:
param.check()
@ -277,19 +284,15 @@ class Agent(LLM, ToolBase):
else:
user_request = history[-1]["content"]
def build_task_desc(prompt: str, user_request: str, tool_metas: list[dict], user_defined_prompt: dict | None = None) -> str:
def build_task_desc(prompt: str, user_request: str, user_defined_prompt: dict | None = None) -> str:
"""Build a minimal task_desc by concatenating prompt, query, and tool schemas."""
user_defined_prompt = user_defined_prompt or {}
tools_json = json.dumps(tool_metas, ensure_ascii=False, indent=2)
task_desc = (
"### Agent Prompt\n"
f"{prompt}\n\n"
"### User Request\n"
f"{user_request}\n\n"
"### Tools (schemas)\n"
f"{tools_json}\n"
)
if user_defined_prompt:
@ -368,7 +371,7 @@ class Agent(LLM, ToolBase):
hist.append({"role": "user", "content": content})
st = timer()
task_desc = build_task_desc(prompt, user_request, tool_metas, user_defined_prompt)
task_desc = build_task_desc(prompt, user_request, user_defined_prompt)
self.callback("analyze_task", {}, task_desc, elapsed_time=timer()-st)
for _ in range(self._param.max_rounds + 1):
if self.check_if_canceled("Agent streaming"):

View File

@ -56,7 +56,6 @@ class LLMParam(ComponentParamBase):
self.check_nonnegative_number(int(self.max_tokens), "[Agent] Max tokens")
self.check_decimal_float(float(self.top_p), "[Agent] Top P")
self.check_empty(self.llm_id, "[Agent] LLM")
self.check_empty(self.sys_prompt, "[Agent] System prompt")
self.check_empty(self.prompts, "[Agent] User prompt")
def gen_conf(self):

View File

@ -33,6 +33,7 @@ from api.db.db_models import DB, Document, Knowledgebase, Task, Tenant, UserTena
from api.db.db_utils import bulk_insert_into_db
from api.db.services.common_service import CommonService
from api.db.services.knowledgebase_service import KnowledgebaseService
from common.metadata_utils import dedupe_list
from common.misc_utils import get_uuid
from common.time_utils import current_timestamp, get_format_time
from common.constants import LLMType, ParserType, StatusEnum, TaskStatus, SVR_CONSUMER_GROUP_NAME
@ -696,10 +697,12 @@ class DocumentService(CommonService):
for k,v in r.meta_fields.items():
if k not in meta:
meta[k] = {}
v = str(v)
if v not in meta[k]:
meta[k][v] = []
meta[k][v].append(doc_id)
if not isinstance(v, list):
v = [v]
for vv in v:
if vv not in meta[k]:
meta[k][vv] = []
meta[k][vv].append(doc_id)
return meta
@classmethod
@ -797,7 +800,10 @@ class DocumentService(CommonService):
match_provided = "match" in upd
if isinstance(meta[key], list):
if not match_provided:
meta[key] = new_value
if isinstance(new_value, list):
meta[key] = dedupe_list(new_value)
else:
meta[key] = new_value
changed = True
else:
match_value = upd.get("match")
@ -810,7 +816,7 @@ class DocumentService(CommonService):
else:
new_list.append(item)
if replaced:
meta[key] = new_list
meta[key] = dedupe_list(new_list)
changed = True
else:
if not match_provided:

View File

@ -117,6 +117,8 @@ class MemoryService(CommonService):
if len(memory_name) > MEMORY_NAME_LIMIT:
return False, f"Memory name {memory_name} exceeds limit of {MEMORY_NAME_LIMIT}."
timestamp = current_timestamp()
format_time = get_format_time()
# build create dict
memory_info = {
"id": get_uuid(),
@ -126,10 +128,10 @@ class MemoryService(CommonService):
"embd_id": embd_id,
"llm_id": llm_id,
"system_prompt": PromptAssembler.assemble_system_prompt({"memory_type": memory_type}),
"create_time": current_timestamp(),
"create_date": get_format_time(),
"update_time": current_timestamp(),
"update_date": get_format_time(),
"create_time": timestamp,
"create_date": format_time,
"update_time": timestamp,
"update_date": format_time,
}
obj = cls.model(**memory_info).save(force_insert=True)

View File

@ -44,21 +44,27 @@ def meta_filter(metas: dict, filters: list[dict], logic: str = "and"):
def filter_out(v2docs, operator, value):
ids = []
for input, docids in v2docs.items():
if operator in ["=", "", ">", "<", "", ""]:
try:
if isinstance(input, list):
input = input[0]
input = float(input)
value = float(value)
except Exception:
input = str(input)
value = str(value)
pass
if isinstance(input, str):
input = input.lower()
if isinstance(value, str):
value = value.lower()
for conds in [
(operator == "contains", str(value).lower() in str(input).lower()),
(operator == "not contains", str(value).lower() not in str(input).lower()),
(operator == "in", str(input).lower() in str(value).lower()),
(operator == "not in", str(input).lower() not in str(value).lower()),
(operator == "start with", str(input).lower().startswith(str(value).lower())),
(operator == "end with", str(input).lower().endswith(str(value).lower())),
(operator == "contains", input in value if not isinstance(input, list) else all([i in value for i in input])),
(operator == "not contains", input not in value if not isinstance(input, list) else all([i not in value for i in input])),
(operator == "in", input in value if not isinstance(input, list) else all([i in value for i in input])),
(operator == "not in", input not in value if not isinstance(input, list) else all([i not in value for i in input])),
(operator == "start with", str(input).lower().startswith(str(value).lower()) if not isinstance(input, list) else "".join([str(i).lower() for i in input]).startswith(str(value).lower())),
(operator == "end with", str(input).lower().endswith(str(value).lower()) if not isinstance(input, list) else "".join([str(i).lower() for i in input]).endswith(str(value).lower())),
(operator == "empty", not input),
(operator == "not empty", input),
(operator == "=", input == value),
@ -145,6 +151,18 @@ async def apply_meta_data_filter(
return doc_ids
def dedupe_list(values: list) -> list:
seen = set()
deduped = []
for item in values:
key = str(item)
if key in seen:
continue
seen.add(key)
deduped.append(item)
return deduped
def update_metadata_to(metadata, meta):
if not meta:
return metadata
@ -156,11 +174,13 @@ def update_metadata_to(metadata, meta):
return metadata
if not isinstance(meta, dict):
return metadata
for k, v in meta.items():
if isinstance(v, list):
v = [vv for vv in v if isinstance(vv, str)]
if not v:
continue
v = dedupe_list(v)
if not isinstance(v, list) and not isinstance(v, str):
continue
if k not in metadata:
@ -171,6 +191,7 @@ def update_metadata_to(metadata, meta):
metadata[k].extend(v)
else:
metadata[k].append(v)
metadata[k] = dedupe_list(metadata[k])
else:
metadata[k] = v
@ -202,4 +223,4 @@ def metadata_schema(metadata: list|None) -> Dict[str, Any]:
}
json_schema["additionalProperties"] = False
return json_schema
return json_schema

View File

@ -0,0 +1,90 @@
---
sidebar_position: 30
slug: /http_request_component
---
# HTTP request component
A component that calls remote services.
---
An **HTTP request** component lets you access remote APIs or services by providing a URL and an HTTP method, and then receive the response. You can customize headers, parameters, proxies, and timeout settings, and use common methods like GET and POST. Its useful for exchanging data with external systems in a workflow.
## Prerequisites
- An accessible remote API or service.
- Add a Token or credentials to the request header, if the target service requires authentication.
## Configurations
### Url
*Required*. The complete request address, for example: http://api.example.com/data.
### Method
The HTTP request method to select. Available options:
- GET
- POST
- PUT
### Timeout
The maximum waiting time for the request, in seconds. Defaults to `60`.
### Headers
Custom HTTP headers can be set here, for example:
```http
{
"Accept": "application/json",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
```
### Proxy
Optional. The proxy server address to use for this request.
### Clean HTML
`Boolean`: Whether to remove HTML tags from the returned results and keep plain text only.
### Parameter
*Optional*. Parameters to send with the HTTP request. Supports key-value pairs:
- To assign a value using a dynamic system variable, set it as Variable.
- To override these dynamic values under certain conditions and use a fixed static value instead, Value is the appropriate choice.
:::tip NOTE
- For GET requests, these parameters are appended to the end of the URL.
- For POST/PUT requests, they are sent as the request body.
:::
#### Example setting
![](https://raw.githubusercontent.com/infiniflow/ragflow-docs/main/images/http_settings.png)
#### Example response
```html
{ "args": { "App": "RAGFlow", "Query": "How to do?", "Userid": "241ed25a8e1011f0b979424ebc5b108b" }, "headers": { "Accept": "/", "Accept-Encoding": "gzip, deflate, br, zstd", "Cache-Control": "no-cache", "Host": "httpbin.org", "User-Agent": "python-requests/2.32.2", "X-Amzn-Trace-Id": "Root=1-68c9210c-5aab9088580c130a2f065523" }, "origin": "185.36.193.38", "url": "https://httpbin.org/get?Userid=241ed25a8e1011f0b979424ebc5b108b&App=RAGFlow&Query=How+to+do%3F" }
```
### Output
The global variable name for the output of the HTTP request component, which can be referenced by other components in the workflow.
- `Result`: `string` The response returned by the remote service.
## Example
This is a usage example: a workflow sends a GET request from the **Begin** component to `https://httpbin.org/get` via the **HTTP Request_0** component, passes parameters to the server, and finally outputs the result through the **Message_0** component.
![](https://raw.githubusercontent.com/infiniflow/ragflow-docs/main/images/http_usage.PNG)

View File

@ -348,7 +348,8 @@ def tokenize_table(tbls, doc, eng, batch_size=10):
d["doc_type_kwd"] = "table"
if img:
d["image"] = img
d["doc_type_kwd"] = "image"
if d["content_with_weight"].find("<tr>") < 0:
d["doc_type_kwd"] = "image"
if poss:
add_positions(d, poss)
res.append(d)
@ -361,7 +362,8 @@ def tokenize_table(tbls, doc, eng, batch_size=10):
d["doc_type_kwd"] = "table"
if img:
d["image"] = img
d["doc_type_kwd"] = "image"
if d["content_with_weight"].find("<tr>") < 0:
d["doc_type_kwd"] = "image"
add_positions(d, poss)
res.append(d)
return res

View File

@ -339,7 +339,7 @@ def tool_schema(tools_description: list[dict], complete_task=False):
}
for idx, tool in enumerate(tools_description):
name = tool["function"]["name"]
desc[f"{name}_{idx}"] = tool
desc[name] = tool
return "\n\n".join([f"## {i+1}. {fnm}\n{json.dumps(des, ensure_ascii=False, indent=4)}" for i, (fnm, des) in enumerate(desc.items())])

View File

@ -94,7 +94,7 @@ This content will NOT be shown to the user.
## Step 2: Structured Reflection (MANDATORY before `complete_task`)
### Context
- Goal: {{ task_analysis }}
- Goal: Reflect on the current task based on the full conversation context
- Executed tool calls so far (if any): reflect from conversation history
### Task Complexity Assessment

View File

@ -395,9 +395,9 @@ async def build_chunks(task, progress_callback):
await asyncio.gather(*tasks, return_exceptions=True)
raise
metadata = {}
for ck in cks:
metadata = update_metadata_to(metadata, ck["metadata_obj"])
del ck["metadata_obj"]
for doc in docs:
metadata = update_metadata_to(metadata, doc["metadata_obj"])
del doc["metadata_obj"]
if metadata:
e, doc = DocumentService.get_by_id(task["doc_id"])
if e:

View File

@ -35,6 +35,7 @@ import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { Loader } from 'lucide-react';
import { MultiSelect, MultiSelectOptionType } from './ui/multi-select';
import { Switch } from './ui/switch';
// Field type enumeration
export enum FormFieldType {
@ -46,6 +47,7 @@ export enum FormFieldType {
Select = 'select',
MultiSelect = 'multi-select',
Checkbox = 'checkbox',
Switch = 'switch',
Tag = 'tag',
Custom = 'custom',
}
@ -154,6 +156,7 @@ export const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
}
break;
case FormFieldType.Checkbox:
case FormFieldType.Switch:
fieldSchema = z.boolean();
break;
case FormFieldType.Tag:
@ -193,6 +196,8 @@ export const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
if (
field.type !== FormFieldType.Number &&
field.type !== FormFieldType.Checkbox &&
field.type !== FormFieldType.Switch &&
field.type !== FormFieldType.Custom &&
field.type !== FormFieldType.Tag &&
field.required
) {
@ -289,7 +294,10 @@ const generateDefaultValues = <T extends FieldValues>(
const lastKey = keys[keys.length - 1];
if (field.defaultValue !== undefined) {
current[lastKey] = field.defaultValue;
} else if (field.type === FormFieldType.Checkbox) {
} else if (
field.type === FormFieldType.Checkbox ||
field.type === FormFieldType.Switch
) {
current[lastKey] = false;
} else if (field.type === FormFieldType.Tag) {
current[lastKey] = [];
@ -299,7 +307,10 @@ const generateDefaultValues = <T extends FieldValues>(
} else {
if (field.defaultValue !== undefined) {
defaultValues[field.name] = field.defaultValue;
} else if (field.type === FormFieldType.Checkbox) {
} else if (
field.type === FormFieldType.Checkbox ||
field.type === FormFieldType.Switch
) {
defaultValues[field.name] = false;
} else if (
field.type === FormFieldType.Tag ||
@ -502,6 +513,32 @@ export const RenderField = ({
)}
/>
);
case FormFieldType.Switch:
return (
<RAGFlowFormItem
{...field}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (checked: boolean) => {
fieldProps.onChange(checked);
field.onChange?.(checked);
},
}
: fieldProps;
return (
<Switch
checked={finalFieldProps.value as boolean}
onCheckedChange={(checked) => finalFieldProps.onChange(checked)}
disabled={field.disabled}
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Tag:
return (

View File

@ -31,14 +31,16 @@ const handleCheckChange = ({
(value: string) => value !== item.id.toString(),
);
const newValue = {
...currentValue,
[parentId]: newParentValues,
};
const newValue = newParentValues?.length
? {
...currentValue,
[parentId]: newParentValues,
}
: { ...currentValue };
if (newValue[parentId].length === 0) {
delete newValue[parentId];
}
// if (newValue[parentId].length === 0) {
// delete newValue[parentId];
// }
return field.onChange(newValue);
} else {
@ -66,20 +68,31 @@ const FilterItem = memo(
}) => {
return (
<div
className={`flex items-center justify-between text-text-primary text-xs ${level > 0 ? 'ml-4' : ''}`}
className={`flex items-center justify-between text-text-primary text-xs ${level > 0 ? 'ml-1' : ''}`}
>
<FormItem className="flex flex-row space-x-3 space-y-0 items-center">
<FormItem className="flex flex-row space-x-3 space-y-0 items-center ">
<FormControl>
<Checkbox
checked={field.value?.includes(item.id.toString())}
onCheckedChange={(checked: boolean) =>
handleCheckChange({ checked, field, item })
}
/>
<div className="flex space-x-3">
<Checkbox
checked={field.value?.includes(item.id.toString())}
onCheckedChange={(checked: boolean) =>
handleCheckChange({ checked, field, item })
}
// className="hidden group-hover:block"
/>
<FormLabel
onClick={() =>
handleCheckChange({
checked: !field.value?.includes(item.id.toString()),
field,
item,
})
}
>
{item.label}
</FormLabel>
</div>
</FormControl>
<FormLabel onClick={(e) => e.stopPropagation()}>
{item.label}
</FormLabel>
</FormItem>
{item.count !== undefined && (
<span className="text-sm">{item.count}</span>
@ -107,11 +120,11 @@ export const FilterField = memo(
<FormField
key={item.id}
control={form.control}
name={parent.field as string}
name={parent.field?.toString() as string}
render={({ field }) => {
if (hasNestedList) {
return (
<div className={`flex flex-col gap-2 ${level > 0 ? 'ml-4' : ''}`}>
<div className={`flex flex-col gap-2 ${level > 0 ? 'ml-1' : ''}`}>
<div
className="flex items-center justify-between cursor-pointer"
onClick={() => {
@ -138,23 +151,6 @@ export const FilterField = memo(
}}
level={level + 1}
/>
// <FilterItem key={child.id} item={child} field={child.field} level={level+1} />
// <div
// className="flex flex-row space-x-3 space-y-0 items-center"
// key={child.id}
// >
// <FormControl>
// <Checkbox
// checked={field.value?.includes(child.id.toString())}
// onCheckedChange={(checked) =>
// handleCheckChange({ checked, field, item: child })
// }
// />
// </FormControl>
// <FormLabel onClick={(e) => e.stopPropagation()}>
// {child.label}
// </FormLabel>
// </div>
))}
</div>
);

View File

@ -11,7 +11,7 @@ import {
useMemo,
useState,
} from 'react';
import { useForm } from 'react-hook-form';
import { FieldPath, useForm } from 'react-hook-form';
import { ZodArray, ZodString, z } from 'zod';
import { Button } from '@/components/ui/button';
@ -178,7 +178,9 @@ function CheckboxFormMultiple({
<FormField
key={x.field}
control={form.control}
name={x.field}
name={
x.field.toString() as FieldPath<z.infer<typeof FormSchema>>
}
render={() => (
<FormItem className="space-y-4">
<div>
@ -186,19 +188,20 @@ function CheckboxFormMultiple({
{x.label}
</FormLabel>
</div>
{x.list.map((item) => {
return (
<FilterField
key={item.id}
item={{ ...item }}
parent={{
...x,
id: x.field,
// field: `${x.field}${item.field ? '.' + item.field : ''}`,
}}
/>
);
})}
{x.list?.length &&
x.list.map((item) => {
return (
<FilterField
key={item.id}
item={{ ...item }}
parent={{
...x,
id: x.field,
// field: `${x.field}${item.field ? '.' + item.field : ''}`,
}}
/>
);
})}
<FormMessage />
</FormItem>
)}

View File

@ -176,6 +176,11 @@ Procedural Memory: Learned skills, habits, and automated procedures.`,
},
knowledgeDetails: {
metadata: {
valueExists:
'Value already exists. Confirm to merge duplicates and combine all associated files.',
fieldNameExists:
'Field name already exists. Confirm to merge duplicates and combine all associated files.',
fieldExists: 'Field already exists.',
fieldSetting: 'Field settings',
changesAffectNewParses: 'Changes affect new parses only.',
editMetadataForDataset: 'View and edit metadata for ',
@ -190,6 +195,7 @@ Procedural Memory: Learned skills, habits, and automated procedures.`,
description: 'Description',
fieldName: 'Field name',
editMetadata: 'Edit metadata',
deleteWarn: 'This {{field}} will be removed from all associated files',
},
metadataField: 'Metadata field',
systemAttribute: 'System attribute',

View File

@ -182,6 +182,10 @@ export default {
description: '描述',
fieldName: '字段名',
editMetadata: '编辑元数据',
valueExists: '值已存在。确认合并重复项并组合所有关联文件。',
fieldNameExists: '字段名已存在。确认合并重复项并组合所有关联文件。',
fieldExists: '字段名已存在。',
deleteWarn: '此 {{field}} 将从所有关联文件中移除',
},
localUpload: '本地上传',
fileSize: '文件大小',

View File

@ -4,6 +4,7 @@ import { useSendAgentMessage } from './use-send-agent-message';
import { FileUploadProps } from '@/components/file-upload';
import { NextMessageInput } from '@/components/message-input/next';
import MarkdownContent from '@/components/next-markdown-content';
import MessageItem from '@/components/next-message-item';
import PdfSheet from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
@ -102,8 +103,10 @@ function AgentChatBox() {
{message.role === MessageType.Assistant &&
derivedMessages.length - 1 !== i && (
<div>
<div>{message?.data?.tips}</div>
<MarkdownContent
content={message?.data?.tips}
loading={false}
></MarkdownContent>
<div>
{buildInputList(message)?.map((item) => item.value)}
</div>

View File

@ -1,3 +1,4 @@
import MarkdownContent from '@/components/next-markdown-content';
import { ButtonLoading } from '@/components/ui/button';
import {
Form,
@ -234,7 +235,14 @@ const DebugContent = ({
return (
<>
<section>
{message?.data?.tips && <div className="mb-2">{message.data.tips}</div>}
{message?.data?.tips && (
<div className="mb-2">
<MarkdownContent
content={message?.data?.tips}
loading={false}
></MarkdownContent>
</div>
)}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{parameters.map((x, idx) => {

View File

@ -1,11 +1,14 @@
import message from '@/components/ui/message';
import { useSetModalState } from '@/hooks/common-hooks';
import { useSetDocumentMeta } from '@/hooks/use-document-request';
import {
DocumentApiAction,
useSetDocumentMeta,
} from '@/hooks/use-document-request';
import kbService, {
getMetaDataService,
updateMetaData,
} from '@/services/knowledge-service';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
@ -191,7 +194,7 @@ export const useManageMetaDataModal = (
const { data, loading } = useFetchMetaDataManageData(type);
const [tableData, setTableData] = useState<IMetaDataTableData[]>(metaData);
const queryClient = useQueryClient();
const { operations, addDeleteRow, addDeleteValue, addUpdateValue } =
useMetadataOperations();
@ -259,11 +262,14 @@ export const useManageMetaDataModal = (
data: operations,
});
if (res.code === 0) {
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
message.success(t('message.operated'));
callback();
}
},
[operations, id, t],
[operations, id, t, queryClient],
);
const handleSaveUpdateSingle = useCallback(

View File

@ -39,6 +39,7 @@ export type IManageModalProps = {
export interface IManageValuesProps {
title: ReactNode;
existsKeys: string[];
visible: boolean;
isEditField?: boolean;
isAddValue?: boolean;
@ -46,6 +47,7 @@ export interface IManageValuesProps {
isShowValueSwitch?: boolean;
isVerticalShowValue?: boolean;
data: IMetaDataTableData;
type: MetadataType;
hideModal: () => void;
onSave: (data: IMetaDataTableData) => void;
addUpdateValue: (key: string, value: string | string[]) => void;

View File

@ -54,6 +54,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
values: [],
});
const [currentValueIndex, setCurrentValueIndex] = useState<number>(0);
const [deleteDialogContent, setDeleteDialogContent] = useState({
visible: false,
title: '',
@ -94,12 +95,12 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
description: '',
values: [],
});
// setCurrentValueIndex(tableData.length || 0);
setCurrentValueIndex(tableData.length || 0);
showManageValuesModal();
};
const handleEditValueRow = useCallback(
(data: IMetaDataTableData) => {
// setCurrentValueIndex(index);
(data: IMetaDataTableData, index: number) => {
setCurrentValueIndex(index);
setValueData(data);
showManageValuesModal();
},
@ -153,10 +154,33 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
variant={'delete'}
className="p-0 bg-transparent"
onClick={() => {
handleDeleteSingleValue(
row.getValue('field'),
value,
);
setDeleteDialogContent({
visible: true,
title:
t('common.delete') +
' ' +
t('knowledgeDetails.metadata.metadata'),
name: row.getValue('field') + '/' + value,
warnText: t(
'knowledgeDetails.metadata.deleteWarn',
{
field:
t('knowledgeDetails.metadata.field') +
'/' +
t('knowledgeDetails.metadata.values'),
},
),
onOk: () => {
hideDeleteModal();
handleDeleteSingleValue(
row.getValue('field'),
value,
);
},
onCancel: () => {
hideDeleteModal();
},
});
}}
>
<Trash2 />
@ -185,7 +209,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
variant={'ghost'}
className="bg-transparent px-1 py-0"
onClick={() => {
handleEditValueRow(row.original);
handleEditValueRow(row.original, row.index);
}}
>
<Settings />
@ -201,7 +225,9 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
' ' +
t('knowledgeDetails.metadata.metadata'),
name: row.getValue('field'),
warnText: t('knowledgeDetails.metadata.deleteWarn'),
warnText: t('knowledgeDetails.metadata.deleteWarn', {
field: t('knowledgeDetails.metadata.field'),
}),
onOk: () => {
hideDeleteModal();
handleDeleteSingleRow(row.getValue('field'));
@ -243,12 +269,26 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
const handleSaveValues = (data: IMetaDataTableData) => {
setTableData((prev) => {
//If the keys are the same, they need to be merged.
const fieldMap = new Map<string, any>();
let newData;
if (currentValueIndex >= prev.length) {
// Add operation
newData = [...prev, data];
} else {
// Edit operation
newData = prev.map((item, index) => {
if (index === currentValueIndex) {
return data;
}
return item;
});
}
prev.forEach((item) => {
// Deduplicate by field and merge values
const fieldMap = new Map<string, IMetaDataTableData>();
newData.forEach((item) => {
if (fieldMap.has(item.field)) {
const existingItem = fieldMap.get(item.field);
// Merge values if field exists
const existingItem = fieldMap.get(item.field)!;
const mergedValues = [
...new Set([...existingItem.values, ...item.values]),
];
@ -258,20 +298,14 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
}
});
if (fieldMap.has(data.field)) {
const existingItem = fieldMap.get(data.field);
const mergedValues = [
...new Set([...existingItem.values, ...data.values]),
];
fieldMap.set(data.field, { ...existingItem, values: mergedValues });
} else {
fieldMap.set(data.field, data);
}
return Array.from(fieldMap.values());
});
};
const existsKeys = useMemo(() => {
return tableData.map((item) => item.field);
}, [tableData]);
return (
<>
<Modal
@ -357,6 +391,8 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
: t('knowledgeDetails.metadata.editMetadata')}
</div>
}
type={metadataType}
existsKeys={existsKeys}
visible={manageValuesVisible}
hideModal={hideManageValuesModal}
data={valueData}

View File

@ -1,3 +1,7 @@
import {
ConfirmDeleteDialog,
ConfirmDeleteDialogNode,
} from '@/components/confirm-delete-dialog';
import EditTag from '@/components/edit-tag';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@ -7,6 +11,7 @@ import { Textarea } from '@/components/ui/textarea';
import { Plus, Trash2 } from 'lucide-react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MetadataType } from './hook';
import { IManageValuesProps, IMetaDataTableData } from './interface';
// Create a separate input component, wrapped with memo to avoid unnecessary re-renders
@ -63,17 +68,62 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
onSave,
addUpdateValue,
addDeleteValue,
existsKeys,
type,
} = props;
const [metaData, setMetaData] = useState(data);
const { t } = useTranslation();
const [valueError, setValueError] = useState<Record<string, string>>({
field: '',
values: '',
});
const [deleteDialogContent, setDeleteDialogContent] = useState({
visible: false,
title: '',
name: '',
warnText: '',
onOk: () => {},
onCancel: () => {},
});
const hideDeleteModal = () => {
setDeleteDialogContent({
visible: false,
title: '',
name: '',
warnText: '',
onOk: () => {},
onCancel: () => {},
});
};
// Use functional update to avoid closure issues
const handleChange = useCallback((field: string, value: any) => {
setMetaData((prev) => ({
...prev,
[field]: value,
}));
}, []);
const handleChange = useCallback(
(field: string, value: any) => {
if (field === 'field' && existsKeys.includes(value)) {
setValueError((prev) => {
return {
...prev,
field:
type === MetadataType.Setting
? t('knowledgeDetails.metadata.fieldExists')
: t('knowledgeDetails.metadata.fieldNameExists'),
};
});
} else if (field === 'field' && !existsKeys.includes(value)) {
setValueError((prev) => {
return {
...prev,
field: '',
};
});
}
setMetaData((prev) => ({
...prev,
[field]: value,
}));
},
[existsKeys, type, t],
);
// Maintain separate state for each input box
const [tempValues, setTempValues] = useState<string[]>([...data.values]);
@ -89,6 +139,9 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
}, [hideModal]);
const handleSave = useCallback(() => {
if (type === MetadataType.Setting && valueError.field) {
return;
}
if (!metaData.restrictDefinedValues && isShowValueSwitch) {
const newMetaData = { ...metaData, values: [] };
onSave(newMetaData);
@ -96,17 +149,35 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
onSave(metaData);
}
handleHideModal();
}, [metaData, onSave, handleHideModal, isShowValueSwitch]);
}, [metaData, onSave, handleHideModal, isShowValueSwitch, type, valueError]);
// Handle value changes, only update temporary state
const handleValueChange = useCallback((index: number, value: string) => {
setTempValues((prev) => {
const newValues = [...prev];
newValues[index] = value;
const handleValueChange = useCallback(
(index: number, value: string) => {
setTempValues((prev) => {
if (prev.includes(value)) {
setValueError((prev) => {
return {
...prev,
values: t('knowledgeDetails.metadata.valueExists'),
};
});
} else {
setValueError((prev) => {
return {
...prev,
values: '',
};
});
}
const newValues = [...prev];
newValues[index] = value;
return newValues;
});
}, []);
return newValues;
});
},
[t],
);
// Handle blur event, synchronize to main state
const handleValueBlur = useCallback(() => {
@ -137,6 +208,27 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
[addDeleteValue, metaData],
);
const showDeleteModal = (item: string, callback: () => void) => {
setDeleteDialogContent({
visible: true,
title: t('common.delete') + ' ' + t('knowledgeDetails.metadata.metadata'),
name: metaData.field + '/' + item,
warnText: t('knowledgeDetails.metadata.deleteWarn', {
field:
t('knowledgeDetails.metadata.field') +
'/' +
t('knowledgeDetails.metadata.values'),
}),
onOk: () => {
hideDeleteModal();
callback();
},
onCancel: () => {
hideDeleteModal();
},
});
};
// Handle adding new value
const handleAddValue = useCallback(() => {
setTempValues((prev) => [...new Set([...prev, ''])]);
@ -172,9 +264,13 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
<Input
value={metaData.field}
onChange={(e) => {
handleChange('field', e.target?.value || '');
const value = e.target?.value || '';
if (/^[a-zA-Z_]*$/.test(value)) {
handleChange('field', value);
}
}}
/>
<div className="text-state-error text-sm">{valueError.field}</div>
</div>
</div>
)}
@ -230,7 +326,11 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
item={item}
index={index}
onValueChange={handleValueChange}
onDelete={handleDelete}
onDelete={(idx: number) => {
showDeleteModal(item, () => {
handleDelete(idx);
});
}}
onBlur={handleValueBlur}
/>
);
@ -240,11 +340,41 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
{!isVerticalShowValue && (
<EditTag
value={metaData.values}
onChange={(value) => handleChange('values', value)}
onChange={(value) => {
// find deleted value
const item = metaData.values.find(
(item) => !value.includes(item),
);
if (item) {
showDeleteModal(item, () => {
// handleDelete(idx);
handleChange('values', value);
});
} else {
handleChange('values', value);
}
}}
/>
)}
<div className="text-state-error text-sm">{valueError.values}</div>
</div>
)}
{deleteDialogContent.visible && (
<ConfirmDeleteDialog
open={deleteDialogContent.visible}
onCancel={deleteDialogContent.onCancel}
onOk={deleteDialogContent.onOk}
title={deleteDialogContent.title}
content={{
node: (
<ConfirmDeleteDialogNode
name={deleteDialogContent.name}
warnText={deleteDialogContent.warnText}
/>
),
}}
/>
)}
</div>
</Modal>
);

View File

@ -17,6 +17,7 @@ import {
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { FormLayout } from '@/constants/form';
import { useFetchTenantInfo } from '@/hooks/use-user-setting-request';
import { IModalProps } from '@/interfaces/common';
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react';
@ -33,6 +34,7 @@ const FormId = 'dataset-creating-form';
export function InputForm({ onOk }: IModalProps<any>) {
const { t } = useTranslation();
const { data: tenantInfo } = useFetchTenantInfo();
const FormSchema = z
.object({
@ -80,7 +82,7 @@ export function InputForm({ onOk }: IModalProps<any>) {
name: '',
parseType: 1,
parser_id: '',
embd_id: '',
embd_id: tenantInfo?.embd_id,
},
});

View File

@ -263,7 +263,7 @@ export const documentFilter = (kb_id: string) =>
export const getMetaDataService = ({ kb_id }: { kb_id: string }) =>
request.post(api.getMetaData, { data: { kb_id } });
export const updateMetaData = ({ kb_id, data }: { kb_id: string; data: any }) =>
request.post(api.updateMetaData, { data: { kb_id, data } });
request.post(api.updateMetaData, { data: { kb_id, ...data } });
export const listDataPipelineLogDocument = (
params?: IFetchKnowledgeListRequestParams,