Compare commits

...

7 Commits

Author SHA1 Message Date
df8d31451b Feat: Import dsl from agent list page #9869 (#10033)
### What problem does this PR solve?

Feat: Import dsl from agent list page #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-09-10 18:22:16 +08:00
fc95d113c3 Feat(config): Update service config template new defaults (#10029)
### What problem does this PR solve?

- Update default LLM configuration with BAAI and model details #9404
- Add SMTP configuration section #9479
- Add OpenDAL storage configuration option #8232

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-09-10 16:39:26 +08:00
7d14455fbe Feat: Add type card to create agent dialog #9869 (#10025)
### What problem does this PR solve?

Feat: Add type card to create agent dialog #9869
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-09-10 15:56:10 +08:00
bbe6ed3b90 Fix: Fixed the issue where newly added tool operators would disappear after editing the form #10013 (#10016)
### What problem does this PR solve?

Fix: Fixed the issue where newly added tool operators would disappear
after editing the form #10013

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-09-10 15:55:59 +08:00
127af4e45c Refactor:Improve BytesIO usage for image2base64 (#9997)
### What problem does this PR solve?

Improve BytesIO usage for image2base64

### Type of change

- [x] Refactoring
2025-09-10 15:55:33 +08:00
41cdba19ba Feat: dataflow supports markdown (#10003)
### What problem does this PR solve?

Dataflow supports markdown.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
2025-09-10 13:31:02 +08:00
0d9c1f1c3c Feat: dataflow supports Spreadsheet and Word processor document (#9996)
### What problem does this PR solve?

Dataflow supports Spreadsheet and Word processor document

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-09-10 13:02:53 +08:00
32 changed files with 577 additions and 321 deletions

View File

@ -22,10 +22,10 @@ from openpyxl import Workbook, load_workbook
from rag.nlp import find_codec from rag.nlp import find_codec
# copied from `/openpyxl/cell/cell.py` # copied from `/openpyxl/cell/cell.py`
ILLEGAL_CHARACTERS_RE = re.compile(r'[\000-\010]|[\013-\014]|[\016-\037]') ILLEGAL_CHARACTERS_RE = re.compile(r"[\000-\010]|[\013-\014]|[\016-\037]")
class RAGFlowExcelParser: class RAGFlowExcelParser:
@staticmethod @staticmethod
def _load_excel_to_workbook(file_like_object): def _load_excel_to_workbook(file_like_object):
if isinstance(file_like_object, bytes): if isinstance(file_like_object, bytes):
@ -36,7 +36,7 @@ class RAGFlowExcelParser:
file_head = file_like_object.read(4) file_head = file_like_object.read(4)
file_like_object.seek(0) file_like_object.seek(0)
if not (file_head.startswith(b'PK\x03\x04') or file_head.startswith(b'\xD0\xCF\x11\xE0')): if not (file_head.startswith(b"PK\x03\x04") or file_head.startswith(b"\xd0\xcf\x11\xe0")):
logging.info("Not an Excel file, converting CSV to Excel Workbook") logging.info("Not an Excel file, converting CSV to Excel Workbook")
try: try:
@ -48,7 +48,7 @@ class RAGFlowExcelParser:
raise Exception(f"Failed to parse CSV and convert to Excel Workbook: {e_csv}") raise Exception(f"Failed to parse CSV and convert to Excel Workbook: {e_csv}")
try: try:
return load_workbook(file_like_object,data_only= True) return load_workbook(file_like_object, data_only=True)
except Exception as e: except Exception as e:
logging.info(f"openpyxl load error: {e}, try pandas instead") logging.info(f"openpyxl load error: {e}, try pandas instead")
try: try:
@ -59,7 +59,7 @@ class RAGFlowExcelParser:
except Exception as ex: except Exception as ex:
logging.info(f"pandas with default engine load error: {ex}, try calamine instead") logging.info(f"pandas with default engine load error: {ex}, try calamine instead")
file_like_object.seek(0) file_like_object.seek(0)
df = pd.read_excel(file_like_object, engine='calamine') df = pd.read_excel(file_like_object, engine="calamine")
return RAGFlowExcelParser._dataframe_to_workbook(df) return RAGFlowExcelParser._dataframe_to_workbook(df)
except Exception as e_pandas: except Exception as e_pandas:
raise Exception(f"pandas.read_excel error: {e_pandas}, original openpyxl error: {e}") raise Exception(f"pandas.read_excel error: {e_pandas}, original openpyxl error: {e}")
@ -116,9 +116,7 @@ class RAGFlowExcelParser:
tb = "" tb = ""
tb += f"<table><caption>{sheetname}</caption>" tb += f"<table><caption>{sheetname}</caption>"
tb += tb_rows_0 tb += tb_rows_0
for r in list( for r in list(rows[1 + chunk_i * chunk_rows : min(1 + (chunk_i + 1) * chunk_rows, len(rows))]):
rows[1 + chunk_i * chunk_rows: min(1 + (chunk_i + 1) * chunk_rows, len(rows))]
):
tb += "<tr>" tb += "<tr>"
for i, c in enumerate(r): for i, c in enumerate(r):
if c.value is None: if c.value is None:
@ -133,8 +131,16 @@ class RAGFlowExcelParser:
def markdown(self, fnm): def markdown(self, fnm):
import pandas as pd import pandas as pd
file_like_object = BytesIO(fnm) if not isinstance(fnm, str) else fnm file_like_object = BytesIO(fnm) if not isinstance(fnm, str) else fnm
df = pd.read_excel(file_like_object) try:
file_like_object.seek(0)
df = pd.read_excel(file_like_object)
except Exception as e:
logging.warning(f"Parse spreadsheet error: {e}, trying to interpret as CSV file")
file_like_object.seek(0)
df = pd.read_csv(file_like_object)
df = df.replace(r"^\s*$", "", regex=True)
return df.to_markdown(index=False) return df.to_markdown(index=False)
def __call__(self, fnm): def __call__(self, fnm):

View File

@ -29,7 +29,6 @@ redis:
db: 1 db: 1
password: '${REDIS_PASSWORD:-infini_rag_flow}' password: '${REDIS_PASSWORD:-infini_rag_flow}'
host: '${REDIS_HOST:-redis}:6379' host: '${REDIS_HOST:-redis}:6379'
# postgres: # postgres:
# name: '${POSTGRES_DBNAME:-rag_flow}' # name: '${POSTGRES_DBNAME:-rag_flow}'
# user: '${POSTGRES_USER:-rag_flow}' # user: '${POSTGRES_USER:-rag_flow}'
@ -65,15 +64,26 @@ redis:
# secret: 'secret' # secret: 'secret'
# tenant_id: 'tenant_id' # tenant_id: 'tenant_id'
# container_name: 'container_name' # container_name: 'container_name'
# The OSS object storage uses the MySQL configuration above by default. If you need to switch to another object storage service, please uncomment and configure the following parameters.
# opendal:
# scheme: 'mysql' # Storage type, such as s3, oss, azure, etc.
# config:
# oss_table: 'opendal_storage'
# user_default_llm: # user_default_llm:
# factory: 'Tongyi-Qianwen' # factory: 'BAAI'
# api_key: 'sk-xxxxxxxxxxxxx' # api_key: 'backup'
# base_url: '' # base_url: 'backup_base_url'
# default_models: # default_models:
# chat_model: 'qwen-plus' # chat_model:
# embedding_model: 'BAAI/bge-large-zh-v1.5@BAAI' # name: 'qwen2.5-7b-instruct'
# rerank_model: '' # factory: 'xxxx'
# asr_model: '' # api_key: 'xxxx'
# base_url: 'https://api.xx.com'
# embedding_model:
# name: 'bge-m3'
# rerank_model: 'bge-reranker-v2'
# asr_model:
# model: 'whisper-large-v3' # alias of name
# image2text_model: '' # image2text_model: ''
# oauth: # oauth:
# oauth2: # oauth2:
@ -109,3 +119,14 @@ redis:
# switch: false # switch: false
# component: false # component: false
# dataset: false # dataset: false
# smtp:
# mail_server: ""
# mail_port: 465
# mail_use_ssl: true
# mail_use_tls: false
# mail_username: ""
# mail_password: ""
# mail_default_sender:
# - "RAGFlow" # display name
# - "" # sender email address
# mail_frontend_url: "https://your-frontend.example.com"

View File

@ -73,11 +73,13 @@ class Chunker(ProcessBase):
def _general(self, from_upstream: ChunkerFromUpstream): def _general(self, from_upstream: ChunkerFromUpstream):
self.callback(random.randint(1, 5) / 100.0, "Start to chunk via `General`.") self.callback(random.randint(1, 5) / 100.0, "Start to chunk via `General`.")
if from_upstream.output_format in ["markdown", "text"]: if from_upstream.output_format in ["markdown", "text", "html"]:
if from_upstream.output_format == "markdown": if from_upstream.output_format == "markdown":
payload = from_upstream.markdown_result payload = from_upstream.markdown_result
else: # == "text" elif from_upstream.output_format == "text":
payload = from_upstream.text_result payload = from_upstream.text_result
else: # == "html"
payload = from_upstream.html_result
if not payload: if not payload:
payload = "" payload = ""
@ -90,6 +92,7 @@ class Chunker(ProcessBase):
) )
return [{"text": c} for c in cks] return [{"text": c} for c in cks]
# json
sections, section_images = [], [] sections, section_images = [], []
for o in from_upstream.json_result or []: for o in from_upstream.json_result or []:
sections.append((o.get("text", ""), o.get("position_tag", ""))) sections.append((o.get("text", ""), o.get("position_tag", "")))

View File

@ -29,7 +29,7 @@ class ChunkerFromUpstream(BaseModel):
json_result: list[dict[str, Any]] | None = Field(default=None, alias="json") json_result: list[dict[str, Any]] | None = Field(default=None, alias="json")
markdown_result: str | None = Field(default=None, alias="markdown") markdown_result: str | None = Field(default=None, alias="markdown")
text_result: str | None = Field(default=None, alias="text") text_result: str | None = Field(default=None, alias="text")
html_result: str | None = Field(default=None, alias="html") html_result: list[str] | None = Field(default=None, alias="html")
model_config = ConfigDict(populate_by_name=True, extra="forbid") model_config = ConfigDict(populate_by_name=True, extra="forbid")

View File

@ -12,6 +12,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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 random import random
import trio import trio
@ -29,8 +30,18 @@ class ParserParam(ProcessParamBase):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.allowed_output_format = { self.allowed_output_format = {
"pdf": ["json", "markdown"], "pdf": [
"excel": ["json", "markdown", "html"], "json",
"markdown",
],
"spreadsheet": [
"json",
"markdown",
"html",
],
"word": [
"json",
],
"ppt": [], "ppt": [],
"image": [], "image": [],
"email": [], "email": [],
@ -44,12 +55,29 @@ class ParserParam(ProcessParamBase):
"parse_method": "deepdoc", # deepdoc/plain_text/vlm "parse_method": "deepdoc", # deepdoc/plain_text/vlm
"vlm_name": "", "vlm_name": "",
"lang": "Chinese", "lang": "Chinese",
"suffix": ["pdf"], "suffix": [
"pdf",
],
"output_format": "json", "output_format": "json",
}, },
"excel": { "spreadsheet": {
"output_format": "html", "output_format": "html",
"suffix": ["xls", "xlsx", "csv"], "suffix": [
"xls",
"xlsx",
"csv",
],
},
"word": {
"suffix": [
"doc",
"docx",
],
"output_format": "json",
},
"markdown": {
"suffix": ["md", "markdown"],
"output_format": "json",
}, },
"ppt": {}, "ppt": {},
"image": { "image": {
@ -76,10 +104,15 @@ class ParserParam(ProcessParamBase):
pdf_output_format = pdf_config.get("output_format", "") pdf_output_format = pdf_config.get("output_format", "")
self.check_valid_value(pdf_output_format, "PDF output format abnormal.", self.allowed_output_format["pdf"]) self.check_valid_value(pdf_output_format, "PDF output format abnormal.", self.allowed_output_format["pdf"])
excel_config = self.setups.get("excel", "") spreadsheet_config = self.setups.get("spreadsheet", "")
if excel_config: if spreadsheet_config:
excel_output_format = excel_config.get("output_format", "") spreadsheet_output_format = spreadsheet_config.get("output_format", "")
self.check_valid_value(excel_output_format, "Excel output format abnormal.", self.allowed_output_format["excel"]) self.check_valid_value(spreadsheet_output_format, "Spreadsheet output format abnormal.", self.allowed_output_format["spreadsheet"])
doc_config = self.setups.get("doc", "")
if doc_config:
doc_output_format = doc_config.get("output_format", "")
self.check_valid_value(doc_output_format, "Word processer document output format abnormal.", self.allowed_output_format["doc"])
image_config = self.setups.get("image", "") image_config = self.setups.get("image", "")
if image_config: if image_config:
@ -93,10 +126,13 @@ class ParserParam(ProcessParamBase):
class Parser(ProcessBase): class Parser(ProcessBase):
component_name = "Parser" component_name = "Parser"
def _pdf(self, blob): def _pdf(self, from_upstream: ParserFromUpstream):
self.callback(random.randint(1, 5) / 100.0, "Start to work on a PDF.") self.callback(random.randint(1, 5) / 100.0, "Start to work on a PDF.")
blob = from_upstream.blob
conf = self._param.setups["pdf"] conf = self._param.setups["pdf"]
self.set_output("output_format", conf["output_format"]) self.set_output("output_format", conf["output_format"])
if conf.get("parse_method") == "deepdoc": if conf.get("parse_method") == "deepdoc":
bboxes = RAGFlowPdfParser().parse_into_bboxes(blob, callback=self.callback) bboxes = RAGFlowPdfParser().parse_into_bboxes(blob, callback=self.callback)
elif conf.get("parse_method") == "plain_text": elif conf.get("parse_method") == "plain_text":
@ -110,6 +146,7 @@ class Parser(ProcessBase):
for t, poss in lines: for t, poss in lines:
pn, x0, x1, top, bott = poss.split(" ") pn, x0, x1, top, bott = poss.split(" ")
bboxes.append({"page_number": int(pn), "x0": float(x0), "x1": float(x1), "top": float(top), "bottom": float(bott), "text": t}) bboxes.append({"page_number": int(pn), "x0": float(x0), "x1": float(x1), "top": float(top), "bottom": float(bott), "text": t})
if conf.get("output_format") == "json": if conf.get("output_format") == "json":
self.set_output("json", bboxes) self.set_output("json", bboxes)
if conf.get("output_format") == "markdown": if conf.get("output_format") == "markdown":
@ -123,23 +160,93 @@ class Parser(ProcessBase):
mkdn += b.get("text", "") + "\n" mkdn += b.get("text", "") + "\n"
self.set_output("markdown", mkdn) self.set_output("markdown", mkdn)
def _excel(self, blob): def _spreadsheet(self, from_upstream: ParserFromUpstream):
self.callback(random.randint(1, 5) / 100.0, "Start to work on a Excel.") self.callback(random.randint(1, 5) / 100.0, "Start to work on a Spreadsheet.")
conf = self._param.setups["excel"]
blob = from_upstream.blob
conf = self._param.setups["spreadsheet"]
self.set_output("output_format", conf["output_format"]) self.set_output("output_format", conf["output_format"])
excel_parser = ExcelParser()
print("spreadsheet {conf=}", flush=True)
spreadsheet_parser = ExcelParser()
if conf.get("output_format") == "html": if conf.get("output_format") == "html":
html = excel_parser.html(blob, 1000000000) html = spreadsheet_parser.html(blob, 1000000000)
self.set_output("html", html) self.set_output("html", html)
elif conf.get("output_format") == "json": elif conf.get("output_format") == "json":
self.set_output("json", [{"text": txt} for txt in excel_parser(blob) if txt]) self.set_output("json", [{"text": txt} for txt in spreadsheet_parser(blob) if txt])
elif conf.get("output_format") == "markdown": elif conf.get("output_format") == "markdown":
self.set_output("markdown", excel_parser.markdown(blob)) self.set_output("markdown", spreadsheet_parser.markdown(blob))
def _word(self, from_upstream: ParserFromUpstream):
from tika import parser as word_parser
self.callback(random.randint(1, 5) / 100.0, "Start to work on a Word Processor Document")
blob = from_upstream.blob
name = from_upstream.name
conf = self._param.setups["word"]
self.set_output("output_format", conf["output_format"])
print("word {conf=}", flush=True)
doc_parsed = word_parser.from_buffer(blob)
sections = []
if doc_parsed.get("content"):
sections = doc_parsed["content"].split("\n")
sections = [{"text": section} for section in sections if section]
else:
logging.warning(f"tika.parser got empty content from {name}.")
# json
assert conf.get("output_format") == "json", "have to be json for doc"
if conf.get("output_format") == "json":
self.set_output("json", sections)
def _markdown(self, from_upstream: ParserFromUpstream):
from functools import reduce
from rag.app.naive import Markdown as naive_markdown_parser
from rag.nlp import concat_img
self.callback(random.randint(1, 5) / 100.0, "Start to work on a Word Processor Document")
blob = from_upstream.blob
name = from_upstream.name
conf = self._param.setups["markdown"]
self.set_output("output_format", conf["output_format"])
print("markdown {conf=}", flush=True)
markdown_parser = naive_markdown_parser()
sections, tables = markdown_parser(name, blob, separate_tables=False)
# json
assert conf.get("output_format") == "json", "have to be json for doc"
if conf.get("output_format") == "json":
json_results = []
for section_text, _ in sections:
json_result = {
"text": section_text,
}
images = markdown_parser.get_pictures(section_text) if section_text else None
if images:
# If multiple images found, combine them using concat_img
combined_image = reduce(concat_img, images) if len(images) > 1 else images[0]
json_result["image"] = combined_image
json_results.append(json_result)
self.set_output("json", json_results)
async def _invoke(self, **kwargs): async def _invoke(self, **kwargs):
function_map = { function_map = {
"pdf": self._pdf, "pdf": self._pdf,
"excel": self._excel, "markdown": self._markdown,
"spreadsheet": self._spreadsheet,
"word": self._word
} }
try: try:
from_upstream = ParserFromUpstream.model_validate(kwargs) from_upstream = ParserFromUpstream.model_validate(kwargs)
@ -150,5 +257,5 @@ class Parser(ProcessBase):
for p_type, conf in self._param.setups.items(): for p_type, conf in self._param.setups.items():
if from_upstream.name.split(".")[-1].lower() not in conf.get("suffix", []): if from_upstream.name.split(".")[-1].lower() not in conf.get("suffix", []):
continue continue
await trio.to_thread.run_sync(function_map[p_type], from_upstream.blob) await trio.to_thread.run_sync(function_map[p_type], from_upstream)
break break

View File

@ -23,16 +23,31 @@
], ],
"output_format": "json" "output_format": "json"
}, },
"excel": { "spreadsheet": {
"output_format": "html",
"suffix": [ "suffix": [
"xls", "xls",
"xlsx", "xlsx",
"csv" "csv"
] ],
"output_format": "html"
},
"word": {
"suffix": [
"doc",
"docx"
],
"output_format": "json"
},
"markdown": {
"suffix": [
"md",
"markdown"
],
"output_format": "json"
} }
} }
} }
}
}, },
"downstream": ["Chunker:0"], "downstream": ["Chunker:0"],
"upstream": ["Begin"] "upstream": ["Begin"]

