diff --git a/.gitignore b/.gitignore
index 403ed35b..6aeb1ca2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,7 +20,6 @@ logs
*.log.*
*.db
*.lock
-!uv.lock
tmp
cache
@@ -29,6 +28,7 @@ cache
.idea
*.nogit*
*.private*
+*.local*
*.secret*
*.pdf
diff --git a/docker-compose.yml b/docker-compose.yml
index f6cb1b22..f133a893 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,6 +3,9 @@ services:
build:
context: .
dockerfile: docker/api.Dockerfile
+ # args:
+ # http_proxy: http://172.19.13.5:7890
+ # https_proxy: http://172.19.13.5:7890
image: yuxi-api:0.1.0
container_name: api-dev
working_dir: /app
@@ -16,7 +19,7 @@ services:
reservations:
devices:
- driver: nvidia
- device_ids: ['1']
+ device_ids: ['0'] # 使用GPU 1
capabilities: [gpu]
ports:
- "5050:5050"
@@ -28,8 +31,8 @@ services:
- NEO4J_URI=bolt://graph:7687
- NEO4J_USERNAME=${NEO4J_USERNAME:-neo4j}
- NEO4J_PASSWORD=${NEO4J_PASSWORD:-0123456789}
- - MILVUS_URI=http://milvus:19530
- - MINERU_OCR_URI=http://mineru-api:5051
+ - MILVUS_URI=http://127.0.0.1:19530
+ - MINERU_OCR_URI=http://127.0.0.1:30000
- MODEL_DIR=/models
- RUNNING_IN_DOCKER=true
command: uv run uvicorn server.main:app --host 0.0.0.0 --port 5050 --reload
@@ -143,61 +146,37 @@ services:
networks:
- app-network
restart: unless-stopped
-
- mineru-api:
+ # lastest version: wget https://gcore.jsdelivr.net/gh/opendatalab/MinerU@master/docker/compose.yaml
+ mineru-sglang:
build:
- context: scripts/mineru-api
+ context: scripts/mineru-sglang
dockerfile: Dockerfile
- image: mineru-api:latest
- container_name: mineru-api
- profiles:
- - all
+ image: mineru-sglang:latest
+ container_name: mineru-sglang
+ ports:
+ - 30000:30000
+ environment:
+ MINERU_MODEL_SOURCE: modelscope
+ entrypoint: mineru-sglang-server
+ command:
+ --host 0.0.0.0
+ --port 30000
+ ulimits:
+ memlock: -1
+ stack: 67108864
+ ipc: host
+ healthcheck:
+ test: ["CMD-SHELL", "curl -f http://localhost:30000/health || exit 1"]
deploy:
resources:
reservations:
devices:
- driver: nvidia
- device_ids: ['0']
+ device_ids: ["0"]
capabilities: [gpu]
- ports:
- - "5051:5051"
networks:
- app-network
restart: unless-stopped
- command: python -m uvicorn app:app --host 0.0.0.0 --port 5051 --app-dir /app
-
-
- # # lastest version: wget https://gcore.jsdelivr.net/gh/opendatalab/MinerU@master/docker/compose.yaml
- # mineru-sglang:
- # build:
- # context: scripts/mineru-sglang
- # dockerfile: Dockerfile
- # image: mineru-sglang:latest
- # container_name: mineru-sglang
- # ports:
- # - 30000:30000
- # environment:
- # MINERU_MODEL_SOURCE: modelscope
- # entrypoint: mineru-sglang-server
- # command:
- # --host 0.0.0.0
- # --port 30000
- # ulimits:
- # memlock: -1
- # stack: 67108864
- # ipc: host
- # healthcheck:
- # test: ["CMD-SHELL", "curl -f http://localhost:30000/health || exit 1"]
- # deploy:
- # resources:
- # reservations:
- # devices:
- # - driver: nvidia
- # device_ids: ["0"]
- # capabilities: [gpu]
- # networks:
- # - app-network
- # restart: unless-stopped
networks:
app-network:
diff --git a/docker/api.Dockerfile b/docker/api.Dockerfile
index e0c9749b..7c9f967b 100644
--- a/docker/api.Dockerfile
+++ b/docker/api.Dockerfile
@@ -1,5 +1,5 @@
# 使用基础镜像
-FROM python:3.12
+FROM nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04
COPY --from=ghcr.io/astral-sh/uv:0.7.2 /uv /uvx /bin/
# 设置工作目录
diff --git a/pyproject.toml b/pyproject.toml
index a2ababb3..dd108932 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,6 +14,7 @@ dependencies = [
"langchain-deepseek>=0.1.3",
"langchain-huggingface>=0.2.0",
"langchain-openai>=0.3.14",
+ "langchain-tavily>=0.2.3",
"langchain-together>=0.3.0",
"langgraph>=0.3.34",
"langgraph-checkpoint-sqlite>=2.0.7",
@@ -21,6 +22,7 @@ dependencies = [
"langsmith>=0.3.37",
"llama-index>=0.12.33",
"llama-index-readers-file>=0.4.7",
+ "mineru[all]>=2.0.3",
"neo4j>=5.28.1",
"openai>=1.76.0",
"opencv-python-headless>=4.11.0.86",
diff --git a/src/agents/tools_factory.py b/src/agents/tools_factory.py
index 71d40f95..b93960d5 100644
--- a/src/agents/tools_factory.py
+++ b/src/agents/tools_factory.py
@@ -3,7 +3,7 @@ import re
from collections.abc import Callable
from typing import Annotated, Any
-from langchain_community.tools.tavily_search import TavilySearchResults
+from langchain_tavily import TavilySearch
from langchain_core.tools import BaseTool, StructuredTool, tool
from pydantic import BaseModel, Field
@@ -151,4 +151,4 @@ _TOOLS_REGISTRY = {
}
if config.enable_web_search:
- _TOOLS_REGISTRY["WebSearchWithTavily"] = TavilySearchResults(max_results=10)
+ _TOOLS_REGISTRY["WebSearchWithTavily"] = TavilySearch(max_results=10)
diff --git a/src/core/knowledgebase.py b/src/core/knowledgebase.py
index 3e93f6e7..88b7332f 100644
--- a/src/core/knowledgebase.py
+++ b/src/core/knowledgebase.py
@@ -668,11 +668,11 @@ class KnowledgeBase:
logger.info(f"Successfully connected to Milvus at {uri}")
return True
except MilvusException as e:
- logger.error(f"Failed to connect to Milvus: {e},请检查 milvus 的容器是否正常运行。")
- logger.error("如果已退出,请重新启动 `docker restart milvus-standalone-dev`。")
+ logger.error(f"Failed to connect to Milvus: {e},请检查 milvus 的容器是否正常运行。{traceback.format_exc()}")
+ logger.error("如果已退出,请重新启动 `docker restart milvus`。")
return False
except Exception as e: # Catch other potential errors like requests.exceptions.ConnectionError
- logger.error(f"An unexpected error occurred while connecting to Milvus at {uri}: {e}")
+ logger.error(f"An unexpected error occurred while connecting to Milvus at {uri}: {e}, {traceback.format_exc()}")
return False
diff --git a/src/plugins/_ocr.py b/src/plugins/_ocr.py
index b19aea36..5c36b697 100644
--- a/src/plugins/_ocr.py
+++ b/src/plugins/_ocr.py
@@ -159,53 +159,26 @@ class OCRPlugin:
:param pdf_path: PDF文件路径
:return: 提取的文本
"""
- mineru_ocr_uri = os.getenv("MINERU_OCR_URI", "http://localhost:5051")
+ mineru_ocr_uri = os.getenv("MINERU_OCR_URI", "http://localhost:30000")
+ mineru_ocr_uri_health = f"{mineru_ocr_uri}/health"
import requests
import json
+ from .mineru import parse_doc
- health_check_response = requests.get(f"{mineru_ocr_uri}/health", timeout=5)
- if health_check_response.status_code != 200 or health_check_response.json().get("status") != "healthy":
- logger.error("Mineru OCR service health check failed.")
+ health_check_response = requests.get(mineru_ocr_uri_health, timeout=5)
+ if health_check_response.status_code != 200:
+ logger.error(f"Mineru OCR service health check failed with {mineru_ocr_uri_health}: {health_check_response.json()}")
raise RuntimeError("Mineru OCR service health check failed. Please check the log use `docker logs mineru-api`")
- # 读取PDF文件
- with open(pdf_path, 'rb') as f:
- files = {'file': f}
- data = {
- 'parse_method': 'ocr', # 使用OCR模式
- 'is_json_md_dump': False, # 不需要保存中间文件
- 'return_layout': False, # 不需要返回布局信息
- 'return_info': False, # 不需要返回额外信息
- 'return_content_list': False, # 不需要返回内容列表
- 'return_images': False, # 不需要返回图片
- }
+ pdf_path_list = [pdf_path]
+ output_dir = os.path.join(os.getcwd(), "tmp", "mineru_ocr")
- try:
- # 发送POST请求到Mineru OCR服务
- response = requests.post(
- f"{mineru_ocr_uri}/file_parse",
- files=files,
- data=data
- )
- response.raise_for_status() # 检查响应状态
+ pdf_text = parse_doc(pdf_path_list, output_dir,
+ backend="vlm-sglang-client",
+ server_url=mineru_ocr_uri)[0]
- # 解析响应
- result = response.json()
- if 'md_content' in result:
- return result['md_content']
- else:
- logger.error("Mineru OCR response does not contain md_content")
- return ""
-
- except requests.exceptions.RequestException as e:
- logger.error(f"Mineru OCR request failed: {str(e)}")
- return ""
- except json.JSONDecodeError as e:
- logger.error(f"Failed to parse Mineru OCR response: {str(e)}")
- return ""
- except Exception as e:
- logger.error(f"Unexpected error in Mineru OCR processing: {str(e)}")
- return ""
+ logger.debug(f"Mineru OCR result: {pdf_text[:50]}(...) total {len(pdf_text)} characters.")
+ return pdf_text
def get_state(task_id):
return GOLBAL_STATE.get(task_id, {})
diff --git a/src/plugins/mineru.py b/src/plugins/mineru.py
new file mode 100644
index 00000000..9edc0133
--- /dev/null
+++ b/src/plugins/mineru.py
@@ -0,0 +1,247 @@
+# Copyright (c) Opendatalab. All rights reserved.
+import copy
+import json
+import os
+from pathlib import Path
+from tqdm import tqdm
+
+from mineru.cli.common import convert_pdf_bytes_to_bytes_by_pypdfium2, prepare_env, read_fn
+from mineru.data.data_reader_writer import FileBasedDataWriter
+from mineru.utils.draw_bbox import draw_layout_bbox, draw_span_bbox
+from mineru.utils.enum_class import MakeMode
+from mineru.backend.vlm.vlm_analyze import doc_analyze as vlm_doc_analyze
+from mineru.backend.pipeline.pipeline_analyze import doc_analyze as pipeline_doc_analyze
+from mineru.backend.pipeline.pipeline_middle_json_mkcontent import union_make as pipeline_union_make
+from mineru.backend.pipeline.model_json_to_middle_json import result_to_middle_json as pipeline_result_to_middle_json
+from mineru.backend.vlm.vlm_middle_json_mkcontent import union_make as vlm_union_make
+
+from src.utils.logging_config import logger
+
+
+def do_parse(
+ output_dir, # Output directory for storing parsing results
+ pdf_file_names: list[str], # List of PDF file names to be parsed
+ pdf_bytes_list: list[bytes], # List of PDF bytes to be parsed
+ p_lang_list: list[str], # List of languages for each PDF, default is 'ch' (Chinese)
+ backend="pipeline", # The backend for parsing PDF, default is 'pipeline'
+ parse_method="auto", # The method for parsing PDF, default is 'auto'
+ p_formula_enable=True, # Enable formula parsing
+ p_table_enable=True, # Enable table parsing
+ server_url=None, # Server URL for vlm-sglang-client backend
+ f_draw_layout_bbox=True, # Whether to draw layout bounding boxes
+ f_draw_span_bbox=True, # Whether to draw span bounding boxes
+ f_dump_md=True, # Whether to dump markdown files
+ f_dump_middle_json=True, # Whether to dump middle JSON files
+ f_dump_model_output=True, # Whether to dump model output files
+ f_dump_orig_pdf=True, # Whether to dump original PDF files
+ f_dump_content_list=True, # Whether to dump content list files
+ f_make_md_mode=MakeMode.MM_MD, # The mode for making markdown content, default is MM_MD
+ start_page_id=0, # Start page ID for parsing, default is 0
+ end_page_id=None, # End page ID for parsing, default is None (parse all pages until the end of the document)
+) -> list[str]:
+
+ if backend == "pipeline":
+ for idx, pdf_bytes in enumerate(pdf_bytes_list):
+ new_pdf_bytes = convert_pdf_bytes_to_bytes_by_pypdfium2(pdf_bytes, start_page_id, end_page_id)
+ pdf_bytes_list[idx] = new_pdf_bytes
+
+ infer_results, all_image_lists, all_pdf_docs, lang_list, ocr_enabled_list = pipeline_doc_analyze(pdf_bytes_list, p_lang_list, parse_method=parse_method, formula_enable=p_formula_enable,table_enable=p_table_enable)
+
+
+ md_results = []
+ for idx, model_list in enumerate(infer_results):
+ model_json = copy.deepcopy(model_list)
+ pdf_file_name = pdf_file_names[idx]
+ local_image_dir, local_md_dir = prepare_env(output_dir, pdf_file_name, parse_method)
+ image_writer, md_writer = FileBasedDataWriter(local_image_dir), FileBasedDataWriter(local_md_dir)
+
+ images_list = all_image_lists[idx]
+ pdf_doc = all_pdf_docs[idx]
+ _lang = lang_list[idx]
+ _ocr_enable = ocr_enabled_list[idx]
+ middle_json = pipeline_result_to_middle_json(model_list, images_list, pdf_doc, image_writer, _lang, _ocr_enable, p_formula_enable)
+
+ pdf_info = middle_json["pdf_info"]
+
+ pdf_bytes = pdf_bytes_list[idx]
+ if f_draw_layout_bbox:
+ draw_layout_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_layout.pdf")
+
+ if f_draw_span_bbox:
+ draw_span_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_span.pdf")
+
+ if f_dump_orig_pdf:
+ md_writer.write(
+ f"{pdf_file_name}_origin.pdf",
+ pdf_bytes,
+ )
+
+ if f_dump_md:
+ image_dir = str(os.path.basename(local_image_dir))
+ md_content_str = pipeline_union_make(pdf_info, f_make_md_mode, image_dir)
+ md_writer.write_string(
+ f"{pdf_file_name}.md",
+ md_content_str,
+ )
+ md_results.append(md_content_str)
+
+ if f_dump_content_list:
+ image_dir = str(os.path.basename(local_image_dir))
+ content_list = pipeline_union_make(pdf_info, MakeMode.CONTENT_LIST, image_dir)
+ md_writer.write_string(
+ f"{pdf_file_name}_content_list.json",
+ json.dumps(content_list, ensure_ascii=False, indent=4),
+ )
+
+ if f_dump_middle_json:
+ md_writer.write_string(
+ f"{pdf_file_name}_middle.json",
+ json.dumps(middle_json, ensure_ascii=False, indent=4),
+ )
+
+ if f_dump_model_output:
+ md_writer.write_string(
+ f"{pdf_file_name}_model.json",
+ json.dumps(model_json, ensure_ascii=False, indent=4),
+ )
+
+ logger.info(f"local output dir is {local_md_dir}")
+
+ return md_results
+
+ else:
+ if backend.startswith("vlm-"):
+ backend = backend[4:]
+
+ f_draw_span_bbox = False
+ parse_method = "vlm"
+ md_results = []
+
+ for idx, pdf_bytes in enumerate(tqdm(pdf_bytes_list, desc="Parsing documents bytes")):
+ pdf_file_name = pdf_file_names[idx]
+ pdf_bytes = convert_pdf_bytes_to_bytes_by_pypdfium2(pdf_bytes, start_page_id, end_page_id)
+ local_image_dir, local_md_dir = prepare_env(output_dir, pdf_file_name, parse_method)
+ image_writer, md_writer = FileBasedDataWriter(local_image_dir), FileBasedDataWriter(local_md_dir)
+ middle_json, infer_result = vlm_doc_analyze(pdf_bytes, image_writer=image_writer, backend=backend, server_url=server_url)
+
+ pdf_info = middle_json["pdf_info"]
+
+ if f_draw_layout_bbox:
+ draw_layout_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_layout.pdf")
+
+ if f_draw_span_bbox:
+ draw_span_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_span.pdf")
+
+ if f_dump_orig_pdf:
+ md_writer.write(
+ f"{pdf_file_name}_origin.pdf",
+ pdf_bytes,
+ )
+
+ if f_dump_md:
+ image_dir = str(os.path.basename(local_image_dir))
+ md_content_str = vlm_union_make(pdf_info, f_make_md_mode, image_dir)
+ md_writer.write_string(
+ f"{pdf_file_name}.md",
+ md_content_str,
+ )
+ md_results.append(md_content_str)
+
+ if f_dump_content_list:
+ image_dir = str(os.path.basename(local_image_dir))
+ content_list = vlm_union_make(pdf_info, MakeMode.CONTENT_LIST, image_dir)
+ md_writer.write_string(
+ f"{pdf_file_name}_content_list.json",
+ json.dumps(content_list, ensure_ascii=False, indent=4),
+ )
+
+ if f_dump_middle_json:
+ md_writer.write_string(
+ f"{pdf_file_name}_middle.json",
+ json.dumps(middle_json, ensure_ascii=False, indent=4),
+ )
+
+ if f_dump_model_output:
+ model_output = ("\n" + "-" * 50 + "\n").join(infer_result)
+ md_writer.write_string(
+ f"{pdf_file_name}_model_output.txt",
+ model_output,
+ )
+
+ logger.info(f"local output dir is {local_md_dir}")
+
+ return md_results
+
+
+def parse_doc(
+ path_list: list[Path],
+ output_dir,
+ lang="ch",
+ backend="pipeline",
+ method="auto",
+ server_url=None,
+ start_page_id=0, # Start page ID for parsing, default is 0
+ end_page_id=None # End page ID for parsing, default is None (parse all pages until the end of the document)
+) -> list[str]:
+ """
+ Parameter description:
+ path_list: List of document paths to be parsed, can be PDF or image files.
+ output_dir: Output directory for storing parsing results.
+ lang: Language option, default is 'ch', optional values include['ch', 'ch_server', 'ch_lite', 'en', 'korean', 'japan', 'chinese_cht', 'ta', 'te', 'ka']。
+ Input the languages in the pdf (if known) to improve OCR accuracy. Optional.
+ Adapted only for the case where the backend is set to "pipeline"
+ backend: the backend for parsing pdf:
+ pipeline: More general.
+ vlm-transformers: More general.
+ vlm-sglang-engine: Faster(engine).
+ vlm-sglang-client: Faster(client).
+ without method specified, pipeline will be used by default.
+ method: the method for parsing pdf:
+ auto: Automatically determine the method based on the file type.
+ txt: Use text extraction method.
+ ocr: Use OCR method for image-based PDFs.
+ Without method specified, 'auto' will be used by default.
+ Adapted only for the case where the backend is set to "pipeline".
+ server_url: When the backend is `sglang-client`, you need to specify the server_url, for example:`http://127.0.0.1:30000`
+ """
+ try:
+ file_name_list = []
+ pdf_bytes_list = []
+ lang_list = []
+ for path in tqdm(path_list, desc="Parsing documents"):
+ file_name = str(Path(path).stem)
+ pdf_bytes = read_fn(path)
+ file_name_list.append(file_name)
+ pdf_bytes_list.append(pdf_bytes)
+ lang_list.append(lang)
+
+ result = do_parse(
+ output_dir=output_dir,
+ pdf_file_names=file_name_list,
+ pdf_bytes_list=pdf_bytes_list,
+ p_lang_list=lang_list,
+ backend=backend,
+ parse_method=method,
+ server_url=server_url,
+ start_page_id=start_page_id,
+ end_page_id=end_page_id
+ )
+ return result if result else [""]
+
+ except Exception as e:
+ logger.exception(e)
+ return [""]
+
+if __name__ == "__main__":
+
+ pdf_files_dir = "/home/zwj/workspace/projects/Yuxi-Know/test/struct_pdf"
+ output_dir = "/home/zwj/workspace/projects/Yuxi-Know/test/struct_pdf_output"
+ pdf_suffixes = [".pdf"]
+ image_suffixes = [".png", ".jpeg", ".jpg"]
+
+ doc_path_list = []
+ for doc_path in Path(pdf_files_dir).glob('*'):
+ if doc_path.suffix in pdf_suffixes + image_suffixes:
+ doc_path_list.append(doc_path)
+
+ parse_doc(doc_path_list, output_dir, backend="vlm-sglang-client", server_url="http://172.19.13.5:30000") # faster(client).
\ No newline at end of file
diff --git a/src/utils/logging_config.py b/src/utils/logging_config.py
index 26a48417..376564f7 100644
--- a/src/utils/logging_config.py
+++ b/src/utils/logging_config.py
@@ -3,52 +3,43 @@ import os
from datetime import datetime
import pytz
-from colorlog import ColoredFormatter
+
+from loguru import logger
DATETIME = datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d-%H%M%S')
# DATETIME = "debug" # 为了方便,调试的时候输出到 debug.log 文件
LOG_FILE = f'saves/log/project-{DATETIME}.log'
-
-def setup_logger(name, level=logging.DEBUG, console=True):
+def setup_logger(name, level="DEBUG", console=True):
+ """使用 loguru 设置日志记录器"""
os.makedirs("saves/log", exist_ok=True)
-
- """Function to setup logger with the given name and log file."""
- logger = logging.getLogger(name)
- logger.setLevel(level)
-
- # 清除已有的 Handler,防止重复添加
- if logger.hasHandlers():
- logger.handlers.clear()
-
- # 文件日志(无颜色)
- file_handler = logging.FileHandler(LOG_FILE, encoding='utf-8')
- file_handler.setLevel(level)
- file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
- file_handler.setFormatter(file_formatter)
- logger.addHandler(file_handler)
-
- # 控制台日志(有颜色)
+
+ # 移除默认的 handler
+ logger.remove()
+
+ # 添加文件日志(无颜色)
+ logger.add(
+ LOG_FILE,
+ level=level,
+ format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {name} - {message}",
+ encoding="utf-8",
+ rotation="10 MB", # 文件大小达到 10MB 时轮转
+ retention="30 days", # 保留30天的日志
+ compression="zip" # 压缩旧日志文件
+ )
+
+ # 添加控制台日志(有颜色)
if console:
- console_handler = logging.StreamHandler()
- console_handler.setLevel(level)
- color_formatter = ColoredFormatter(
- "%(log_color)s%(asctime)s - %(levelname)s - %(name)s - %(message)s",
- datefmt="%Y-%m-%d %H:%M:%S",
- log_colors={
- 'DEBUG': 'cyan',
- 'INFO': 'green',
- 'WARNING': 'yellow',
- 'ERROR': 'red',
- 'CRITICAL': 'bold_red',
- }
+ logger.add(
+ lambda msg: print(msg, end=""),
+ level=level,
+ format=" 调整分块参数可以控制文本的切分方式,影响检索质量和文档加载效率。 每个文本片段的最大字符数 相邻文本片段间的重叠字符数 点击或者把文件拖拽到这里上传
+ 目前仅支持上传文本文件,如 .pdf, .txt, .md。且同名文件无法重复添加
+
+ 支持添加网页内容,系统会自动抓取网页文本并进行分块。请确保URL格式正确且可以公开访问。
+
点击或者把文件拖拽到这里上传
-- 目前仅支持上传文本文件,如 .pdf, .txt, .md。且同名文件无法重复添加 -
-- 支持添加网页内容,系统会自动抓取网页文本并进行分块。请确保URL格式正确且可以公开访问。 -
-