mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-01-04 03:25:30 +08:00
Compare commits
13 Commits
4be3754340
...
ff2365b146
| Author | SHA1 | Date | |
|---|---|---|---|
| ff2365b146 | |||
| ac75bcdf95 | |||
| a62a1a5012 | |||
| 361c74ab42 | |||
| 5059d3db18 | |||
| 5674d762f7 | |||
| fa38aed01b | |||
| ab52ffc9c0 | |||
| 5f65c7f48e | |||
| bb9504d1cc | |||
| 5d79912274 | |||
| b52f09adfe | |||
| 27f0d82102 |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -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: |
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
||||
@ -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})
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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 + '@';
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -1529,7 +1529,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
extractorDescription:
|
||||
'使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。',
|
||||
outputFormat: '输出格式',
|
||||
fileFormats: '文件格式',
|
||||
fileFormats: '文件类型',
|
||||
fields: '字段',
|
||||
addParser: '增加解析器',
|
||||
hierarchy: '层次结构',
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -307,6 +307,7 @@ function AdminUserDetail() {
|
||||
};
|
||||
},
|
||||
enabled: !!id,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -617,7 +617,10 @@ export const initialAgentValues = {
|
||||
type: 'string',
|
||||
value: '',
|
||||
},
|
||||
[AgentStructuredOutputField]: {},
|
||||
[AgentStructuredOutputField]: {
|
||||
type: 'Object Array String Number Boolean',
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
92
web/src/pages/agent/hooks/use-build-structured-output.ts
Normal file
92
web/src/pages/agent/hooks/use-build-structured-output.ts
Normal 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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user