View File

@ -31,7 +31,7 @@ class TokenizerFromUpstream(BaseModel):
json_result: list[dict[str, Any]] | None = Field(default=None, alias="json") json_result: list[dict[str, Any]] | None = Field(default=None, alias="json")
markdown_result: str | None = Field(default=None, alias="markdown") markdown_result: str | None = Field(default=None, alias="markdown")
text_result: str | None = Field(default=None, alias="text") text_result: str | None = Field(default=None, alias="text")
html_result: str | None = Field(default=None, alias="html") html_result: list[str] | None = Field(default=None, alias="html")
model_config = ConfigDict(populate_by_name=True, extra="forbid") model_config = ConfigDict(populate_by_name=True, extra="forbid")

View File

@ -117,11 +117,13 @@ class Tokenizer(ProcessBase):
ck["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(ck["content_ltks"]) ck["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(ck["content_ltks"])
if i % 100 == 99: if i % 100 == 99:
self.callback(i * 1.0 / len(chunks) / parts) self.callback(i * 1.0 / len(chunks) / parts)
elif from_upstream.output_format in ["markdown", "text"]: elif from_upstream.output_format in ["markdown", "text", "html"]:
if from_upstream.output_format == "markdown": if from_upstream.output_format == "markdown":
payload = from_upstream.markdown_result payload = from_upstream.markdown_result
else: # == "text" elif from_upstream.output_format == "text":
payload = from_upstream.text_result payload = from_upstream.text_result
else: # == "html"
payload = from_upstream.html_result
if not payload: if not payload:
return "" return ""

View File

@ -124,17 +124,19 @@ class Base(ABC):
mime = "image/jpeg" mime = "image/jpeg"
b64 = base64.b64encode(data).decode("utf-8") b64 = base64.b64encode(data).decode("utf-8")
return f"data:{mime};base64,{b64}" return f"data:{mime};base64,{b64}"
buffered = BytesIO() with BytesIO() as buffered:
fmt = "JPEG" fmt = "JPEG"
try: try:
image.save(buffered, format="JPEG") image.save(buffered, format="JPEG")
except Exception: except Exception:
buffered = BytesIO() # reset buffer before saving PNG # reset buffer before saving PNG
image.save(buffered, format="PNG") buffered.seek(0)
fmt = "PNG" buffered.truncate()
data = buffered.getvalue() image.save(buffered, format="PNG")
b64 = base64.b64encode(data).decode("utf-8") fmt = "PNG"
mime = f"image/{fmt.lower()}" data = buffered.getvalue()
b64 = base64.b64encode(data).decode("utf-8")
mime = f"image/{fmt.lower()}"
return f"data:{mime};base64,{b64}" return f"data:{mime};base64,{b64}"
def prompt(self, b64): def prompt(self, b64):

View File

@ -751,6 +751,8 @@ class SILICONFLOWEmbed(Base):
token_count = 0 token_count = 0
for i in range(0, len(texts), batch_size): for i in range(0, len(texts), batch_size):
texts_batch = texts[i : i + batch_size] texts_batch = texts[i : i + batch_size]
texts_batch = [" " if not text.strip() else text for text in texts_batch]
payload = { payload = {
"model": self.model_name, "model": self.model_name,
"input": texts_batch, "input": texts_batch,

View File

@ -518,7 +518,7 @@ def hierarchical_merge(bull, sections, depth):
return res return res
def naive_merge(sections, chunk_token_num=128, delimiter="\n。;!?", overlapped_percent=0): def naive_merge(sections: str | list, chunk_token_num=128, delimiter="\n。;!?", overlapped_percent=0):
from deepdoc.parser.pdf_parser import RAGFlowPdfParser from deepdoc.parser.pdf_parser import RAGFlowPdfParser
if not sections: if not sections:
return [] return []

View File

@ -15,6 +15,7 @@ type RAGFlowFormItemProps = {
tooltip?: ReactNode; tooltip?: ReactNode;
children: ReactNode | ((field: ControllerRenderProps) => ReactNode); children: ReactNode | ((field: ControllerRenderProps) => ReactNode);
horizontal?: boolean; horizontal?: boolean;
required?: boolean;
}; };
export function RAGFlowFormItem({ export function RAGFlowFormItem({
@ -23,6 +24,7 @@ export function RAGFlowFormItem({
tooltip, tooltip,
children, children,
horizontal = false, horizontal = false,
required = false,
}: RAGFlowFormItemProps) { }: RAGFlowFormItemProps) {
const form = useFormContext(); const form = useFormContext();
return ( return (
@ -35,7 +37,11 @@ export function RAGFlowFormItem({
'flex items-center': horizontal, 'flex items-center': horizontal,
})} })}
> >
<FormLabel tooltip={tooltip} className={cn({ 'w-1/4': horizontal })}> <FormLabel
required={required}
tooltip={tooltip}
className={cn({ 'w-1/4': horizontal })}
>
{label} {label}
</FormLabel> </FormLabel>
<FormControl> <FormControl>

View File

@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-colors-background-neutral-standard p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-bg-base p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className, className,
)} )}
{...props} {...props}

