Compare commits

...

13 Commits

Author SHA1 Message Date
ff2365b146 Replaced twine with uv 2025-10-30 21:08:00 +08:00
ac75bcdf95 Feat: Modify the style of the query variable dropdown list. #10866 (#10903)
### What problem does this PR solve?

Feat: Modify the style of the query variable dropdown list. #10866

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-10-30 20:14:15 +08:00
a62a1a5012 Fix(ci): Add error handling to Docker image removal in tests workflow (#10904)
### What problem does this PR solve?

Add '|| true' to docker rmi command to prevent workflow failure when
image removal fails. This ensures the CI pipeline continues even if the
Docker image cannot be removed for any reason.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-30 20:14:02 +08:00
361c74ab42 Fix several admin UI issues (#10869)
### What problem does this PR solve?

- Fix login card will overlap title in admin login page.
- Disable unnecessary `listRoles()` query in user management page and
create user form
- Disable admin UI API queries and mutations retry mechanism
- Fix page not redirect to login page automatically if API reports
unauthorized (401)
- Fix change password form not reset when change password modal close
- Resolve admin UI content (mostly long texts) may break layout main box
issue

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-30 20:13:39 +08:00
5059d3db18 Feat: The query variables of the subsequent operators can reference the structured variables defined in the agent operator. #10866 (#10902)
### What problem does this PR solve?

Feat: The query variables of the subsequent operators can reference the
structured variables defined in the agent operator. #10866

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-10-30 19:06:44 +08:00
5674d762f7 Feat:check embedding model api (#10854)
### What problem does this PR solve?
change:
Randomly sample `check_num` chunks from knowledge base `kb_id`, re-embed
them using `embd_id`, and compare with stored vectors via cosine
similarity. If `avg_cos_sim > 0.99`, return success (`code=0`);
otherwise return business failure (`code=10`).

url:
`/v1/kb/check_embedding`

Request Body:
```
{
  "kb_id": "<dataset_id>",
  "embd_id": "BAAI/bge-m3@SILICONFLOW",
  "check_num": 5
}

```
Success Response:
```
{
  "code": 0,
  "message": "success",
  "data": {
    "summary": { "avg_cos_sim": 0.999999, "sampled": 5, "valid": 5, "max_cos_sim":0.999999,"min_cos_sim":0.999999,"model":"BAAI/bge-m3@SILICONFLOW" },
    "results": [ ... ]
  }
}
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-10-30 19:06:16 +08:00
fa38aed01b Fix: the input length exceeds the context length (#10895)
### What problem does this PR solve?

Fix: the input length exceeds the context length #10750

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-30 19:00:53 +08:00
ab52ffc9c0 Fix: law parser (#10897)
### What problem does this PR solve?

Fix: law parser  #10888

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-30 19:00:11 +08:00
5f65c7f48e Fix: video parser should follow selected VLM in pipeline (#10900)
### What problem does this PR solve?

Video parser should follow selected VLM, rather than default one.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-30 17:59:50 +08:00
bb9504d1cc Fix:enhance delimiters in markdown parser (#10896)
### What problem does this PR solve?
issue:
[#10890](https://github.com/infiniflow/ragflow/issues/10890)
change:
enhance delimiters in markdown parser
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-30 17:36:51 +08:00
5d79912274 Feat: location rule for admin UI (#10894)
### What problem does this PR solve?

Location rule for admin UI.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-10-30 17:32:32 +08:00
b52f09adfe Mineru api support (#10874)
### What problem does this PR solve?

support local mineru api in docker instance. like no gpu in wsl on
windows, but has mineru api with gpu support.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
2025-10-30 17:31:46 +08:00
27f0d82102 Fix: opensearch retrieval error (#10891)
### What problem does this PR solve?

Fix: opensearch retrieval error #10828

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-30 17:30:54 +08:00
31 changed files with 1032 additions and 376 deletions

View File

@ -78,14 +78,12 @@ jobs:
- name: Build and push ragflow-sdk
if: startsWith(github.ref, 'refs/tags/v')
run: |
cd sdk/python && uv build
twine upload sdk/python/dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }}
cd sdk/python && uv build && uv publish --token ${{ secrets.PYPI_API_TOKEN }}
- name: Build and push ragflow-cli
if: startsWith(github.ref, 'refs/tags/v')
run: |
cd admin/client && uv build
twine upload admin/client/dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }}
cd admin/client && uv build && uv publish --token ${{ secrets.PYPI_API_TOKEN }}
- name: Build and push image
run: |

View File

@ -227,4 +227,4 @@ jobs:
if: always() # always run this step even if previous steps failed
run: |
sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} down -v
sudo docker rmi -f ${RAGFLOW_IMAGE}
sudo docker rmi -f ${RAGFLOW_IMAGE:-NO_IMAGE} || true

View File

@ -15,11 +15,15 @@
#
import json
import logging
import random
from flask import request
from flask_login import login_required, current_user
import numpy as np
from api.db import LLMType
from api.db.services import duplicate_name
from api.db.services.llm_service import LLMBundle
from api.db.services.document_service import DocumentService, queue_raptor_o_graphrag_tasks
from api.db.services.file2document_service import File2DocumentService
from api.db.services.file_service import FileService
@ -38,6 +42,7 @@ from api.constants import DATASET_NAME_LIMIT
from rag.settings import PAGERANK_FLD
from rag.utils.redis_conn import REDIS_CONN
from rag.utils.storage_factory import STORAGE_IMPL
from rag.utils.doc_store_conn import OrderByExpr
@manager.route('/create', methods=['post']) # noqa: F821
@ -788,3 +793,141 @@ def delete_kb_task():
return server_error_response(f"Internal error: cannot delete task {pipeline_task_type}")
return get_json_result(data=True)
@manager.route("/check_embedding", methods=["post"]) # noqa: F821
@login_required
def check_embedding():
def _guess_vec_field(src: dict) -> str | None:
for k in src or {}:
if k.endswith("_vec"):
return k
return None
def _as_float_vec(v):
if v is None:
return []
if isinstance(v, str):
return [float(x) for x in v.split("\t") if x != ""]
if isinstance(v, (list, tuple, np.ndarray)):
return [float(x) for x in v]
return []
def _to_1d(x):
a = np.asarray(x, dtype=np.float32)
return a.reshape(-1)
def _cos_sim(a, b, eps=1e-12):
a = _to_1d(a)
b = _to_1d(b)
na = np.linalg.norm(a)
nb = np.linalg.norm(b)
if na < eps or nb < eps:
return 0.0
return float(np.dot(a, b) / (na * nb))
def sample_random_chunks_with_vectors(
docStoreConn,
tenant_id: str,
kb_id: str,
n: int = 5,
base_fields=("docnm_kwd","doc_id","content_with_weight","page_num_int","position_int","top_int"),
):
index_nm = search.index_name(tenant_id)
res0 = docStoreConn.search(
selectFields=[], highlightFields=[],
condition={"kb_id": kb_id, "available_int": 1},
matchExprs=[], orderBy=OrderByExpr(),
offset=0, limit=1,
indexNames=index_nm, knowledgebaseIds=[kb_id]
)
total = docStoreConn.getTotal(res0)
if total <= 0:
return []
n = min(n, total)
offsets = sorted(random.sample(range(total), n))
out = []
for off in offsets:
res1 = docStoreConn.search(
selectFields=list(base_fields),
highlightFields=[],
condition={"kb_id": kb_id, "available_int": 1},
matchExprs=[], orderBy=OrderByExpr(),
offset=off, limit=1,
indexNames=index_nm, knowledgebaseIds=[kb_id]
)
ids = docStoreConn.getChunkIds(res1)
if not ids:
continue
cid = ids[0]
full_doc = docStoreConn.get(cid, index_nm, [kb_id]) or {}
vec_field = _guess_vec_field(full_doc)
vec = _as_float_vec(full_doc.get(vec_field))
out.append({
"chunk_id": cid,
"kb_id": kb_id,
"doc_id": full_doc.get("doc_id"),
"doc_name": full_doc.get("docnm_kwd"),
"vector_field": vec_field,
"vector_dim": len(vec),
"vector": vec,
"page_num_int": full_doc.get("page_num_int"),
"position_int": full_doc.get("position_int"),
"top_int": full_doc.get("top_int"),
"content_with_weight": full_doc.get("content_with_weight") or "",
})
return out
req = request.json
kb_id = req.get("kb_id", "")
embd_id = req.get("embd_id", "")
n = int(req.get("check_num", 5))
_, kb = KnowledgebaseService.get_by_id(kb_id)
tenant_id = kb.tenant_id
emb_mdl = LLMBundle(tenant_id, LLMType.EMBEDDING, embd_id)
samples = sample_random_chunks_with_vectors(settings.docStoreConn, tenant_id=tenant_id, kb_id=kb_id, n=n)
results, eff_sims = [], []
for ck in samples:
txt = (ck.get("content_with_weight") or "").strip()
if not txt:
results.append({"chunk_id": ck["chunk_id"], "reason": "no_text"})
continue
if not ck.get("vector"):
results.append({"chunk_id": ck["chunk_id"], "reason": "no_stored_vector"})
continue
try:
qv, _ = emb_mdl.encode_queries(txt)
sim = _cos_sim(qv, ck["vector"])
except Exception:
return get_error_data_result(message="embedding failure")
eff_sims.append(sim)
results.append({
"chunk_id": ck["chunk_id"],
"doc_id": ck["doc_id"],
"doc_name": ck["doc_name"],
"vector_field": ck["vector_field"],
"vector_dim": ck["vector_dim"],
"cos_sim": round(sim, 6),
})
summary = {
"kb_id": kb_id,
"model": embd_id,
"sampled": len(samples),
"valid": len(eff_sims),
"avg_cos_sim": round(float(np.mean(eff_sims)) if eff_sims else 0.0, 6),
"min_cos_sim": round(float(np.min(eff_sims)) if eff_sims else 0.0, 6),
"max_cos_sim": round(float(np.max(eff_sims)) if eff_sims else 0.0, 6),
}
if summary["avg_cos_sim"] > 0.99:
return get_json_result(data={"summary": summary, "results": results})
return get_json_result(code=settings.RetCode.NOT_EFFECTIVE, message="failed", data={"summary": summary, "results": results})

View File

@ -215,7 +215,7 @@ def add_llm():
mdl = EmbeddingModel[factory](
key=llm['api_key'],
model_name=mdl_nm,
base_url=llm["api_base"])
base_url=llm["api_base"])
try:
arr, tc = mdl.encode(["Test if the api key is available"])
if len(arr[0]) == 0:

View File

@ -16,6 +16,7 @@
import inspect
import logging
import re
from rag.utils import num_tokens_from_string
from functools import partial
from typing import Generator
from api.db.db_models import LLM
@ -79,9 +80,19 @@ class LLMBundle(LLM4Tenant):
def encode(self, texts: list):
if self.langfuse:
generation = self.langfuse.start_generation(trace_context=self.trace_context, name="encode", model=self.llm_name, input={"texts": texts})
generation = self.langfuse.start_generation(trace_context=self.trace_context, name="encode", model=self.llm_name, input={"texts": texts})
safe_texts = []
for text in texts:
token_size = num_tokens_from_string(text)
if token_size > self.max_length:
target_len = int(self.max_length * 0.95)
safe_texts.append(text[:target_len])
else:
safe_texts.append(text)
embeddings, used_tokens = self.mdl.encode(safe_texts)
embeddings, used_tokens = self.mdl.encode(texts)
llm_name = getattr(self, "llm_name", None)
if not TenantLLMService.increase_usage(self.tenant_id, self.llm_type, used_tokens, llm_name):
logging.error("LLMBundle.encode can't update token usage for {}/EMBEDDING used_tokens: {}".format(self.tenant_id, used_tokens))

View File

@ -117,11 +117,24 @@ class MarkdownElementExtractor:
self.markdown_content = markdown_content
self.lines = markdown_content.split("\n")
def extract_elements(self):
def get_delimiters(self,delimiters):
toks = re.findall(r"`([^`]+)`", delimiters)
toks = sorted(set(toks), key=lambda x: -len(x))
return "|".join(re.escape(t) for t in toks if t)
def extract_elements(self,delimiter=None):
"""Extract individual elements (headers, code blocks, lists, etc.)"""
sections = []
i = 0
dels=""
if delimiter:
dels = self.get_delimiters(delimiter)
if len(dels) > 0:
text = "\n".join(self.lines)
parts = re.split(dels, text)
sections = [p.strip() for p in parts if p and p.strip()]
return sections
while i < len(self.lines):
line = self.lines[i]

View File

@ -27,6 +27,9 @@ from os import PathLike
from pathlib import Path
from queue import Empty, Queue
from typing import Any, Callable, Optional
import requests
import os
import zipfile
import numpy as np
import pdfplumber
@ -51,10 +54,52 @@ class MinerUContentType(StrEnum):
class MinerUParser(RAGFlowPdfParser):
def __init__(self, mineru_path: str = "mineru"):
def __init__(self, mineru_path: str = "mineru", mineru_api: str = "http://host.docker.internal:9987"):
self.mineru_path = Path(mineru_path)
self.mineru_api = mineru_api.rstrip('/')
self.using_api = False
self.logger = logging.getLogger(self.__class__.__name__)
def _extract_zip_no_root(self, zip_path, extract_to, root_dir):
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
if not root_dir:
files = zip_ref.namelist()
if files and files[0].endswith('/'):
root_dir = files[0]
else:
root_dir = None
if not root_dir or not root_dir.endswith('/'):
self.logger.info(f"[MinerU] No root directory found, extracting all...fff{root_dir}")
zip_ref.extractall(extract_to)
return
root_len = len(root_dir)
for member in zip_ref.infolist():
filename = member.filename
if filename == root_dir:
self.logger.info("[MinerU] Ignore root folder...")
continue
path = filename
if path.startswith(root_dir):
path = path[root_len:]
full_path = os.path.join(extract_to, path)
if member.is_dir():
os.makedirs(full_path, exist_ok=True)
else:
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, 'wb') as f:
f.write(zip_ref.read(filename))
def _is_http_endpoint_valid(self, url, timeout=5):
try:
response = requests.head(url, timeout=timeout, allow_redirects=True)
return response.status_code in [200, 301, 302, 307, 308]
except Exception:
return False
def check_installation(self) -> bool:
subprocess_kwargs = {
"capture_output": True,
@ -81,9 +126,97 @@ class MinerUParser(RAGFlowPdfParser):
logging.warning("[MinerU] MinerU not found. Please install it via: pip install -U 'mineru[core]'")
except Exception as e:
logging.error(f"[MinerU] Unexpected error during installation check: {e}")
try:
if self.mineru_api:
# check openapi.json
openapi_exists = self._is_http_endpoint_valid(self.mineru_api + "/openapi.json")
logging.info(f"[MinerU] Detected {self.mineru_api}/openapi.json: {openapi_exists}")
self.using_api = openapi_exists
return openapi_exists
else:
logging.info("[MinerU] api not exists.")
except Exception as e:
logging.error(f"[MinerU] Unexpected error during api check: {e}")
return False
def _run_mineru(self, input_path: Path, output_dir: Path, method: str = "auto", backend: str = "pipeline", lang: Optional[str] = None):
def _run_mineru(self, input_path: Path, output_dir: Path, method: str = "auto", backend: str = "pipeline", lang: Optional[str] = None, callback: Optional[Callable] = None):
if self.using_api:
self._run_mineru_api(input_path, output_dir, method, backend, lang, callback)
else:
self._run_mineru_executable(input_path, output_dir, method, backend, lang, callback)
def _run_mineru_api(self, input_path: Path, output_dir: Path, method: str = "auto", backend: str = "pipeline", lang: Optional[str] = None, callback: Optional[Callable] = None):
OUTPUT_ZIP_PATH = os.path.join(str(output_dir), "output.zip")
pdf_file_path = str(input_path)
if not os.path.exists(pdf_file_path):
raise RuntimeError(f"[MinerU] PDF file not exists: {pdf_file_path}")
pdf_file_name = Path(pdf_file_path).stem.strip()
output_path = os.path.join(str(output_dir), pdf_file_name, method)
os.makedirs(output_path, exist_ok=True)
files = {
"files": (pdf_file_name + ".pdf", open(pdf_file_path, "rb"), "application/pdf")
}
data = {
"output_dir": "./output",
"lang_list": lang,
"backend": backend,
"parse_method": method,
"formula_enable": True,
"table_enable": True,
"server_url": None,
"return_md": True,
"return_middle_json": True,
"return_model_output": True,
"return_content_list": True,
"return_images": True,
"response_format_zip": True,
"start_page_id": 0,
"end_page_id": 99999
}
headers = {
"Accept": "application/json"
}
try:
self.logger.info(f"[MinerU] invoke api: {self.mineru_api}/file_parse")
if callback:
callback(0.20, f"[MinerU] invoke api: {self.mineru_api}/file_parse")
response = requests.post(
url=f"{self.mineru_api}/file_parse",
files=files,
data=data,
headers=headers,
timeout=1800
)
response.raise_for_status()
if response.headers.get("Content-Type") == "application/zip":
self.logger.info(f"[MinerU] zip file returned, saving to {OUTPUT_ZIP_PATH}...")
if callback:
callback(0.30, f"[MinerU] zip file returned, saving to {OUTPUT_ZIP_PATH}...")
with open(OUTPUT_ZIP_PATH, "wb") as f:
f.write(response.content)
self.logger.info(f"[MinerU] Unzip to {output_path}...")
self._extract_zip_no_root(OUTPUT_ZIP_PATH, output_path, pdf_file_name + "/")
if callback:
callback(0.40, f"[MinerU] Unzip to {output_path}...")
else:
self.logger.warning("[MinerU] not zip returned from api%s " % response.headers.get("Content-Type"))
except Exception as e:
raise RuntimeError(f"[MinerU] api failed with exception {e}")
self.logger.info("[MinerU] Api completed successfully.")
def _run_mineru_executable(self, input_path: Path, output_dir: Path, method: str = "auto", backend: str = "pipeline", lang: Optional[str] = None, callback: Optional[Callable] = None):
cmd = [str(self.mineru_path), "-p", str(input_path), "-o", str(output_dir), "-m", method]
if backend:
cmd.extend(["-b", backend])
@ -261,7 +394,7 @@ class MinerUParser(RAGFlowPdfParser):
case MinerUContentType.TEXT:
section = output["text"]
case MinerUContentType.TABLE:
section = output["table_body"] + "\n".join(output["table_caption"]) + "\n".join(output["table_footnote"])
section = output["table_body"] if "table_body" in output else "" + "\n".join(output["table_caption"]) + "\n".join(output["table_footnote"])
case MinerUContentType.IMAGE:
section = "".join(output["image_caption"]) + "\n" + "".join(output["image_footnote"])
case MinerUContentType.EQUATION:
@ -297,9 +430,14 @@ class MinerUParser(RAGFlowPdfParser):
temp_pdf = None
created_tmp_dir = False
# remove spaces, or mineru crash, and _read_output fail too
file_path = Path(filepath)
pdf_file_name = file_path.stem.replace(" ", "") + ".pdf"
pdf_file_path_valid = os.path.join(file_path.parent, pdf_file_name)
if binary:
temp_dir = Path(tempfile.mkdtemp(prefix="mineru_bin_pdf_"))
temp_pdf = temp_dir / Path(filepath).name
temp_pdf = temp_dir / pdf_file_name
with open(temp_pdf, "wb") as f:
f.write(binary)
pdf = temp_pdf
@ -307,7 +445,10 @@ class MinerUParser(RAGFlowPdfParser):
if callback:
callback(0.15, f"[MinerU] Received binary PDF -> {temp_pdf}")
else:
pdf = Path(filepath)
if pdf_file_path_valid != filepath:
self.logger.info(f"[MinerU] Remove all space in file name: {pdf_file_path_valid}")
shutil.move(filepath, pdf_file_path_valid)
pdf = Path(pdf_file_path_valid)
if not pdf.exists():
if callback:
callback(-1, f"[MinerU] PDF not found: {pdf}")
@ -327,7 +468,7 @@ class MinerUParser(RAGFlowPdfParser):
self.__images__(pdf, zoomin=1)
try:
self._run_mineru(pdf, out_dir, method=method, backend=backend, lang=lang)
self._run_mineru(pdf, out_dir, method=method, backend=backend, lang=lang, callback=callback)
outputs = self._read_output(out_dir, pdf.stem, method=method, backend=backend)
self.logger.info(f"[MinerU] Parsed {len(outputs)} blocks from PDF.")
if callback:

View File

@ -22,6 +22,11 @@ server {
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
location ~ ^/api/v1/admin {
proxy_pass http://ragflow:9381;
include proxy.conf;
}
location ~ ^/(v1|api) {
proxy_pass http://ragflow:9380;
include proxy.conf;

View File

@ -87,7 +87,7 @@ class Docx(DocxParser):
root = Node(level=0, depth=h2_level, texts=[])
root.build_tree(lines)
return [("\n").join(element) for element in root.get_tree() if element]
return [element for element in root.get_tree() if element]
def __str__(self) -> str:

View File

@ -397,7 +397,7 @@ class Markdown(MarkdownParser):
return images if images else None
def __call__(self, filename, binary=None, separate_tables=True):
def __call__(self, filename, binary=None, separate_tables=True,delimiter=None):
if binary:
encoding = find_codec(binary)
txt = binary.decode(encoding, errors="ignore")
@ -408,7 +408,7 @@ class Markdown(MarkdownParser):
remainder, tables = self.extract_tables_and_remainder(f'{txt}\n', separate_tables=separate_tables)
extractor = MarkdownElementExtractor(txt)
element_sections = extractor.extract_elements()
element_sections = extractor.extract_elements(delimiter)
sections = [(element, "") for element in element_sections]
tbls = []
@ -520,7 +520,8 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
elif layout_recognizer == "MinerU":
mineru_executable = os.environ.get("MINERU_EXECUTABLE", "mineru")
pdf_parser = MinerUParser(mineru_path=mineru_executable)
mineru_api = os.environ.get("MINERU_APISERVER", "http://host.docker.internal:9987")
pdf_parser = MinerUParser(mineru_path=mineru_executable, mineru_api=mineru_api)
if not pdf_parser.check_installation():
callback(-1, "MinerU not found.")
return res
@ -599,7 +600,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
elif re.search(r"\.(md|markdown)$", filename, re.IGNORECASE):
callback(0.1, "Start to parse.")
markdown_parser = Markdown(int(parser_config.get("chunk_token_num", 128)))
sections, tables = markdown_parser(filename, binary, separate_tables=False)
sections, tables = markdown_parser(filename, binary, separate_tables=False,delimiter=parser_config.get("delimiter", "\n!?;。;!?"))
try:
vision_model = LLMBundle(kwargs["tenant_id"], LLMType.IMAGE2TEXT)

View File

@ -222,7 +222,8 @@ class Parser(ProcessBase):
bboxes = [{"text": t} for t, _ in lines]
elif conf.get("parse_method").lower() == "mineru":
mineru_executable = os.environ.get("MINERU_EXECUTABLE", "mineru")
pdf_parser = MinerUParser(mineru_path=mineru_executable)
mineru_api = os.environ.get("MINERU_APISERVER", "http://host.docker.internal:9987")
pdf_parser = MinerUParser(mineru_path=mineru_executable, mineru_api=mineru_api)
if not pdf_parser.check_installation():
raise RuntimeError("MinerU not found. Please install it via: pip install -U 'mineru[core]'.")
@ -430,7 +431,7 @@ class Parser(ProcessBase):
conf = self._param.setups["video"]
self.set_output("output_format", conf["output_format"])
cv_mdl = LLMBundle(self._canvas.get_tenant_id(), LLMType.IMAGE2TEXT)
cv_mdl = LLMBundle(self._canvas.get_tenant_id(), LLMType.IMAGE2TEXT, llm_name=conf["llm_id"])
txt = cv_mdl.chat(system="", history=[], gen_conf={}, video_bytes=blob, filename=name)
self.set_output("text", txt)

View File

@ -400,7 +400,7 @@ class Dealer:
page_index = (page % max_pages) - 1
begin = max(page_index * page_size, 0)
sim = sim[begin : begin + page_size]
sim_np = np.array(sim)
sim_np = np.array(sim, dtype=np.float64)
idx = np.argsort(sim_np * -1)
dim = len(sres.query_vector)
vector_column = f"q_{dim}_vec"
@ -408,7 +408,7 @@ class Dealer:
filtered_count = (sim_np >= similarity_threshold).sum()
ranks["total"] = int(filtered_count) # Convert from np.int64 to Python int otherwise JSON serializable error
for i in idx:
if sim[i] < similarity_threshold:
if np.float64(sim[i]) < similarity_threshold:
break
id = sres.ids[i]

View File

@ -1,5 +1,5 @@
import i18n from '@/locales/config';
import { BeginId } from '@/pages/flow/constant';
import { BeginId } from '@/pages/agent/constant';
import { DecoratorNode, LexicalNode, NodeKey } from 'lexical';
import { ReactNode } from 'react';
const prefix = BeginId + '@';

View File

@ -114,7 +114,7 @@ export default function VariablePickerMenuPlugin({
minLength: 0,
});
const [queryString, setQueryString] = React.useState<string| null>('');
const [queryString, setQueryString] = React.useState<string | null>('');
const options = useBuildComponentIdSelectOptions(node?.id, node?.parentId);

View File

@ -124,7 +124,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-bg-card data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className,
)}
{...props}

View File

@ -1623,7 +1623,7 @@ This delimiter is used to split the input text into several text pieces echo of
extractorDescription:
'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.',
outputFormat: 'Output format',
fileFormats: 'File format',
fileFormats: 'File type',
fileFormatOptions: {
pdf: 'PDF',
spreadsheet: 'Spreadsheet',
@ -1644,7 +1644,7 @@ This delimiter is used to split the input text into several text pieces echo of
searchMethodTip: `Defines how the content can be searched — by full-text, embedding, or both.
The Indexer will store the content in the corresponding data structures for the selected methods.`,
// file: 'File',
parserMethod: 'Parsing method',
parserMethod: 'PDF parser',
// systemPrompt: 'System Prompt',
systemPromptPlaceholder:
'Enter system prompt for image analysis, if empty the system default value will be used',

View File

@ -1529,7 +1529,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
extractorDescription:
'使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。',
outputFormat: '输出格式',
fileFormats: '文件格式',
fileFormats: '文件类型',
fields: '字段',
addParser: '增加解析器',
hierarchy: '层次结构',

View File

@ -24,7 +24,9 @@ import {
SelectValue,
} from '@/components/ui/select';
import { listRoles } from '@/services/admin-service';
import EnterpriseFeature from '../components/enterprise-feature';
import { IS_ENTERPRISE } from '../utils';
interface CreateUserFormData {
email: string;
@ -49,6 +51,8 @@ export const CreateUserForm = ({
const { data: roleList } = useQuery({
queryKey: ['admin/listRoles'],
queryFn: async () => (await listRoles()).data.data.roles,
enabled: IS_ENTERPRISE,
retry: false,
});
return (

View File

@ -1,3 +1,16 @@
import { type AxiosResponseHeaders } from 'axios';
import { useEffect, useId, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'umi';
import { LucideEye, LucideEyeOff } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import Spotlight from '@/components/spotlight';
import { ButtonLoading } from '@/components/ui/button';
import { Card, CardContent, CardFooter } from '@/components/ui/card';
@ -11,22 +24,17 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Authorization } from '@/constants/authorization';
import { useAuth } from '@/hooks/auth-hooks';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import adminService from '@/services/admin-service';
import { rsaPsw } from '@/utils';
import authorizationUtil from '@/utils/authorization-util';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { AxiosResponseHeaders } from 'axios';
import { LucideEye, LucideEyeOff } from 'lucide-react';
import { useEffect, useId, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'umi';
import { z } from 'zod';
import { login } from '@/services/admin-service';
import { BgSvg } from '../login-next/bg';
import ThemeSwitch from './components/theme-switch';
@ -37,13 +45,19 @@ function AdminLogin() {
const [showPassword, setShowPassword] = useState(false);
const { isPending: signLoading, mutateAsync: login } = useMutation({
const loginMutation = useMutation({
mutationKey: ['adminLogin'],
mutationFn: async (params: { email: string; password: string }) => {
const request = await adminService.login(params);
const rsaPassWord = rsaPsw(params.password) as string;
return await login({
email: params.email,
password: rsaPassWord,
});
},
onSuccess: (request) => {
const { data: req, headers } = request;
if (req.code === 0) {
if (req?.code === 0) {
const authorization = (headers as AxiosResponseHeaders)?.get(
Authorization,
);
@ -60,13 +74,17 @@ function AdminLogin() {
Token: token,
userInfo: JSON.stringify(userInfo),
});
}
return req;
navigate('/admin/services');
}
},
onError: (error) => {
console.log('Failed:', error);
},
retry: false,
});
const loading = signLoading;
const loading = loginMutation.isPending;
useEffect(() => {
if (isLogin) {
@ -93,172 +111,159 @@ function AdminLogin() {
resolver: zodResolver(FormSchema),
});
const onCheck: SubmitHandler<z.infer<typeof FormSchema>> = async (params) => {
try {
const rsaPassWord = rsaPsw(params.password) as string;
const { code } = await login({
email: `${params.email}`.trim(),
password: rsaPassWord,
});
if (code === 0) {
navigate('/admin/services');
}
} catch (errorInfo) {
console.log('Failed:', errorInfo);
}
};
return (
<div className="relative w-screen h-screen">
<Spotlight opcity={0.4} coverage={60} color="rgb(128, 255, 248)" />
<Spotlight
opcity={0.3}
coverage={12}
X="10%"
Y="-10%"
color="rgb(128, 255, 248)"
/>
<Spotlight
opcity={0.3}
coverage={12}
X="90%"
Y="-10%"
color="rgb(128, 255, 248)"
/>
<ScrollArea className="w-screen h-screen">
<div className="relative">
<Spotlight opcity={0.4} coverage={60} color="rgb(128, 255, 248)" />
<Spotlight
opcity={0.3}
coverage={12}
X="10%"
Y="-10%"
color="rgb(128, 255, 248)"
/>
<Spotlight
opcity={0.3}
coverage={12}
X="90%"
Y="-10%"
color="rgb(128, 255, 248)"
/>
<BgSvg />
<BgSvg />
<div className="absolute top-3 left-0 w-full">
<div className="absolute mt-12 ml-12 flex items-center">
<img className="size-8 mr-5" src="/logo.svg" alt="logo" />
<span className="text-xl font-bold">RAGFlow</span>
<div className="absolute top-3 left-0 w-full">
<div className="absolute mt-12 ml-12 flex items-center">
<img className="size-8 mr-5" src="/logo.svg" alt="logo" />
<span className="text-xl font-bold">RAGFlow</span>
</div>
<h1 className="mt-[6.5rem] text-4xl font-medium text-center mb-12">
{t('loginTitle', { keyPrefix: 'admin' })}
</h1>
</div>
<h1 className="mt-[6.5rem] text-4xl font-medium text-center mb-12">
{t('loginTitle', { keyPrefix: 'admin' })}
</h1>
</div>
<div className="flex items-center justify-center w-screen h-screen">
<div className="w-full max-w-[540px]">
<Card className="w-full bg-bg-component backdrop-blur-sm rounded-2xl border border-border-button">
<CardContent className="px-10 pt-14 pb-10">
<Form {...form}>
<form
id={formId}
className="space-y-8 text-text-primary"
onSubmit={form.handleSubmit(onCheck)}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl>
<Input
className="h-10 px-2.5"
placeholder={t('emailPlaceholder')}
autoComplete="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
<div className="flex items-center justify-center w-screen min-h-[1050px]">
<div className="w-full max-w-[540px]">
<Card className="w-full bg-bg-component backdrop-blur-sm rounded-2xl border border-border-button">
<CardContent className="px-10 pt-14 pb-10">
<Form {...form}>
<form
id={formId}
className="space-y-8 text-text-primary"
onSubmit={form.handleSubmit((data) =>
loginMutation.mutate(data),
)}
/>
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl>
<div className="relative">
<FormControl>
<Input
className="h-10 px-2.5"
type={showPassword ? 'text' : 'password'}
placeholder={t('passwordPlaceholder')}
autoComplete="password"
placeholder={t('emailPlaceholder')}
autoComplete="email"
{...field}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<LucideEyeOff className="h-4 w-4 text-gray-500" />
) : (
<LucideEye className="h-4 w-4 text-gray-500" />
)}
</button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="remember"
render={({ field }) => (
<FormItem className="!mt-5">
<FormLabel
className={cn(
'flex items-center hover:text-text-primary',
field.value
? 'text-text-primary'
: 'text-text-disabled',
)}
>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<span className="ml-2">{t('rememberMe')}</span>
</FormLabel>
</FormItem>
)}
/>
</form>
</Form>
</CardContent>
<FormMessage />
</FormItem>
)}
/>
<CardFooter className="px-10 pt-8 pb-14">
<ButtonLoading
form={formId}
size="lg"
className="
w-full h-10
bg-metallic-gradient border-b-[#00BEB4] border-b-2
hover:bg-metallic-gradient hover:border-b-[#02bcdd]
"
type="submit"
loading={loading}
>
{t('login')}
</ButtonLoading>
</CardFooter>
</Card>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<div className="mt-8 flex justify-center">
<ThemeSwitch />
<FormControl>
<div className="relative">
<Input
className="h-10 px-2.5"
type={showPassword ? 'text' : 'password'}
placeholder={t('passwordPlaceholder')}
autoComplete="password"
{...field}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<LucideEyeOff className="h-4 w-4 text-gray-500" />
) : (
<LucideEye className="h-4 w-4 text-gray-500" />
)}
</button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="remember"
render={({ field }) => (
<FormItem className="!mt-5">
<FormLabel
className={cn(
'flex items-center hover:text-text-primary',
field.value
? 'text-text-primary'
: 'text-text-disabled',
)}
>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<span className="ml-2">{t('rememberMe')}</span>
</FormLabel>
</FormItem>
)}
/>
</form>
</Form>
</CardContent>
<CardFooter className="px-10 pt-8 pb-14">
<ButtonLoading
form={formId}
size="lg"
className="
w-full h-10
bg-metallic-gradient border-b-[#00BEB4] border-b-2
hover:bg-metallic-gradient hover:border-b-[#02bcdd]
"
type="submit"
loading={loading}
>
{t('login')}
</ButtonLoading>
</CardFooter>
</Card>
<div className="mt-8 flex justify-center">
<ThemeSwitch />
</div>
</div>
</div>
</div>
</div>
</ScrollArea>
);
}

View File

@ -14,13 +14,12 @@ import {
} from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { NavLink, Outlet, useLocation, useNavigate } from 'umi';
import { NavLink, Outlet, useNavigate } from 'umi';
import ThemeSwitch from './components/theme-switch';
import { IS_ENTERPRISE } from './utils';
const AdminLayout = () => {
const { t } = useTranslation();
const { pathname } = useLocation();
const navigate = useNavigate();
const navItems = useMemo(
@ -58,11 +57,7 @@ const AdminLayout = () => {
[t],
);
const {
data,
isPending,
mutateAsync: logout,
} = useMutation({
const logoutMutation = useMutation({
mutationKey: ['adminLogout'],
mutationFn: async () => {
await adminService.logout();
@ -71,11 +66,12 @@ const AdminLayout = () => {
authorizationUtil.removeAll();
navigate(Routes.Admin);
},
retry: false,
});
return (
<main className="w-screen h-screen flex flex-row px-6 pt-12 pb-6 dark:*:focus-visible:ring-white">
<aside className="w-[28rem] mr-6 flex flex-col gap-6">
<aside className="w-72 mr-6 flex flex-col gap-6">
<div className="flex items-center mb-6">
<img className="size-8 mr-5" src="/logo.svg" alt="logo" />
<span className="text-xl font-bold">{t('admin.title')}</span>
@ -87,17 +83,18 @@ const AdminLayout = () => {
<li key={it.path}>
<NavLink
to={it.path}
className={cn(
'px-4 py-3 rounded-lg',
'text-base w-full flex items-center justify-start text-text-secondary',
'hover:bg-bg-card focus:bg-bg-card focus-visible:bg-bg-card',
'hover:text-text-primary focus:text-text-primary focus-visible:text-text-primary',
'active:text-text-primary',
{
'bg-bg-card text-text-primary':
it.path && pathname.startsWith(it.path),
},
)}
className={({ isActive }) =>
cn(
'px-4 py-3 rounded-lg',
'text-base w-full flex items-center justify-start text-text-secondary',
'hover:bg-bg-card focus:bg-bg-card focus-visible:bg-bg-card',
'hover:text-text-primary focus:text-text-primary focus-visible:text-text-primary',
'active:text-text-primary',
{
'bg-bg-card text-text-primary': isActive,
},
)
}
>
{it.icon}
<span className="ml-3">{it.name}</span>
@ -116,14 +113,14 @@ const AdminLayout = () => {
size="lg"
variant="transparent"
className="block w-full dark:border-border-button"
onClick={() => logout()}
onClick={() => logoutMutation.mutate()}
>
{t('header.logout')}
</Button>
</div>
</aside>
<section className="w-full h-full">
<section className="flex-1 h-full">
<Outlet />
</section>
</main>

View File

@ -307,6 +307,7 @@ function AdminUserDetail() {
};
},
enabled: !!id,
retry: false,
});
return (

View File

@ -126,11 +126,14 @@ function AdminUserManagement() {
const { data: roleList } = useQuery({
queryKey: ['admin/listRoles'],
queryFn: async () => (await listRoles()).data.data.roles,
enabled: IS_ENTERPRISE,
retry: false,
});
const { data: usersList, isPending } = useQuery({
queryKey: ['admin/listUsers'],
queryFn: async () => (await listUsers()).data.data,
retry: false,
});
// Delete user mutation
@ -142,17 +145,19 @@ function AdminUserManagement() {
setDeleteModalOpen(false);
setUserToMakeAction(null);
},
retry: false,
});
// Change password mutation
const changePasswordMutation = useMutation({
mutationFn: ({ email, password }: { email: string; password: string }) =>
updateUserPassword(email, password),
updateUserPassword(email, rsaPsw(password) as string),
onSuccess: () => {
// message.success(t('admin.passwordChangedSuccessfully'));
setPasswordModalOpen(false);
setUserToMakeAction(null);
},
retry: false,
});
// Update user role mutation
@ -162,6 +167,7 @@ function AdminUserManagement() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
},
retry: false,
});
// Create user mutation
@ -175,7 +181,7 @@ function AdminUserManagement() {
password: string;
role?: string;
}) => {
await createUser(email, password);
await createUser(email, rsaPsw(password) as string);
if (IS_ENTERPRISE && role) {
await updateUserRoleMutation.mutateAsync({ email, role });
@ -187,6 +193,7 @@ function AdminUserManagement() {
setCreateUserModalOpen(false);
createUserForm.form.reset();
},
retry: false,
});
// Update user status mutation
@ -196,6 +203,7 @@ function AdminUserManagement() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
},
retry: false,
});
const columnDefs = useMemo(
@ -573,7 +581,8 @@ function AdminUserManagement() {
className="px-4 h-10"
variant="destructive"
onClick={() =>
deleteUserMutation.mutate(userToMakeAction?.email || '')
userToMakeAction &&
deleteUserMutation.mutate(userToMakeAction?.email)
}
disabled={deleteUserMutation.isPending}
loading={deleteUserMutation.isPending}
@ -586,7 +595,14 @@ function AdminUserManagement() {
{/* Change Password Modal */}
<Dialog open={passwordModalOpen} onOpenChange={setPasswordModalOpen}>
<DialogContent className="p-0 border-border-button">
<DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => {
if (!passwordModalOpen) {
changePasswordForm.form.reset();
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.changePassword')}</DialogTitle>
</DialogHeader>
@ -599,7 +615,7 @@ function AdminUserManagement() {
if (userToMakeAction) {
changePasswordMutation.mutate({
email: userToMakeAction.email,
password: rsaPsw(newPassword) as string,
password: newPassword,
});
}
}}
@ -649,12 +665,7 @@ function AdminUserManagement() {
<section className="px-12 py-4">
<createUserForm.FormComponent
id={createUserForm.id}
onSubmit={({ email, password }) => {
createUserMutation.mutate({
email: email,
password: rsaPsw(password) as string,
});
}}
onSubmit={createUserMutation.mutate}
/>
</section>

View File

@ -617,7 +617,10 @@ export const initialAgentValues = {
type: 'string',
value: '',
},
[AgentStructuredOutputField]: {},
[AgentStructuredOutputField]: {
type: 'Object Array String Number Boolean',
value: '',
},
},
};

View File

@ -31,20 +31,15 @@ import * as ReactDOM from 'react-dom';
import { $createVariableNode } from './variable-node';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { Operator } from '@/constants/agent';
import { cn } from '@/lib/utils';
import { AgentStructuredOutputField } from '@/pages/agent/constant';
useFilterStructuredOutputByValue,
useFindAgentStructuredOutputLabel,
useShowSecondaryMenu,
} from '@/pages/agent/hooks/use-build-structured-output';
import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query';
import useGraphStore from '@/pages/agent/store';
import { get, isPlainObject } from 'lodash';
import { PromptIdentity } from '../../agent-form/use-build-prompt-options';
import { StructuredOutputSecondaryMenu } from '../structured-output-secondary-menu';
import { ProgrammaticTag } from './constant';
import './index.css';
import { filterAgentStructuredOutput } from './utils';
class VariableInnerOption extends MenuOption {
label: string;
value: string;
@ -82,10 +77,6 @@ class VariableOption extends MenuOption {
}
}
function getNodeId(value: string) {
return value.split('@').at(0);
}
function VariablePickerMenuItem({
index,
option,
@ -97,58 +88,9 @@ function VariablePickerMenuItem({
option: VariableOption | VariableInnerOption,
) => void;
}) {
const { getOperatorTypeFromId, getNode, clickedNodeId } = useGraphStore(
(state) => state,
);
const filterStructuredOutput = useFilterStructuredOutputByValue();
const showSecondaryMenu = useCallback(
(value: string, outputLabel: string) => {
const nodeId = getNodeId(value);
return (
getOperatorTypeFromId(nodeId) === Operator.Agent &&
outputLabel === AgentStructuredOutputField
);
},
[getOperatorTypeFromId],
);
const renderAgentStructuredOutput = useCallback(
(values: any, option: VariableInnerOption) => {
if (isPlainObject(values) && 'properties' in values) {
return (
<ul className="border-l">
{Object.entries(values.properties).map(([key, value]) => {
const nextOption = new VariableInnerOption(
option.label + `.${key}`,
option.value + `.${key}`,
option.parentLabel,
option.icon,
);
const dataType = get(value, 'type');
return (
<li key={key} className="pl-1">
<div
onClick={() => selectOptionAndCleanUp(nextOption)}
className="hover:bg-bg-card p-1 text-text-primary rounded-sm flex justify-between"
>
{key}
<span className="text-text-secondary">{dataType}</span>
</div>
{dataType === 'object' &&
renderAgentStructuredOutput(value, nextOption)}
</li>
);
})}
</ul>
);
}
return <div></div>;
},
[selectOptionAndCleanUp],
);
const showSecondaryMenu = useShowSecondaryMenu();
return (
<li
@ -165,39 +107,20 @@ function VariablePickerMenuItem({
const shouldShowSecondary = showSecondaryMenu(x.value, x.label);
if (shouldShowSecondary) {
const node = getNode(getNodeId(x.value));
const structuredOutput = get(
node,
`data.form.outputs.${AgentStructuredOutputField}`,
);
const filteredStructuredOutput = filterAgentStructuredOutput(
structuredOutput,
getOperatorTypeFromId(clickedNodeId),
);
const filteredStructuredOutput = filterStructuredOutput(x.value);
return (
<HoverCard key={x.value} openDelay={100} closeDelay={100}>
<HoverCardTrigger asChild>
<li className="hover:bg-bg-card p-1 text-text-primary rounded-sm">
{x.label}
</li>
</HoverCardTrigger>
<HoverCardContent
side="left"
align="start"
className={cn(
'min-w-[140px] border border-border rounded-md shadow-lg p-0',
)}
>
<section className="p-2">
<div className="p-1">
{x.parentLabel} structured output:
</div>
{renderAgentStructuredOutput(filteredStructuredOutput, x)}
</section>
</HoverCardContent>
</HoverCard>
<StructuredOutputSecondaryMenu
key={x.value}
data={x}
click={(y) =>
selectOptionAndCleanUp({
...x,
...y,
} as VariableInnerOption)
}
filteredStructuredOutput={filteredStructuredOutput}
></StructuredOutputSecondaryMenu>
);
}
@ -239,9 +162,8 @@ export default function VariablePickerMenuPlugin({
baseOptions,
}: VariablePickerMenuPluginProps): JSX.Element {
const [editor] = useLexicalComposerContext();
const getOperatorTypeFromId = useGraphStore(
(state) => state.getOperatorTypeFromId,
);
const findAgentStructuredOutputLabel = useFindAgentStructuredOutputLabel();
// const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
// minLength: 0,
@ -313,27 +235,17 @@ export default function VariablePickerMenuPlugin({
}, []);
// agent structured output
const fields = value.split('@');
if (
getOperatorTypeFromId(fields.at(0)) === Operator.Agent &&
fields.at(1)?.startsWith(AgentStructuredOutputField)
) {
// is agent structured output
const agentOption = children.find((x) => value.includes(x.value));
const jsonSchemaFields = fields
.at(1)
?.slice(AgentStructuredOutputField.length);
return {
...agentOption,
label: (agentOption?.label ?? '') + jsonSchemaFields,
value: value,
};
const agentStructuredOutput = findAgentStructuredOutputLabel(
value,
children,
);
if (agentStructuredOutput) {
return agentStructuredOutput;
}
return children.find((x) => x.value === value);
},
[getOperatorTypeFromId, options],
[findAgentStructuredOutputLabel, options],
);
const onSelectOption = useCallback(

View File

@ -1,4 +1,3 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
FormControl,
FormField,
@ -12,6 +11,7 @@ import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { VariableType } from '../../constant';
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
import { GroupedSelectWithSecondaryMenu } from './select-with-secondary-menu';
type QueryVariableProps = {
name?: string;
@ -52,11 +52,11 @@ export function QueryVariable({
</FormLabel>
)}
<FormControl>
<SelectWithSearch
<GroupedSelectWithSecondaryMenu
options={finalOptions}
{...field}
allowClear
></SelectWithSearch>
// allowClear
></GroupedSelectWithSecondaryMenu>
</FormControl>
<FormMessage />
</FormItem>

View File

@ -0,0 +1,225 @@
import { Button } from '@/components/ui/button';
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
import { get } from 'lodash';
import { ChevronDownIcon, XIcon } from 'lucide-react';
import * as React from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
useFilterStructuredOutputByValue,
useFindAgentStructuredOutputLabel,
useShowSecondaryMenu,
} from '../../hooks/use-build-structured-output';
import { StructuredOutputSecondaryMenu } from './structured-output-secondary-menu';
type Item = {
label: string;
value: string;
};
type Option = {
label: string;
value: string;
parentLabel?: string;
children?: Item[];
};
type Group = {
label: string | React.ReactNode;
options: Option[];
};
interface GroupedSelectWithSecondaryMenuProps {
options: Group[];
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
}
export function GroupedSelectWithSecondaryMenu({
options,
value,
onChange,
placeholder,
}: GroupedSelectWithSecondaryMenuProps) {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
const showSecondaryMenu = useShowSecondaryMenu();
const filterStructuredOutput = useFilterStructuredOutputByValue();
const findAgentStructuredOutputLabel = useFindAgentStructuredOutputLabel();
// Find the label of the selected item
const flattenedOptions = options.flatMap((g) => g.options);
let selectedItem = flattenedOptions
.flatMap((o) => [o, ...(o.children || [])])
.find((o) => o.value === value);
if (!selectedItem && value) {
selectedItem = findAgentStructuredOutputLabel(value, flattenedOptions);
}
// Handle clear click
const handleClear = (e: React.MouseEvent) => {
e.stopPropagation();
onChange?.('');
setOpen(false);
};
const handleSecondaryMenuClick = useCallback(
(record: Item) => {
onChange?.(record.value);
setOpen(false);
},
[onChange],
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
'!bg-bg-input hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto',
!value && 'text-muted-foreground',
)}
>
{value ? (
<div className="truncate flex items-center gap-1">
<span>{get(selectedItem, 'parentLabel')}</span>
<span className="text-text-disabled">/</span>
<span className="text-accent-primary">{selectedItem?.label}</span>
</div>
) : (
<span className="text-muted-foreground">
{placeholder || t('common.selectPlaceholder')}
</span>
)}
<div className="flex items-center justify-between">
{value && (
<>
<XIcon
className="h-4 mx-2 cursor-pointer text-muted-foreground"
onClick={handleClear}
/>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
</>
)}
<ChevronDownIcon
size={16}
className="text-muted-foreground/80 shrink-0 ml-2"
aria-hidden="true"
/>
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command value={value}>
<CommandInput placeholder="Search..." />
<CommandList className="overflow-visible">
{options.map((group, idx) => (
<CommandGroup key={idx} heading={group.label}>
{group.options.map((option) => {
const shouldShowSecondary = showSecondaryMenu(
option.value,
option.label,
);
if (shouldShowSecondary) {
const filteredStructuredOutput = filterStructuredOutput(
option.value,
);
return (
<StructuredOutputSecondaryMenu
key={option.value}
data={option}
click={handleSecondaryMenuClick}
filteredStructuredOutput={filteredStructuredOutput}
></StructuredOutputSecondaryMenu>
);
}
return option.children ? (
<HoverCard
key={option.value}
openDelay={100}
closeDelay={150}
>
<HoverCardTrigger asChild>
<CommandItem
onSelect={() => {}}
className="flex items-center justify-between cursor-default"
>
{option.label}
<span className="ml-auto text-muted-foreground">
</span>
</CommandItem>
</HoverCardTrigger>
<HoverCardContent
side="right"
align="start"
className="w-[180px] p-1"
>
{option.children.map((child) => (
<div
key={child.value}
className={cn(
'cursor-pointer rounded-sm px-2 py-1.5 text-sm hover:bg-bg-card hover:text-accent-foreground',
value === child.value &&
'bg-accent text-accent-foreground',
)}
onClick={() => {
onChange?.(child.value);
setOpen(false);
}}
>
{child.label}
</div>
))}
</HoverCardContent>
</HoverCard>
) : (
<CommandItem
key={option.value}
onSelect={() => {
onChange?.(option.value);
setOpen(false);
}}
>
{option.label}
</CommandItem>
);
})}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@ -0,0 +1,81 @@
import { JSONSchema } from '@/components/jsonjoy-builder';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { cn } from '@/lib/utils';
import { get, isPlainObject } from 'lodash';
import { ChevronRight } from 'lucide-react';
import { PropsWithChildren, ReactNode, useCallback } from 'react';
type DataItem = { label: ReactNode; value: string; parentLabel?: ReactNode };
type StructuredOutputSecondaryMenuProps = {
data: DataItem;
click(option: { label: ReactNode; value: string }): void;
filteredStructuredOutput: JSONSchema;
} & PropsWithChildren;
export function StructuredOutputSecondaryMenu({
data,
click,
filteredStructuredOutput,
}: StructuredOutputSecondaryMenuProps) {
const renderAgentStructuredOutput = useCallback(
(values: any, option: { label: ReactNode; value: string }) => {
if (isPlainObject(values) && 'properties' in values) {
return (
<ul className="border-l">
{Object.entries(values.properties).map(([key, value]) => {
const nextOption = {
label: option.label + `.${key}`,
value: option.value + `.${key}`,
};
const dataType = get(value, 'type');
return (
<li key={key} className="pl-1">
<div
onClick={() => click(nextOption)}
className="hover:bg-bg-card p-1 text-text-primary rounded-sm flex justify-between"
>
{key}
<span className="text-text-secondary">{dataType}</span>
</div>
{dataType === 'object' &&
renderAgentStructuredOutput(value, nextOption)}
</li>
);
})}
</ul>
);
}
return <div></div>;
},
[click],
);
return (
<HoverCard key={data.value} openDelay={100} closeDelay={100}>
<HoverCardTrigger asChild>
<li className="hover:bg-bg-card py-1 px-2 text-text-primary rounded-sm text-sm flex justify-between items-center">
{data.label} <ChevronRight className="size-3.5 text-text-secondary" />
</li>
</HoverCardTrigger>
<HoverCardContent
side="left"
align="start"
className={cn(
'min-w-[140px] border border-border rounded-md shadow-lg p-0',
)}
>
<section className="p-2">
<div className="p-1">{data?.parentLabel} structured output:</div>
{renderAgentStructuredOutput(filteredStructuredOutput, data)}
</section>
</HoverCardContent>
</HoverCard>
);
}

View File

@ -2,6 +2,7 @@ import { Collapse } from '@/components/collapse';
import { FormContainer } from '@/components/form-container';
import NumberInput from '@/components/originui/number-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { useIsDarkTheme } from '@/components/theme-provider';
import { Button } from '@/components/ui/button';
import {
Form,
@ -86,6 +87,8 @@ function InvokeForm({ node }: INextOperatorForm) {
const variables = useWatch({ control: form.control, name: 'variables' });
const isDarkTheme = useIsDarkTheme();
useWatchFormChange(node?.id, form);
return (
@ -147,7 +150,7 @@ function InvokeForm({ node }: INextOperatorForm) {
<Editor
height={200}
defaultLanguage="json"
theme="vs-dark"
theme={isDarkTheme ? 'vs-dark' : undefined}
{...field}
/>
</FormControl>

View File

@ -0,0 +1,92 @@
import { get } from 'lodash';
import { ReactNode, useCallback } from 'react';
import { AgentStructuredOutputField, Operator } from '../constant';
import useGraphStore from '../store';
import { filterAgentStructuredOutput } from '../utils/filter-agent-structured-output';
function getNodeId(value: string) {
return value.split('@').at(0);
}
export function useShowSecondaryMenu() {
const { getOperatorTypeFromId } = useGraphStore((state) => state);
const showSecondaryMenu = useCallback(
(value: string, outputLabel: string) => {
const nodeId = getNodeId(value);
return (
getOperatorTypeFromId(nodeId) === Operator.Agent &&
outputLabel === AgentStructuredOutputField
);
},
[getOperatorTypeFromId],
);
return showSecondaryMenu;
}
export function useFilterStructuredOutputByValue() {
const { getOperatorTypeFromId, getNode, clickedNodeId } = useGraphStore(
(state) => state,
);
const filterStructuredOutput = useCallback(
(value: string) => {
const node = getNode(getNodeId(value));
const structuredOutput = get(
node,
`data.form.outputs.${AgentStructuredOutputField}`,
);
const filteredStructuredOutput = filterAgentStructuredOutput(
structuredOutput,
getOperatorTypeFromId(clickedNodeId),
);
return filteredStructuredOutput;
},
[clickedNodeId, getNode, getOperatorTypeFromId],
);
return filterStructuredOutput;
}
export function useFindAgentStructuredOutputLabel() {
const getOperatorTypeFromId = useGraphStore(
(state) => state.getOperatorTypeFromId,
);
const findAgentStructuredOutputLabel = useCallback(
(
value: string,
options: Array<{
label: string;
value: string;
parentLabel?: string | ReactNode;
icon?: ReactNode;
}>,
) => {
// agent structured output
const fields = value.split('@');
if (
getOperatorTypeFromId(fields.at(0)) === Operator.Agent &&
fields.at(1)?.startsWith(AgentStructuredOutputField)
) {
// is agent structured output
const agentOption = options.find((x) => value.includes(x.value));
const jsonSchemaFields = fields
.at(1)
?.slice(AgentStructuredOutputField.length);
return {
...agentOption,
label: (agentOption?.label ?? '') + jsonSchemaFields,
value: value,
};
}
},
[getOperatorTypeFromId],
);
return findAgentStructuredOutputLabel;
}

View File

@ -1,6 +1,6 @@
import { message, notification } from 'antd';
import axios from 'axios';
import { Navigate } from 'umi';
import { history } from 'umi';
import { Authorization } from '@/constants/authorization';
import i18n from '@/locales/config';
@ -48,7 +48,7 @@ request.interceptors.response.use(
});
authorizationUtil.removeAll();
Navigate({ to: Routes.Admin });
history.push(Routes.Admin);
} else if (data?.code && data.code !== 0) {
notification.error({
message: `${i18n.t('message.hint')}: ${data?.code}`,
@ -70,15 +70,16 @@ request.interceptors.response.use(
});
} else if (data?.code === 100) {
message.error(data?.message);
} else if (data?.code === 401) {
} else if (response.status === 401 || data?.code === 401) {
notification.error({
message: data?.message,
description: data?.message,
message: data?.message || response.statusText,
description:
data?.message || RetcodeMessage[response?.status as ResultCode],
duration: 3,
});
authorizationUtil.removeAll();
Navigate({ to: Routes.Admin });
history.push(Routes.Admin);
} else if (data?.code && data.code !== 0) {
notification.error({
message: `${i18n.t('message.hint')}: ${data?.code}`,
@ -93,17 +94,9 @@ request.interceptors.response.use(
});
} else if (response.status === 413 || response?.status === 504) {
message.error(RetcodeMessage[response?.status as ResultCode]);
} else if (response.status === 401) {
notification.error({
message: response.data.message,
description: response.data.message,
duration: 3,
});
authorizationUtil.removeAll();
window.location.href = location.origin + '/admin';
}
return error;
throw error;
},
);
@ -112,7 +105,7 @@ const {
adminLogout,
adminListUsers,
adminCreateUser,
adminGetUserDetails: adminShowUserDetails,
adminGetUserDetails,
adminUpdateUserStatus,
adminUpdateUserPassword,
adminDeleteUser,
@ -260,11 +253,11 @@ export namespace AdminService {
update_date: string;
};
export type AssignRolePermissionInput = {
permissions: Record<string, Partial<PermissionData>>;
};
export type RevokeRolePermissionInput = AssignRolePermissionInput;
export type AssignRolePermissionsInput = Record<
string,
Partial<PermissionData>
>;
export type RevokeRolePermissionInput = AssignRolePermissionsInput;
export type UserDetailWithPermission = {
user: {
@ -293,7 +286,7 @@ export const createUser = (email: string, password: string) =>
});
export const getUserDetails = (email: string) =>
request.get<ResponseData<[AdminService.UserDetail]>>(
adminShowUserDetails(email),
adminGetUserDetails(email),
);
export const listUserDatasets = (email: string) =>
request.get<ResponseData<AdminService.ListUserDatasetItem[]>>(
@ -317,7 +310,10 @@ export const showServiceDetails = (serviceId: number) =>
adminShowServiceDetails(String(serviceId)),
);
export const createRole = (params: { roleName: string; description: string }) =>
export const createRole = (params: {
roleName: string;
description?: string;
}) =>
request.post<ResponseData<AdminService.RoleDetail>>(adminCreateRole, params);
export const updateRoleDescription = (role: string, description: string) =>
request.put<ResponseData<AdminService.RoleDetail>>(
@ -343,15 +339,17 @@ export const getRolePermissions = (role: string) =>
);
export const assignRolePermissions = (
role: string,
params: AdminService.AssignRolePermissionInput,
permissions: Partial<AdminService.AssignRolePermissionsInput>,
) =>
request.post<ResponseData<never>>(adminAssignRolePermissions(role), params);
request.post<ResponseData<never>>(adminAssignRolePermissions(role), {
new_permissions: permissions,
});
export const revokeRolePermissions = (
role: string,
params: AdminService.RevokeRolePermissionInput,
permissions: Partial<AdminService.RevokeRolePermissionInput>,
) =>
request.delete<ResponseData<never>>(adminRevokeRolePermissions(role), {
data: params,
data: { revoke_permissions: permissions },
});
export const updateUserRole = (username: string, role: string) =>
@ -365,12 +363,23 @@ export const getUserPermissions = (username: string) =>
export const listResources = () =>
request.get<ResponseData<AdminService.ResourceType>>(adminListResources);
export const whitelistImportFromExcel = (file: File) => {
const fd = new FormData();
fd.append('file', file);
return request.post<ResponseData<never>>(
'/api/v1/admin/whitelist/import',
fd,
);
};
export default {
login,
logout,
listUsers,
createUser,
showUserDetails: getUserDetails,
getUserDetails,
updateUserStatus,
updateUserPassword,
deleteUser,