View File

@ -24,9 +24,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks'; import { useDebounce } from 'ahooks';
import { get, set } from 'lodash'; import { get, set } from 'lodash';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'umi'; import { useParams, useSearchParams } from 'umi';
import { v4 as uuid } from 'uuid';
import { import {
useGetPaginationWithRouter, useGetPaginationWithRouter,
useHandleSearchChange, useHandleSearchChange,
@ -80,7 +78,7 @@ export const EmptyDsl = {
component_name: 'Begin', component_name: 'Begin',
params: {}, params: {},
}, },
downstream: ['Answer:China'], // other edge target is downstream, edge source is current node id downstream: [], // other edge target is downstream, edge source is current node id
upstream: [], // edge source is upstream, edge target is current node id upstream: [], // edge source is upstream, edge target is current node id
}, },
}, },
@ -96,21 +94,11 @@ export const EmptyDsl = {
}; };
export const useFetchAgentTemplates = () => { export const useFetchAgentTemplates = () => {
const { t } = useTranslation();
const { data } = useQuery<IFlowTemplate[]>({ const { data } = useQuery<IFlowTemplate[]>({
queryKey: [AgentApiAction.FetchAgentTemplates], queryKey: [AgentApiAction.FetchAgentTemplates],
initialData: [], initialData: [],
queryFn: async () => { queryFn: async () => {
const { data } = await agentService.listTemplates(); const { data } = await agentService.listTemplates();
if (Array.isArray(data?.data)) {
data.data.unshift({
id: uuid(),
title: t('flow.blank'),
description: t('flow.createFromNothing'),
dsl: EmptyDsl,
});
}
return data.data; return data.data;
}, },

View File

@ -41,8 +41,8 @@ export interface DSL {
path?: string[]; path?: string[];
answer?: any[]; answer?: any[];
graph?: IGraph; graph?: IGraph;
messages: Message[]; messages?: Message[];
reference: IReference[]; reference?: IReference[];
globals: Record<string, any>; globals: Record<string, any>;
retrieval: IReference[]; retrieval: IReference[];
} }

View File

@ -934,7 +934,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
exceptionMethod: 'Exception method', exceptionMethod: 'Exception method',
maxRounds: 'Max reflection rounds', maxRounds: 'Max reflection rounds',
delayEfterError: 'Delay after error', delayEfterError: 'Delay after error',
maxRetries: 'Max retries', maxRetries: 'Max reflection rounds',
advancedSettings: 'Advanced Settings', advancedSettings: 'Advanced Settings',
addTools: 'Add Tools', addTools: 'Add Tools',
sysPromptDefultValue: ` sysPromptDefultValue: `

View File

@ -892,7 +892,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
exceptionMethod: '异常处理方法', exceptionMethod: '异常处理方法',
maxRounds: '最大反思轮数', maxRounds: '最大反思轮数',
delayEfterError: '错误后延迟', delayEfterError: '错误后延迟',
maxRetries: '最大重试次数', maxRetries: '最大反思轮数',
advancedSettings: '高级设置', advancedSettings: '高级设置',
addTools: '添加工具', addTools: '添加工具',
sysPromptDefultValue: ` sysPromptDefultValue: `

View File

@ -57,13 +57,6 @@ const FormSchema = z.object({
// ) // )
// .optional(), // .optional(),
message_history_window_size: z.coerce.number(), message_history_window_size: z.coerce.number(),
tools: z
.array(
z.object({
component_name: z.string(),
}),
)
.optional(),
...LlmSettingSchema, ...LlmSettingSchema,
max_retries: z.coerce.number(), max_retries: z.coerce.number(),
delay_after_error: z.coerce.number().optional(), delay_after_error: z.coerce.number().optional(),

View File

@ -1,15 +1,21 @@
import { useFetchModelId } from '@/hooks/logic-hooks'; import { useFetchModelId } from '@/hooks/logic-hooks';
import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { get, isEmpty } from 'lodash'; import { get, isEmpty, omit } from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { initialAgentValues } from '../../constant'; import { initialAgentValues } from '../../constant';
// You need to exclude the mcp and tools fields that are not in the form,
// otherwise the form data update will reset the tools or mcp data to an array
function omitToolsAndMcp(values: Record<string, any>) {
return omit(values, ['mcp', 'tools']);
}
export function useValues(node?: RAGFlowNodeType) { export function useValues(node?: RAGFlowNodeType) {
const llmId = useFetchModelId(); const llmId = useFetchModelId();
const defaultValues = useMemo( const defaultValues = useMemo(
() => ({ () => ({
...initialAgentValues, ...omitToolsAndMcp(initialAgentValues),
llm_id: llmId, llm_id: llmId,
prompts: '', prompts: '',
}), }),
@ -24,7 +30,7 @@ export function useValues(node?: RAGFlowNodeType) {
} }
return { return {
...formData, ...omitToolsAndMcp(formData),
prompts: get(formData, 'prompts.0.content', ''), prompts: get(formData, 'prompts.0.content', ''),
}; };
}, [defaultValues, node?.data?.form]); }, [defaultValues, node?.data?.form]);

View File

@ -1,71 +1,17 @@
import { useToast } from '@/components/hooks/use-toast';
import { FileMimeType, Platform } from '@/constants/common';
import { useSetModalState } from '@/hooks/common-hooks';
import { useFetchAgent } from '@/hooks/use-agent-request'; import { useFetchAgent } from '@/hooks/use-agent-request';
import { IGraph } from '@/interfaces/database/flow';
import { downloadJsonFile } from '@/utils/file-util'; import { downloadJsonFile } from '@/utils/file-util';
import { message } from 'antd';
import isEmpty from 'lodash/isEmpty';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useBuildDslData } from './use-build-dsl'; import { useBuildDslData } from './use-build-dsl';
import { useSetGraphInfo } from './use-set-graph';
export const useHandleExportOrImportJsonFile = () => { export const useHandleExportJsonFile = () => {
const { buildDslData } = useBuildDslData(); const { buildDslData } = useBuildDslData();
const {
visible: fileUploadVisible,
hideModal: hideFileUploadModal,
showModal: showFileUploadModal,
} = useSetModalState();
const setGraphInfo = useSetGraphInfo();
const { data } = useFetchAgent(); const { data } = useFetchAgent();
const { t } = useTranslation();
const { toast } = useToast();
const onFileUploadOk = useCallback(
async ({
fileList,
platform,
}: {
fileList: File[];
platform: Platform;
}) => {
console.log('🚀 ~ useHandleExportOrImportJsonFile ~ platform:', platform);
if (fileList.length > 0) {
const file = fileList[0];
if (file.type !== FileMimeType.Json) {
toast({ title: t('flow.jsonUploadTypeErrorMessage') });
return;
}
const graphStr = await file.text();
const errorMessage = t('flow.jsonUploadContentErrorMessage');
try {
const graph = JSON.parse(graphStr);
if (graphStr && !isEmpty(graph) && Array.isArray(graph?.nodes)) {
setGraphInfo(graph ?? ({} as IGraph));
hideFileUploadModal();
} else {
message.error(errorMessage);
}
} catch (error) {
message.error(errorMessage);
}
}
},
[hideFileUploadModal, setGraphInfo, t, toast],
);
const handleExportJson = useCallback(() => { const handleExportJson = useCallback(() => {
downloadJsonFile(buildDslData().graph, `${data.title}.json`); downloadJsonFile(buildDslData().graph, `${data.title}.json`);
}, [buildDslData, data.title]); }, [buildDslData, data.title]);
return { return {
fileUploadVisible,
handleExportJson, handleExportJson,
handleImportJson: showFileUploadModal,
hideFileUploadModal,
onFileUploadOk,
}; };
}; };

View File

@ -24,7 +24,6 @@ import { ReactFlowProvider } from '@xyflow/react';
import { import {
ChevronDown, ChevronDown,
CirclePlay, CirclePlay,
Download,
History, History,
LaptopMinimalCheck, LaptopMinimalCheck,
Logs, Logs,
@ -37,7 +36,7 @@ import { useTranslation } from 'react-i18next';
import { useParams } from 'umi'; import { useParams } from 'umi';
import AgentCanvas from './canvas'; import AgentCanvas from './canvas';
import { DropdownProvider } from './canvas/context'; import { DropdownProvider } from './canvas/context';
import { useHandleExportOrImportJsonFile } 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';
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query'; import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
import { import {
@ -46,7 +45,6 @@ import {
useWatchAgentChange, useWatchAgentChange,
} from './hooks/use-save-graph'; } from './hooks/use-save-graph';
import { SettingDialog } from './setting-dialog'; import { SettingDialog } from './setting-dialog';
import { UploadAgentDialog } from './upload-agent-dialog';
import { useAgentHistoryManager } from './use-agent-history-manager'; import { useAgentHistoryManager } from './use-agent-history-manager';
import { VersionDialog } from './version-dialog'; import { VersionDialog } from './version-dialog';
@ -71,13 +69,8 @@ export default function Agent() {
} = useSetModalState(); } = useSetModalState();
const { t } = useTranslation(); const { t } = useTranslation();
useAgentHistoryManager(); useAgentHistoryManager();
const {
handleExportJson, const { handleExportJson } = useHandleExportJsonFile();
handleImportJson,
fileUploadVisible,
onFileUploadOk,
hideFileUploadModal,
} = useHandleExportOrImportJsonFile();
const { saveGraph, loading } = useSaveGraph(); const { saveGraph, loading } = useSaveGraph();
const { flowDetail: agentDetail } = useFetchDataOnMount(); const { flowDetail: agentDetail } = useFetchDataOnMount();
const inputs = useGetBeginNodeDataInputs(); const inputs = useGetBeginNodeDataInputs();
@ -158,11 +151,6 @@ export default function Agent() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<AgentDropdownMenuItem onClick={handleImportJson}>
<Download />
{t('flow.import')}
</AgentDropdownMenuItem>
<DropdownMenuSeparator />
<AgentDropdownMenuItem onClick={handleExportJson}> <AgentDropdownMenuItem onClick={handleExportJson}>
<Upload /> <Upload />
{t('flow.export')} {t('flow.export')}
@ -193,12 +181,6 @@ export default function Agent() {
></AgentCanvas> ></AgentCanvas>
</DropdownProvider> </DropdownProvider>
</ReactFlowProvider> </ReactFlowProvider>
{fileUploadVisible && (
<UploadAgentDialog
hideModal={hideFileUploadModal}
onOk={onFileUploadOk}
></UploadAgentDialog>
)}
{embedVisible && ( {embedVisible && (
<EmbedDialog <EmbedDialog
visible={embedVisible} visible={embedVisible}

View File

@ -27,9 +27,11 @@ export default function AgentTemplates() {
const [selectMenuItem, setSelectMenuItem] = useState<string>( const [selectMenuItem, setSelectMenuItem] = useState<string>(
MenuItemKey.Recommended, MenuItemKey.Recommended,
); );
useEffect(() => { useEffect(() => {
setTemplateList(list); setTemplateList(list);
}, [list]); }, [list]);
const { const {
visible: creatingVisible, visible: creatingVisible,
hideModal: hideCreatingModal, hideModal: hideCreatingModal,
@ -110,10 +112,9 @@ export default function AgentTemplates() {
<main className="flex-1 bg-text-title-invert/50 h-dvh"> <main className="flex-1 bg-text-title-invert/50 h-dvh">
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 max-h-[94vh] overflow-auto px-8 pt-8"> <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 max-h-[94vh] overflow-auto px-8 pt-8">
{tempListFilter?.map((x, index) => { {tempListFilter?.map((x) => {
return ( return (
<TemplateCard <TemplateCard
isCreate={index === 0}
key={x.id} key={x.id}
data={x} data={x}
showModal={showModal} showModal={showModal}

View File

@ -0,0 +1,4 @@
export enum FlowType {
Agent = 'agent',
Flow = 'flow',
}

View File

@ -6,16 +6,18 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { IModalProps } from '@/interfaces/common';
import { TagRenameId } from '@/pages/add-knowledge/constant'; import { TagRenameId } from '@/pages/add-knowledge/constant';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CreateAgentForm } from './create-agent-form'; import { CreateAgentForm, CreateAgentFormProps } from './create-agent-form';
type CreateAgentDialogProps = CreateAgentFormProps;
export function CreateAgentDialog({ export function CreateAgentDialog({
hideModal, hideModal,
onOk, onOk,
loading, loading,
}: IModalProps<any>) { shouldChooseAgent,
}: CreateAgentDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -24,7 +26,11 @@ export function CreateAgentDialog({
<DialogHeader> <DialogHeader>
<DialogTitle>{t('flow.createGraph')}</DialogTitle> <DialogTitle>{t('flow.createGraph')}</DialogTitle>
</DialogHeader> </DialogHeader>
<CreateAgentForm hideModal={hideModal} onOk={onOk}></CreateAgentForm> <CreateAgentForm
hideModal={hideModal}
onOk={onOk}
shouldChooseAgent={shouldChooseAgent}
></CreateAgentForm>
<DialogFooter> <DialogFooter>
<ButtonLoading type="submit" form={TagRenameId} loading={loading}> <ButtonLoading type="submit" form={TagRenameId} loading={loading}>
{t('common.save')} {t('common.save')}

View File

@ -4,38 +4,94 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { import { RAGFlowFormItem } from '@/components/ragflow-form';
Form, import { Card, CardContent } from '@/components/ui/card';
FormControl, import { Form } from '@/components/ui/form';
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { cn } from '@/lib/utils';
import { TagRenameId } from '@/pages/add-knowledge/constant'; import { TagRenameId } from '@/pages/add-knowledge/constant';
import { BrainCircuit, Check, Route } from 'lucide-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FlowType } from './constant';
import { NameFormField, NameFormSchema } from './name-form-field';
export function CreateAgentForm({ hideModal, onOk }: IModalProps<any>) { export type CreateAgentFormProps = IModalProps<any> & {
shouldChooseAgent?: boolean;
};
type FlowTypeCardProps = {
value?: FlowType;
onChange?: (value: FlowType) => void;
};
function FlowTypeCards({ value, onChange }: FlowTypeCardProps) {
const handleChange = useCallback(
(value: FlowType) => () => {
onChange?.(value);
},
[onChange],
);
return (
<section className="flex gap-10">
{Object.values(FlowType).map((val) => {
const isActive = value === val;
return (
<Card
key={val}
className={cn('flex-1 rounded-lg border bg-transparent', {
'border-text-primary': isActive,
'border-border-default': !isActive,
})}
>
<CardContent
onClick={handleChange(val)}
className={cn(
'cursor-pointer p-5 text-text-secondary flex justify-between items-center',
{
'text-text-primary': isActive,
},
)}
>
<div className="flex gap-2">
{val === FlowType.Agent ? (
<BrainCircuit className="size-6" />
) : (
<Route className="size-6" />
)}
<p>{val}</p>
</div>
{isActive && <Check />}
</CardContent>
</Card>
);
})}
</section>
);
}
export const FormSchema = z.object({
...NameFormSchema,
tag: z.string().trim().optional(),
description: z.string().trim().optional(),
type: z.nativeEnum(FlowType).optional(),
});
export type FormSchemaType = z.infer<typeof FormSchema>;
export function CreateAgentForm({
hideModal,
onOk,
shouldChooseAgent = false,
}: CreateAgentFormProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const FormSchema = z.object({
name: z
.string()
.min(1, {
message: t('common.namePlaceholder'),
})
.trim(),
tag: z.string().trim().optional(),
description: z.string().trim().optional(),
});
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<FormSchemaType>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
defaultValues: { name: '' }, defaultValues: { name: '', type: FlowType.Agent },
}); });
async function onSubmit(data: z.infer<typeof FormSchema>) { async function onSubmit(data: FormSchemaType) {
const ret = await onOk?.(data); const ret = await onOk?.(data);
if (ret) { if (ret) {
hideModal?.(); hideModal?.();
@ -49,57 +105,12 @@ export function CreateAgentForm({ hideModal, onOk }: IModalProps<any>) {
className="space-y-6" className="space-y-6"
id={TagRenameId} id={TagRenameId}
> >
<FormField {shouldChooseAgent && (
control={form.control} <RAGFlowFormItem required name="type" label={t('common.type')}>
name="name" <FlowTypeCards></FlowTypeCards>
render={({ field }) => ( </RAGFlowFormItem>
<FormItem> )}
<FormLabel>{t('common.name')}</FormLabel> <NameFormField></NameFormField>
<FormControl>
<Input
placeholder={t('common.namePlaceholder')}
{...field}
autoComplete="off"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="tag"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.tag')}</FormLabel>
<FormControl>
<Input
placeholder={t('flow.tagPlaceholder')}
{...field}
autoComplete="off"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.description')}</FormLabel>
<FormControl>
<Input
placeholder={t('flow.descriptionPlaceholder')}
{...field}
autoComplete="off"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/> */}
</form> </form>
</Form> </Form>
); );

View File

@ -0,0 +1,42 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { EmptyDsl, useSetAgent } from '@/hooks/use-agent-request';
import { DSL } from '@/interfaces/database/agent';
import { useCallback } from 'react';
import { FlowType } from '../constant';
import { FormSchemaType } from '../create-agent-form';
export function useCreateAgentOrPipeline() {
const { loading, setAgent } = useSetAgent();
const {
visible: creatingVisible,
hideModal: hideCreatingModal,
showModal: showCreatingModal,
} = useSetModalState();
const createAgent = useCallback(
async (name: string) => {
return setAgent({ title: name, dsl: EmptyDsl as DSL });
},
[setAgent],
);
const handleCreateAgentOrPipeline = useCallback(
async (data: FormSchemaType) => {
if (data.type === FlowType.Agent) {
const ret = await createAgent(data.name);
if (ret.code === 0) {
hideCreatingModal();
}
}
},
[createAgent, hideCreatingModal],
);
return {
loading,
creatingVisible,
hideCreatingModal,
showCreatingModal,
handleCreateAgentOrPipeline,
};
}

View File

@ -1,14 +1,24 @@
import ListFilterBar from '@/components/list-filter-bar'; import ListFilterBar from '@/components/list-filter-bar';
import { RenameDialog } from '@/components/rename-dialog'; import { RenameDialog } from '@/components/rename-dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchAgentListByPage } from '@/hooks/use-agent-request'; import { useFetchAgentListByPage } from '@/hooks/use-agent-request';
import { t } from 'i18next'; import { t } from 'i18next';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { Plus } from 'lucide-react'; import { Clipboard, ClipboardPlus, FileInput, Plus } from 'lucide-react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { AgentCard } from './agent-card'; import { AgentCard } from './agent-card';
import { CreateAgentDialog } from './create-agent-dialog';
import { useCreateAgentOrPipeline } from './hooks/use-create-agent';
import { UploadAgentDialog } from './upload-agent-dialog';
import { useHandleImportJsonFile } from './use-import-json';
import { useRenameAgent } from './use-rename-agent'; import { useRenameAgent } from './use-rename-agent';
export default function Agents() { export default function Agents() {
@ -25,6 +35,21 @@ export default function Agents() {
showAgentRenameModal, showAgentRenameModal,
} = useRenameAgent(); } = useRenameAgent();
const {
creatingVisible,
hideCreatingModal,
showCreatingModal,
loading,
handleCreateAgentOrPipeline,
} = useCreateAgentOrPipeline();
const {
handleImportJson,
fileUploadVisible,
onFileUploadOk,
hideFileUploadModal,
} = useHandleImportJsonFile();
const handlePageChange = useCallback( const handlePageChange = useCallback(
(page: number, pageSize?: number) => { (page: number, pageSize?: number) => {
setPagination({ page, pageSize }); setPagination({ page, pageSize });
@ -41,10 +66,37 @@ export default function Agents() {
onSearchChange={handleInputChange} onSearchChange={handleInputChange}
icon="agent" icon="agent"
> >
<Button onClick={navigateToAgentTemplates}> <DropdownMenu>
<Plus className="mr-2 h-4 w-4" /> <DropdownMenuTrigger>
{t('flow.createGraph')} <Button>
</Button> <Plus className="mr-2 h-4 w-4" />
{t('flow.createGraph')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
justifyBetween={false}
onClick={showCreatingModal}
>
<Clipboard />
Create from Blank
</DropdownMenuItem>
<DropdownMenuItem
justifyBetween={false}
onClick={navigateToAgentTemplates}
>
<ClipboardPlus />
Create from Template
</DropdownMenuItem>
<DropdownMenuItem
justifyBetween={false}
onClick={handleImportJson}
>
<FileInput />
Import json file
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ListFilterBar> </ListFilterBar>
</div> </div>
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
@ -75,6 +127,21 @@ export default function Agents() {
loading={agentRenameLoading} loading={agentRenameLoading}
></RenameDialog> ></RenameDialog>
)} )}
{creatingVisible && (
<CreateAgentDialog
loading={loading}
visible={creatingVisible}
hideModal={hideCreatingModal}
shouldChooseAgent
onOk={handleCreateAgentOrPipeline}
></CreateAgentDialog>
)}
{fileUploadVisible && (
<UploadAgentDialog
hideModal={hideFileUploadModal}
onOk={onFileUploadOk}
></UploadAgentDialog>
)}
</section> </section>
); );
} }

View File

@ -0,0 +1,28 @@
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import i18n from '@/locales/config';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
export const NameFormSchema = {
name: z
.string()
.min(1, {
message: i18n.t('common.namePlaceholder'),
})
.trim(),
};
export function NameFormField() {
const { t } = useTranslation();
return (
<RAGFlowFormItem
name="name"
required
label={t('common.name')}
tooltip={t('flow.sqlStatementTip')}
>
<Input placeholder={t('common.namePlaceholder')} autoComplete="off" />
</RAGFlowFormItem>
);
}

View File

@ -3,7 +3,6 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { IFlowTemplate } from '@/interfaces/database/flow'; import { IFlowTemplate } from '@/interfaces/database/flow';
import i18n from '@/locales/config'; import i18n from '@/locales/config';
import { Plus } from 'lucide-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface IProps { interface IProps {
@ -12,7 +11,7 @@ interface IProps {
showModal(record: IFlowTemplate): void; showModal(record: IFlowTemplate): void;
} }
export function TemplateCard({ data, showModal, isCreate = false }: IProps) { export function TemplateCard({ data, showModal }: IProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
@ -26,41 +25,24 @@ export function TemplateCard({ data, showModal, isCreate = false }: IProps) {
return ( return (
<Card className="border-colors-outline-neutral-standard group relative min-h-40"> <Card className="border-colors-outline-neutral-standard group relative min-h-40">
<CardContent className="p-4 "> <CardContent className="p-4 ">
{isCreate && ( <div className="flex justify-start items-center gap-4 mb-4">
<div <RAGFlowAvatar
className="flex flex-col justify-center items-center gap-4 mb-4 absolute top-0 right-0 left-0 bottom-0 cursor-pointer " className="w-7 h-7"
avatar={data.avatar ? data.avatar : 'https://github.com/shadcn.png'}
name={data?.title[language] || 'CN'}
></RAGFlowAvatar>
<div className="text-[18px] font-bold ">{data?.title[language]}</div>
</div>
<p className="break-words">{data?.description[language]}</p>
<div className="group-hover:bg-gradient-to-t from-black/70 from-10% via-black/0 via-50% to-black/0 w-full h-full group-hover:block absolute top-0 left-0 hidden rounded-xl">
<Button
variant="default"
className="w-1/3 absolute bottom-4 right-4 left-4 justify-center text-center m-auto"
onClick={handleClick} onClick={handleClick}
> >
<Plus size={50} fontWeight={700} /> {t('flow.useTemplate')}
<div>{t('flow.createAgent')}</div> </Button>
</div> </div>
)}
{!isCreate && (
<>
<div className="flex justify-start items-center gap-4 mb-4">
<RAGFlowAvatar
className="w-7 h-7"
avatar={
data.avatar ? data.avatar : 'https://github.com/shadcn.png'
}
name={data?.title[language] || 'CN'}
></RAGFlowAvatar>
<div className="text-[18px] font-bold ">
{data?.title[language]}
</div>
</div>
<p className="break-words">{data?.description[language]}</p>
<div className="group-hover:bg-gradient-to-t from-black/70 from-10% via-black/0 via-50% to-black/0 w-full h-full group-hover:block absolute top-0 left-0 hidden rounded-xl">
<Button
variant="default"
className="w-1/3 absolute bottom-4 right-4 left-4 justify-center text-center m-auto"
onClick={handleClick}
>
{t('flow.useTemplate')}
</Button>
</div>
</>
)}
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@ -1,3 +1,4 @@
import { ButtonLoading } from '@/components/ui/button';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -5,7 +6,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { LoadingButton } from '@/components/ui/loading-button';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { TagRenameId } from '@/pages/add-knowledge/constant'; import { TagRenameId } from '@/pages/add-knowledge/constant';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -26,9 +26,9 @@ export function UploadAgentDialog({
</DialogHeader> </DialogHeader>
<UploadAgentForm hideModal={hideModal} onOk={onOk}></UploadAgentForm> <UploadAgentForm hideModal={hideModal} onOk={onOk}></UploadAgentForm>
<DialogFooter> <DialogFooter>
<LoadingButton type="submit" form={TagRenameId} loading={loading}> <ButtonLoading type="submit" form={TagRenameId} loading={loading}>
{t('common.save')} {t('common.save')}
</LoadingButton> </ButtonLoading>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -13,32 +13,24 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { FileMimeType, Platform } from '@/constants/common'; import { FileMimeType } from '@/constants/common';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { TagRenameId } from '@/pages/add-knowledge/constant'; import { TagRenameId } from '@/pages/add-knowledge/constant';
import { useTranslation } from 'react-i18next'; import { NameFormField, NameFormSchema } from '../name-form-field';
// const options = Object.values(Platform).map((x) => ({ label: x, value: x })); export const FormSchema = z.object({
fileList: z.array(z.instanceof(File)),
...NameFormSchema,
});
export type FormSchemaType = z.infer<typeof FormSchema>;
export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) { export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) {
const { t } = useTranslation();
const FormSchema = z.object({
platform: z
.string()
.min(1, {
message: t('common.namePlaceholder'),
})
.trim(),
fileList: z.array(z.instanceof(File)),
});
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
defaultValues: { platform: Platform.RAGFlow }, defaultValues: { name: '' },
}); });
async function onSubmit(data: z.infer<typeof FormSchema>) { async function onSubmit(data: FormSchemaType) {
console.log('🚀 ~ onSubmit ~ data:', data);
const ret = await onOk?.(data); const ret = await onOk?.(data);
if (ret) { if (ret) {
hideModal?.(); hideModal?.();
@ -52,12 +44,13 @@ export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) {
className="space-y-6" className="space-y-6"
id={TagRenameId} id={TagRenameId}
> >
<NameFormField></NameFormField>
<FormField <FormField
control={form.control} control={form.control}
name="fileList" name="fileList"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('common.name')}</FormLabel> <FormLabel required>DSL</FormLabel>
<FormControl> <FormControl>
<FileUploader <FileUploader
value={field.value} value={field.value}
@ -70,19 +63,6 @@ export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) {
</FormItem> </FormItem>
)} )}
/> />
{/* <FormField
control={form.control}
name="platform"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.name')}</FormLabel>
<FormControl>
<RAGFlowSelect {...field} options={options} />
</FormControl>
<FormMessage />
</FormItem>
)}
/> */}
</form> </form>
</Form> </Form>
); );

View File

@ -0,0 +1,56 @@
import { useToast } from '@/components/hooks/use-toast';
import { FileMimeType } from '@/constants/common';
import { useSetModalState } from '@/hooks/common-hooks';
import { EmptyDsl, useSetAgent } from '@/hooks/use-agent-request';
import { message } from 'antd';
import isEmpty from 'lodash/isEmpty';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FormSchemaType } from './upload-agent-dialog/upload-agent-form';
export const useHandleImportJsonFile = () => {
const {
visible: fileUploadVisible,
hideModal: hideFileUploadModal,
showModal: showFileUploadModal,
} = useSetModalState();
const { t } = useTranslation();
const { toast } = useToast();
const { loading, setAgent } = useSetAgent();
const onFileUploadOk = useCallback(
async ({ fileList, name }: FormSchemaType) => {
if (fileList.length > 0) {
const file = fileList[0];
if (file.type !== FileMimeType.Json) {
toast({ title: t('flow.jsonUploadTypeErrorMessage') });
return;
}
const graphStr = await file.text();
const errorMessage = t('flow.jsonUploadContentErrorMessage');
try {
const graph = JSON.parse(graphStr);
if (graphStr && !isEmpty(graph) && Array.isArray(graph?.nodes)) {
const dsl = { ...EmptyDsl, graph };
setAgent({ title: name, dsl });
hideFileUploadModal();
} else {
message.error(errorMessage);
}
} catch (error) {
message.error(errorMessage);
}
}
},
[hideFileUploadModal, setAgent, t, toast],
);
return {
fileUploadVisible,
handleImportJson: showFileUploadModal,
hideFileUploadModal,
onFileUploadOk,
loading,
};
};