revert 6294652de8faae958610cbf84552b7399593a6c8
revert 增加图片文本化接口、切片图片文本抽取‘
This commit is contained in:
parent
6294652de8
commit
93ced6ceb0
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
BIN
__pycache__/app.cpython-312.pyc
Normal file
BIN
__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
__pycache__/checkpointer_config.cpython-312.pyc
Normal file
BIN
__pycache__/checkpointer_config.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/chunk_text.cpython-312.pyc
Normal file
BIN
__pycache__/chunk_text.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/doc2pdf.cpython-312.pyc
Normal file
BIN
__pycache__/doc2pdf.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/documents_prompt.cpython-313.pyc
Normal file
BIN
__pycache__/documents_prompt.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
__pycache__/fileparse_util.cpython-312.pyc
Normal file
BIN
__pycache__/fileparse_util.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/main_agent.cpython-312.pyc
Normal file
BIN
__pycache__/main_agent.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
18
apitest.py
18
apitest.py
@ -1,18 +0,0 @@
|
||||
import base64
|
||||
import requests
|
||||
|
||||
url = "http://192.168.0.46:59085/split_image"
|
||||
image_path = r"E:\ZKYNLP\aidocproject\audiotransaction\gwdoc\船舶图片.png"
|
||||
|
||||
with open(image_path, "rb") as f:
|
||||
image_base64 = base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
payload = {
|
||||
"content": "船舶主发动机为低速二冲程船用推进柴油机,是船舶推进系统的核心动力装置,其主要功能是将燃油化学能转化为机械能,通过曲轴输出转矩,驱动推进轴系和螺旋桨,实现船舶航行。",
|
||||
"filename": "船舶图片.png",
|
||||
"image_base64": image_base64,
|
||||
}
|
||||
|
||||
resp = requests.post(url, json=payload, timeout=120)
|
||||
print(resp.status_code)
|
||||
print(resp.json())
|
||||
112
app.py
112
app.py
@ -15,7 +15,7 @@ try:
|
||||
except ImportError:
|
||||
from langchain.prompts import FewShotPromptTemplate, PromptTemplate
|
||||
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam, ChatCompletionMessageParam
|
||||
from chunk_text import get_chunk_bbox, split_text_preserve_sentences, data_replace, chunk_check,merge_short_slices,find_and_read_content_list,process_pdf_file,process_other_file,safe_original_filename,image_base64_to_data_url,extract_image_text,prepare_split_image
|
||||
from chunk_text import get_chunk_bbox, split_text_preserve_sentences, data_replace, chunk_check,merge_short_slices,find_and_read_content_list,process_pdf_file,process_other_file
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
import requests
|
||||
from pathlib import PurePath, Path as PathLib
|
||||
@ -63,20 +63,14 @@ from doc2pdf import Doc2PDF
|
||||
from workflow_registry import WORKFLOW_CONFIG, VALID_ROUTE_FLAGS
|
||||
from main_agent import extract_conversation_title
|
||||
from workflows.history_manager import filter_image_urls, preprocess_from_request
|
||||
from config import VLM_CONCURRENCY, set_request_user_config
|
||||
from config import set_request_user_config
|
||||
from checkpointer_config import (
|
||||
CheckpointerManager,
|
||||
checkpointer_manager
|
||||
)
|
||||
app = FastAPI(max_request_size=1024 * 1024 * 10)
|
||||
# =============== PDF 文件目录配置 ===============
|
||||
# 创建 PDF 文件目录(用于存储生成的 PDF 等文件)
|
||||
PDF_DIR = os.path.join(os.path.dirname(__file__), "created_pdf")
|
||||
# 创建 word 文件目录(用于存储生成的 word 等文件)
|
||||
WORD_DIR = os.path.join(os.path.dirname(__file__), "created_word")
|
||||
os.makedirs(PDF_DIR, exist_ok=True)
|
||||
VLM_SEMAPHORE = asyncio.Semaphore(max(1, VLM_CONCURRENCY))
|
||||
converter = None
|
||||
|
||||
converter = Doc2PDF()
|
||||
DATA_DIR = "/app/files"
|
||||
load_dotenv()
|
||||
|
||||
@ -99,13 +93,6 @@ STREAM_RESPONSE_HEADERS = {
|
||||
}
|
||||
|
||||
|
||||
def get_doc_converter() -> Doc2PDF:
|
||||
global converter
|
||||
if converter is None:
|
||||
converter = Doc2PDF()
|
||||
return converter
|
||||
|
||||
|
||||
def llm_text_stream_response(content) -> StreamingResponse:
|
||||
return StreamingResponse(
|
||||
content,
|
||||
@ -117,16 +104,20 @@ def llm_text_stream_response(content) -> StreamingResponse:
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""应用启动时初始化"""
|
||||
try:
|
||||
await checkpointer_manager.setup()
|
||||
# 初始化智能体使用统计表
|
||||
from tools.agent_usage_statistics import init_agent_usage_table
|
||||
await init_agent_usage_table()
|
||||
logger.info("应用启动完成,PostgreSQL Checkpointer 已配置")
|
||||
except Exception as e:
|
||||
logger.warning(f"Checkpointer 初始化失败,非 Agent 接口仍可使用: {e}")
|
||||
await checkpointer_manager.setup()
|
||||
# 初始化智能体使用统计表
|
||||
from tools.agent_usage_statistics import init_agent_usage_table
|
||||
await init_agent_usage_table()
|
||||
logger.info("应用启动完成,PostgreSQL Checkpointer 已配置")
|
||||
|
||||
|
||||
# =============== PDF 文件目录配置 ===============
|
||||
# 创建 PDF 文件目录(用于存储生成的 PDF 等文件)
|
||||
PDF_DIR = os.path.join(os.path.dirname(__file__), "created_pdf")
|
||||
# 创建 word 文件目录(用于存储生成的 word 等文件)
|
||||
WORD_DIR = os.path.join(os.path.dirname(__file__), "created_word")
|
||||
os.makedirs(PDF_DIR, exist_ok=True)
|
||||
|
||||
class RewriteRequest(BaseModel):
|
||||
rewrite_type: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
@ -1518,75 +1509,6 @@ class RequestWrapper(BaseModel):
|
||||
pdf_contents: List[PdfContentItem]
|
||||
|
||||
|
||||
class Base64ImageRequest(BaseModel):
|
||||
content: Optional[str] = Field(
|
||||
None,
|
||||
description="Optional text around the image, for example: 图3.1 船舶主发动机组成图(images/test.jpg)",
|
||||
)
|
||||
filename: str = Field(..., description="Original image filename, for example test.png")
|
||||
image_base64: str = Field(..., description="Base64 image content, with or without data:image/... prefix")
|
||||
|
||||
|
||||
async def analyze_image_with_vlm(image_bytes: bytes, suffix: str) -> str:
|
||||
image_url = image_base64_to_data_url(image_bytes, suffix)
|
||||
async with VLM_SEMAPHORE:
|
||||
return await model_api.OpenaiAPI.open_api_vl_without_thinking(image_url)
|
||||
|
||||
|
||||
@app.post("/split_image")
|
||||
async def image_base64(request_data: Base64ImageRequest):
|
||||
filename = safe_original_filename(request_data.filename)
|
||||
suffix = PathLib(filename).suffix.lower()
|
||||
if suffix not in {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".gif", ".tif", ".tiff"}:
|
||||
raise HTTPException(status_code=400, detail="Unsupported image type")
|
||||
|
||||
image_bytes, processed_suffix = prepare_split_image(request_data.image_base64, suffix)
|
||||
if not image_bytes:
|
||||
raise HTTPException(status_code=400, detail="decoded image is empty")
|
||||
|
||||
temp_path = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=processed_suffix) as tmp:
|
||||
tmp.write(image_bytes)
|
||||
temp_path = PathLib(tmp.name)
|
||||
|
||||
ocr_data = await extract_image_text(temp_path)
|
||||
try:
|
||||
temp_path.unlink()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete temp image {temp_path}: {e}")
|
||||
finally:
|
||||
temp_path = None
|
||||
|
||||
content = (request_data.content or "").strip()
|
||||
ocr_full_text = (ocr_data.get("full_text") or "").strip()
|
||||
|
||||
if not content and not ocr_full_text:
|
||||
vlm_text = await analyze_image_with_vlm(image_bytes, processed_suffix)
|
||||
image_content = "图片文本描述为:" + (vlm_text or "").strip()
|
||||
elif content and ocr_full_text:
|
||||
image_content = f"图片上下文内容为:{content}, {ocr_full_text}"
|
||||
elif content:
|
||||
image_content = "图片上下文内容为:" + content
|
||||
else:
|
||||
image_content = ocr_full_text
|
||||
image_content = "图片文件名为:" + filename + ",图片相关内容为:" + image_content
|
||||
return JSONResponse(
|
||||
{
|
||||
"code": 200,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"image_content": image_content,
|
||||
"filename": filename,
|
||||
},
|
||||
}
|
||||
)
|
||||
finally:
|
||||
if temp_path and temp_path.exists():
|
||||
try:
|
||||
temp_path.unlink()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete temp image {temp_path}: {e}")
|
||||
|
||||
|
||||
@app.post("/split_content_list")
|
||||
@ -1668,7 +1590,7 @@ async def split_result(
|
||||
logger.info(f"转换 DOCX -> PDF: {filename}")
|
||||
|
||||
# 执行转换
|
||||
get_doc_converter().convert(str(temp_input_path), output_dir=str(DATA_DIR))
|
||||
converter.convert(str(temp_input_path), output_dir=str(DATA_DIR))
|
||||
|
||||
# ? 使用临时文件的基础名查找 PDF
|
||||
temp_base_name = temp_input_path.stem # 如 tmp2bb0l4cn
|
||||
|
||||
387
chunk_text.py
387
chunk_text.py
@ -8,293 +8,20 @@ import re
|
||||
from typing import List, Dict, Optional, Union, Any, Tuple
|
||||
# 指定 JSON 文件路径
|
||||
# file_path = '/storage01/home/hdf/project/wxkgrag/kgextract/output/122-06A0014-B01001_发动机-维修手册_content_list.json'
|
||||
try:
|
||||
from filename_proceess_and_kgquery import get_entity
|
||||
except ImportError:
|
||||
get_entity = None
|
||||
from pathlib import PurePath, PurePosixPath, Path as PathLib
|
||||
from pathlib import PurePath, Path as PathLib
|
||||
from fastapi import FastAPI, File, Path, UploadFile, HTTPException, Form, Request, Header, Body
|
||||
from fileparse_util import process_document
|
||||
from io import BytesIO
|
||||
from urllib.parse import unquote, urlparse
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
import threading
|
||||
import aiofiles
|
||||
import httpx
|
||||
import logging
|
||||
from rapidocr_onnxruntime import RapidOCR
|
||||
from PIL import Image, ImageOps, UnidentifiedImageError
|
||||
from config import (
|
||||
API_URLS,
|
||||
IMAGE_DIR,
|
||||
MAX_SPLIT_IMAGE_BASE64_CHARS,
|
||||
MAX_SPLIT_IMAGE_BYTES,
|
||||
MAX_SPLIT_IMAGE_INPUT_BYTES,
|
||||
OCR_CONCURRENCY,
|
||||
SPLIT_IMAGE_COMPRESS_THRESHOLD_BYTES,
|
||||
SPLIT_IMAGE_JPEG_QUALITY,
|
||||
SPLIT_IMAGE_MAX_DIMENSION,
|
||||
)
|
||||
try:
|
||||
from config import SHIP_MODEL_NAME
|
||||
except ImportError:
|
||||
SHIP_MODEL_NAME = ""
|
||||
from config import API_URLS
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ocr_engine = RapidOCR()
|
||||
OCR_SEMAPHORE = asyncio.Semaphore(max(1, OCR_CONCURRENCY))
|
||||
_ocr_thread_lock = threading.Lock()
|
||||
_image_ocr_cache: Dict[str, str] = {}
|
||||
_image_ocr_cache_lock = threading.Lock()
|
||||
_api_url_inflight = [0] * len(API_URLS)
|
||||
_api_url_lock = threading.Lock()
|
||||
|
||||
|
||||
def _image_mime_type(suffix: str) -> str:
|
||||
return {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".bmp": "image/bmp",
|
||||
".webp": "image/webp",
|
||||
".gif": "image/gif",
|
||||
".tif": "image/tiff",
|
||||
".tiff": "image/tiff",
|
||||
}.get((suffix or "").lower(), "image/png")
|
||||
|
||||
|
||||
def _image_save_format(suffix: str) -> tuple[str, dict]:
|
||||
suffix = (suffix or "").lower()
|
||||
if suffix in {".jpg", ".jpeg"}:
|
||||
return "JPEG", {"quality": SPLIT_IMAGE_JPEG_QUALITY, "optimize": True}
|
||||
if suffix == ".webp":
|
||||
return "WEBP", {"quality": SPLIT_IMAGE_JPEG_QUALITY, "method": 6}
|
||||
if suffix == ".png":
|
||||
return "PNG", {"optimize": True, "compress_level": 9}
|
||||
if suffix in {".tif", ".tiff"}:
|
||||
return "TIFF", {"compression": "tiff_lzw"}
|
||||
if suffix == ".gif":
|
||||
return "GIF", {"optimize": True}
|
||||
if suffix == ".bmp":
|
||||
return "BMP", {}
|
||||
return "PNG", {"optimize": True, "compress_level": 9}
|
||||
|
||||
|
||||
def _compression_suffix(suffix: str) -> str:
|
||||
suffix = (suffix or "").lower()
|
||||
if suffix in {".jpg", ".jpeg", ".png", ".webp"}:
|
||||
return suffix
|
||||
if suffix in {".bmp", ".gif", ".tif", ".tiff"}:
|
||||
return ".png"
|
||||
return ".png"
|
||||
|
||||
|
||||
def _save_image_to_bytes(img: Image.Image, suffix: str) -> bytes:
|
||||
image_format, save_options = _image_save_format(suffix)
|
||||
if image_format in {"JPEG", "WEBP", "BMP"} and img.mode not in {"RGB", "L"}:
|
||||
img = img.convert("RGB")
|
||||
elif image_format in {"PNG", "TIFF", "GIF"} and img.mode not in {"RGB", "RGBA", "L", "P"}:
|
||||
img = img.convert("RGBA")
|
||||
|
||||
output = BytesIO()
|
||||
img.save(output, format=image_format, **save_options)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def _save_image_as_jpeg(img: Image.Image) -> bytes:
|
||||
if img.mode not in {"RGB", "L"}:
|
||||
img = img.convert("RGB")
|
||||
last_bytes = b""
|
||||
for quality in (SPLIT_IMAGE_JPEG_QUALITY, 82, 74):
|
||||
output = BytesIO()
|
||||
img.save(output, format="JPEG", quality=quality, optimize=True)
|
||||
last_bytes = output.getvalue()
|
||||
if len(last_bytes) <= MAX_SPLIT_IMAGE_BYTES:
|
||||
break
|
||||
return last_bytes
|
||||
|
||||
|
||||
def compress_image_bytes(image_bytes: bytes, suffix: str = "") -> tuple[bytes, str]:
|
||||
if not image_bytes:
|
||||
raise HTTPException(status_code=400, detail="decoded image is empty")
|
||||
if len(image_bytes) > MAX_SPLIT_IMAGE_INPUT_BYTES:
|
||||
raise HTTPException(status_code=413, detail="image input is too large")
|
||||
|
||||
suffix = (suffix or "").lower()
|
||||
try:
|
||||
with Image.open(BytesIO(image_bytes)) as img:
|
||||
img = ImageOps.exif_transpose(img)
|
||||
max_side = max(img.size)
|
||||
should_resize = SPLIT_IMAGE_MAX_DIMENSION > 0 and max_side > SPLIT_IMAGE_MAX_DIMENSION
|
||||
should_reencode = len(image_bytes) > SPLIT_IMAGE_COMPRESS_THRESHOLD_BYTES
|
||||
|
||||
if not should_resize and not should_reencode:
|
||||
if len(image_bytes) > MAX_SPLIT_IMAGE_BYTES:
|
||||
raise HTTPException(status_code=413, detail="image is too large")
|
||||
return image_bytes, suffix
|
||||
|
||||
if should_resize:
|
||||
scale = SPLIT_IMAGE_MAX_DIMENSION / max_side
|
||||
new_size = (
|
||||
max(1, int(img.width * scale)),
|
||||
max(1, int(img.height * scale)),
|
||||
)
|
||||
resample = getattr(getattr(Image, "Resampling", Image), "LANCZOS")
|
||||
img = img.resize(new_size, resample)
|
||||
|
||||
compressed_suffix = _compression_suffix(suffix)
|
||||
compressed_bytes = _save_image_to_bytes(img, compressed_suffix)
|
||||
|
||||
if len(compressed_bytes) > MAX_SPLIT_IMAGE_BYTES:
|
||||
compressed_bytes = _save_image_as_jpeg(img)
|
||||
compressed_suffix = ".jpg"
|
||||
except UnidentifiedImageError as exc:
|
||||
raise HTTPException(status_code=400, detail="image content is not a supported image") from exc
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to compress image, using original bytes: {exc}")
|
||||
compressed_bytes = image_bytes
|
||||
compressed_suffix = suffix
|
||||
|
||||
if len(compressed_bytes) >= len(image_bytes) and len(image_bytes) <= MAX_SPLIT_IMAGE_BYTES:
|
||||
return image_bytes, suffix
|
||||
if len(compressed_bytes) > MAX_SPLIT_IMAGE_BYTES:
|
||||
raise HTTPException(status_code=413, detail="image is too large after compression")
|
||||
return compressed_bytes, compressed_suffix
|
||||
|
||||
|
||||
def prepare_split_image(image_base64: str, suffix: str = "") -> tuple[bytes, str]:
|
||||
raw = (image_base64 or "").strip()
|
||||
if not raw:
|
||||
raise HTTPException(status_code=400, detail="image_base64 is empty")
|
||||
|
||||
if "," in raw and raw.split(",", 1)[0].lower().startswith("data:image"):
|
||||
raw = raw.split(",", 1)[1]
|
||||
|
||||
raw = "".join(raw.split())
|
||||
if len(raw) > MAX_SPLIT_IMAGE_BASE64_CHARS:
|
||||
raise HTTPException(status_code=413, detail="image_base64 is too large")
|
||||
|
||||
try:
|
||||
image_bytes = base64.b64decode(raw, validate=True)
|
||||
except (binascii.Error, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail="image_base64 is not valid base64") from exc
|
||||
|
||||
return compress_image_bytes(image_bytes, suffix)
|
||||
|
||||
|
||||
def decode_base64_image(image_base64: str, suffix: str = "") -> bytes:
|
||||
image_bytes, _ = prepare_split_image(image_base64, suffix)
|
||||
return image_bytes
|
||||
|
||||
|
||||
def safe_original_filename(filename: str) -> str:
|
||||
name = os.path.basename((filename or "").strip())
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="filename is empty")
|
||||
safe_name = re.sub(r"[^\w.\-\u4e00-\u9fff]+", "_", name).strip("._")
|
||||
if not safe_name:
|
||||
raise HTTPException(status_code=400, detail="filename is invalid")
|
||||
return safe_name
|
||||
|
||||
|
||||
def _strip_markdown_image_path(img_path: str) -> str:
|
||||
value = (img_path or "").strip()
|
||||
match = re.match(r"!\[[^\]]*\]\((.*?)\)", value)
|
||||
return match.group(1).strip() if match else value
|
||||
|
||||
|
||||
def resolve_local_image_path(img_path: str) -> Optional[PathLib]:
|
||||
value = _strip_markdown_image_path(str(img_path or ""))
|
||||
if not value:
|
||||
return None
|
||||
|
||||
parsed_path = unquote(urlparse(value).path or value)
|
||||
marker = "/images/"
|
||||
if marker in parsed_path:
|
||||
relative_path = parsed_path.split(marker, 1)[1]
|
||||
else:
|
||||
normalized = parsed_path.replace("\\", "/").lstrip("/")
|
||||
if normalized.startswith("images/"):
|
||||
relative_path = normalized.split("/", 1)[1]
|
||||
else:
|
||||
relative_path = normalized
|
||||
|
||||
relative_path = relative_path.replace("\\", "/").lstrip("/")
|
||||
if not relative_path:
|
||||
return None
|
||||
|
||||
image_root = PathLib(IMAGE_DIR).resolve()
|
||||
local_path = (image_root / PurePosixPath(relative_path)).resolve()
|
||||
try:
|
||||
local_path.relative_to(image_root)
|
||||
except ValueError:
|
||||
logger.warning(f"Image path is outside IMAGE_DIR and will be ignored: {img_path}")
|
||||
return None
|
||||
|
||||
return local_path if local_path.is_file() else None
|
||||
|
||||
|
||||
def extract_image_text_sync(image_path: Union[str, PathLib]) -> dict:
|
||||
resolved_path = str(PathLib(image_path).resolve())
|
||||
with _image_ocr_cache_lock:
|
||||
cached_text = _image_ocr_cache.get(resolved_path)
|
||||
if cached_text is not None:
|
||||
return {"texts": [], "full_text": cached_text, "elapse": None}
|
||||
|
||||
with _ocr_thread_lock:
|
||||
ocr_result, elapse = ocr_engine(resolved_path)
|
||||
|
||||
texts = [item[1] for item in (ocr_result or [])]
|
||||
full_text = "图片包含的文字内容为:" + ",".join(texts) if texts else ""
|
||||
with _image_ocr_cache_lock:
|
||||
_image_ocr_cache[resolved_path] = full_text
|
||||
return {
|
||||
"texts": texts,
|
||||
"full_text": full_text,
|
||||
"elapse": elapse,
|
||||
}
|
||||
|
||||
|
||||
def build_image_markdown_with_ocr(markdown_image: str, local_image_path: Optional[PathLib]) -> str:
|
||||
if "图片包含的文字内容为:" in markdown_image:
|
||||
return markdown_image
|
||||
if not local_image_path:
|
||||
return markdown_image
|
||||
|
||||
try:
|
||||
ocr_data = extract_image_text_sync(local_image_path)
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to OCR local image {local_image_path}: {exc}")
|
||||
return markdown_image
|
||||
|
||||
ocr_full_text = (ocr_data.get("full_text") or "").strip()
|
||||
if not ocr_full_text:
|
||||
return markdown_image
|
||||
return f"{markdown_image}\n{ocr_full_text}"
|
||||
|
||||
|
||||
def image_base64_to_data_url(image_base64: Union[str, bytes], suffix: str) -> str:
|
||||
if isinstance(image_base64, bytes):
|
||||
raw = base64.b64encode(image_base64).decode("ascii")
|
||||
else:
|
||||
raw = (image_base64 or "").strip()
|
||||
if "," in raw and raw.split(",", 1)[0].lower().startswith("data:image"):
|
||||
raw = raw.split(",", 1)[1]
|
||||
raw = "".join(raw.split())
|
||||
|
||||
mime_type = _image_mime_type(suffix)
|
||||
return f"data:{mime_type};base64,{raw}"
|
||||
|
||||
|
||||
async def extract_image_text(image_path: PathLib) -> dict:
|
||||
async with OCR_SEMAPHORE:
|
||||
return await asyncio.to_thread(extract_image_text_sync, image_path)
|
||||
|
||||
def format_entity_text(data):
|
||||
# 1. 定义实体类型与属性的映射关系
|
||||
# 格式: '实体类型': [ ('前缀1', '属性1'), ('前缀2', '属性2') ]
|
||||
@ -982,35 +709,18 @@ def data_replace(data, prefix):
|
||||
遍历 data 列表,若元素包含 'img_path' 字段,则移除前缀 'images/' 并拼接新前缀。
|
||||
使用 pathlib 安全处理路径。
|
||||
"""
|
||||
from pathlib import PurePosixPath
|
||||
for ins in data:
|
||||
if isinstance(ins, dict) and "img_path" in ins:
|
||||
img_path = ins["img_path"]
|
||||
if isinstance(img_path, list):
|
||||
ins["img_path"] = "\n".join(
|
||||
data_replace([{"img_path": str(item)}], prefix)[0]["img_path"]
|
||||
for item in img_path
|
||||
if str(item).strip()
|
||||
)
|
||||
continue
|
||||
|
||||
img_path = str(img_path)
|
||||
if img_path.strip().startswith("!["):
|
||||
local_image_path = resolve_local_image_path(img_path)
|
||||
ins["img_path"] = build_image_markdown_with_ocr(img_path, local_image_path)
|
||||
continue
|
||||
|
||||
p = PurePosixPath(img_path)
|
||||
# 如果以 images/ 开头,去掉第一级目录
|
||||
if p.parts and p.parts[0] == "images":
|
||||
relative_path = str(PurePosixPath(*p.parts[1:]))
|
||||
markdown_image = f""
|
||||
local_image_path = resolve_local_image_path(relative_path)
|
||||
ins["img_path"] = build_image_markdown_with_ocr(markdown_image, local_image_path)
|
||||
ins["img_path"] = f""
|
||||
else:
|
||||
# 否则保留原路径(或按需处理)
|
||||
markdown_image = f""
|
||||
local_image_path = resolve_local_image_path(img_path)
|
||||
ins["img_path"] = build_image_markdown_with_ocr(markdown_image, local_image_path)
|
||||
ins["img_path"] = f""
|
||||
return data
|
||||
|
||||
def filter_content_bbox(data, max_length=3000):
|
||||
@ -1246,42 +956,6 @@ def merge_short_slices(slices, min_length=30,filename="122-06A0014-B01003_雷达
|
||||
"positions": pending_positions
|
||||
})
|
||||
|
||||
try:
|
||||
final_result = get_entity(filename)
|
||||
xinghao = find_ship_info_by_hull(SHIP_MODEL_NAME, final_result)
|
||||
if xinghao is None: # 确保 xinghao 为 None 时不会报错
|
||||
xinghao = {"model_name": "", "ship_name": ""}
|
||||
print("未找到匹配的舰船信息")
|
||||
|
||||
# 安全处理 xinghao 为 None 的情况
|
||||
model_name = ""
|
||||
if xinghao and 'model_name' in xinghao:
|
||||
model_name = xinghao['model_name']
|
||||
|
||||
other_data = format_entity_text(final_result)
|
||||
logger.info(f"文件名实体提取成功: {other_data}")
|
||||
except Exception as e:
|
||||
logger.warning(f"文件名实体提取失败: {e}")
|
||||
other_data = ""
|
||||
model_name = "" # 确保异常时 model_name 有定义
|
||||
|
||||
# 将提取的信息(如舰艇名、型号)注入到每个切片的开头
|
||||
for ins in result:
|
||||
content = ins['content']
|
||||
# 如果没有提取到有效数据,则跳过注入
|
||||
|
||||
newline_idx = content.find('\n')
|
||||
info = other_data
|
||||
if model_name:
|
||||
info += f',型号为{model_name}'
|
||||
suffix = f'({info})'
|
||||
|
||||
# 将信息插入到第一行末尾
|
||||
if newline_idx == -1:
|
||||
ins['content'] = content + suffix
|
||||
else:
|
||||
ins['content'] = content[:newline_idx] + suffix + content[newline_idx:]
|
||||
|
||||
return result
|
||||
def find_and_read_content_list(directory, original_filename, encoding='utf-8'):
|
||||
"""
|
||||
@ -1324,7 +998,8 @@ async def process_pdf_file(pdf_path: Path, image_prefix: str,filename:str) -> Op
|
||||
slices_check = chunk_check(slices, 8000)
|
||||
slices_check = merge_short_slices(slices_check, min_length=30,filename=filename)
|
||||
return {"slices": slices_check, "images": images}
|
||||
|
||||
else:
|
||||
return {"slices": [], "images": {}}
|
||||
api_url = get_api_url()
|
||||
try:
|
||||
async with aiofiles.open(pdf_path, "rb") as f:
|
||||
@ -1346,9 +1021,8 @@ async def process_pdf_file(pdf_path: Path, image_prefix: str,filename:str) -> Op
|
||||
slices = get_chunk_bbox(replaced_content_list)
|
||||
slices_check = chunk_check(slices, 8000)
|
||||
slices_check = merge_short_slices(slices_check, min_length=30,filename=filename)
|
||||
print(11111111111111111111111111111111111111)
|
||||
print(len(images))
|
||||
return {"slices": slices_check, "images": images}
|
||||
|
||||
return {"slices": [], "images": {}}
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error("❌ 响应不是有效的 JSON 格式")
|
||||
@ -1370,49 +1044,10 @@ async def process_other_file(pdf_path: Path, image_prefix: str, filename: str) -
|
||||
replaced_content_list = data_replace(content_list, image_prefix)
|
||||
slices = get_chunk_bbox(replaced_content_list)
|
||||
slices_check = chunk_check(slices, 8000)
|
||||
slices_check = merge_short_slices(slices_check, min_length=30,filename=filename)
|
||||
return {"slices": slices_check, "images": images}
|
||||
|
||||
api_url = get_api_url()
|
||||
analyze_other_url = api_url.replace("/analyze-pdf", "/analyze-otherfile")
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(pdf_path.name)
|
||||
content_type = mime_type or "application/octet-stream"
|
||||
|
||||
try:
|
||||
async with aiofiles.open(pdf_path, "rb") as f:
|
||||
file_content = await f.read()
|
||||
|
||||
files = {"file": (pdf_path.name, file_content, content_type)}
|
||||
|
||||
async with httpx.AsyncClient(timeout=60 * 20) as client:
|
||||
response = await client.post(analyze_other_url, files=files)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"❌ Office 文件 API 请求失败,状态码: {response.status_code}, 响应: {response.text}")
|
||||
return None
|
||||
|
||||
result = response.json()
|
||||
data = result.get("data", {})
|
||||
content_list = data.get("content_list", [])
|
||||
images = data.get("images", {})
|
||||
|
||||
# 后续的数据替换与切片处理逻辑
|
||||
replaced_content_list = data_replace(content_list, image_prefix)
|
||||
slices = get_chunk_bbox(replaced_content_list)
|
||||
slices_check = chunk_check(slices, 8000)
|
||||
|
||||
return {"slices": slices_check, "images": images}
|
||||
|
||||
except json.JSONDecodeError:
|
||||
error_text = response.text if 'response' in locals() else "请求未到达服务器"
|
||||
logger.error(f"❌ 响应不是有效的 JSON 格式: {error_text}")
|
||||
else:
|
||||
return {"slices": [], "images": {}}
|
||||
except Exception as e:
|
||||
logger.error(f"处理 Office 文件时出错: {e}", exc_info=True)
|
||||
return None
|
||||
finally:
|
||||
release_api_url(api_url)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
filepath = r"E:\ZKYNLP\Hjunproject\project0506\kgrag\122-06A0014-B01001_发动机-维修手册_content_list.json"
|
||||
|
||||
15
config.py
15
config.py
@ -228,18 +228,3 @@ API_URLS = [
|
||||
#"http://192.168.0.111:9980/analyze-pdf",
|
||||
# "http://192.168.0.111:9975/analyze-pdf",
|
||||
]
|
||||
# ==================== 切片图片处理配置 ====================
|
||||
MAX_SPLIT_IMAGE_INPUT_BYTES = 50 * 1024 * 1024
|
||||
MAX_SPLIT_IMAGE_BYTES = 10 * 1024 * 1024
|
||||
MAX_SPLIT_IMAGE_BASE64_CHARS = (MAX_SPLIT_IMAGE_INPUT_BYTES * 4 // 3) + 4096
|
||||
SPLIT_IMAGE_COMPRESS_THRESHOLD_BYTES = MAX_SPLIT_IMAGE_BYTES
|
||||
SPLIT_IMAGE_MAX_DIMENSION = 4096
|
||||
SPLIT_IMAGE_JPEG_QUALITY = 88
|
||||
OCR_CONCURRENCY = 1
|
||||
VLM_CONCURRENCY = 2
|
||||
|
||||
SHIP_MODEL_NAME = "/app/ship_xinghao.json"
|
||||
TREE_JSON_PATH = "/app/tree_data.json"
|
||||
# ==================== mineru和kgrag目录映射地址 ====================
|
||||
SEARCH_DIR ="/app/mineru_output"
|
||||
IMAGE_DIR = "/app/mineru_output/images"
|
||||
|
||||
@ -1,857 +0,0 @@
|
||||
"""
|
||||
PDF 用户上传接口(FastAPI)
|
||||
- 处理文件名和其对应关系
|
||||
"""
|
||||
|
||||
from typing import Dict, Tuple
|
||||
import os
|
||||
import re
|
||||
from typing import Dict
|
||||
from fastapi import FastAPI, HTTPException
|
||||
import logging
|
||||
import json
|
||||
from modelsAPI.model_api import OpenaiAPI
|
||||
from openai import OpenAI
|
||||
from typing import Dict, List, Any
|
||||
from config import TREE_JSON_PATH
|
||||
|
||||
app = FastAPI(title="PDF Upload Service")
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
|
||||
logger = logging.getLogger("filename_neo4j_process")
|
||||
|
||||
|
||||
# =========================
|
||||
# 图方法
|
||||
# =========================
|
||||
|
||||
|
||||
def node_to_json(node):
|
||||
props = dict(node)
|
||||
# ks_raw = props.get("knowledge_source")
|
||||
# knowledge_source = json.loads(ks_raw) if ks_raw else []
|
||||
|
||||
ks_raw = props.get("knowledge_source")
|
||||
knowledge_source = []
|
||||
|
||||
if ks_raw and isinstance(ks_raw, str):
|
||||
try:
|
||||
ks_list = json.loads(ks_raw)
|
||||
if isinstance(ks_list, list):
|
||||
for ks in ks_list:
|
||||
if isinstance(ks, dict):
|
||||
ks.pop("info", None)
|
||||
knowledge_source.append(ks)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# 解析失败或类型错误时,保持 knowledge_source 为空列表
|
||||
pass
|
||||
|
||||
name = props.get("名称")
|
||||
props.pop("embedding", None)
|
||||
props.pop("knowledge_source", None)
|
||||
props.pop("name", None) # 现在安全地移除
|
||||
props.pop("fulltext", None)
|
||||
if "last_updated" in props:
|
||||
props["最后更新时间"] = props.pop("last_updated")
|
||||
|
||||
if "created_at" in props:
|
||||
props["创建时间"] = props.pop("created_at")
|
||||
return {
|
||||
"id": node.element_id,
|
||||
"name": name, # 使用提前保存的值
|
||||
"labels": list(node.labels),
|
||||
"knowledge_source": knowledge_source,
|
||||
"properties": props,
|
||||
}
|
||||
|
||||
|
||||
def rel_to_json(rel):
|
||||
props = dict(rel)
|
||||
props.pop("embedding", None)
|
||||
props.pop("fact_timeline", None)
|
||||
return {"id": rel.element_id, "label": rel.type, "source": rel.start_node.element_id, "target": rel.end_node.element_id, "properties": props}
|
||||
|
||||
|
||||
def is_simple_path(path):
|
||||
"""
|
||||
判断是否为无环路径(simple path)
|
||||
"""
|
||||
seen = set()
|
||||
for n in path.nodes:
|
||||
nid = n.element_id
|
||||
if nid in seen:
|
||||
return False
|
||||
seen.add(nid)
|
||||
return True
|
||||
|
||||
def build_tree(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
按照固定层级构建树:舰船(根) -> 舰艇 -> 系统 -> 子系统
|
||||
不再依赖 links 的连线,直接根据 labels 归类,彻底避免环状数据导致的死循环。
|
||||
默认只展开第一艘舰艇的系统/子系统,其它舰艇 children 置空(前端可点击按需加载)。
|
||||
"""
|
||||
|
||||
# 1. 辅助函数:判断标签是否包含关键字
|
||||
def has_label(node, keyword):
|
||||
labels = node.get("labels", [])
|
||||
if isinstance(labels, list):
|
||||
return any(keyword in lbl for lbl in labels)
|
||||
return False
|
||||
|
||||
# 2. 辅助函数:构建标准的树节点格式
|
||||
def make_node(node_data):
|
||||
return {
|
||||
"id": node_data["id"],
|
||||
"name": node_data.get("name", ""),
|
||||
"labels": node_data.get("labels", []),
|
||||
|
||||
"properties": node_data.get("properties", {}),
|
||||
"color": node_data.get("color"),
|
||||
"size": node_data.get("size"),
|
||||
"children": []
|
||||
}
|
||||
|
||||
# 3. 将节点按层级分类存储到字典中,方便快速查找
|
||||
fleet_roots = {} # 第零层:舰船/舰号(父根节点)
|
||||
ships = {} # 第一层:舰艇
|
||||
systems = {} # 第二层:系统(不含子系统)
|
||||
sub_systems = {} # 第三层:子系统
|
||||
|
||||
for node in nodes:
|
||||
# 注意:判断逻辑要互斥,防止"子系统"被误判为"系统"
|
||||
if has_label(node, "舰船") or has_label(node, "舰号"):
|
||||
fleet_roots[node["id"]] = make_node(node)
|
||||
elif has_label(node, "舰艇"):
|
||||
ships[node["id"]] = make_node(node)
|
||||
elif has_label(node, "子系统"):
|
||||
sub_systems[node["id"]] = make_node(node)
|
||||
elif has_label(node, "系统"):
|
||||
systems[node["id"]] = make_node(node)
|
||||
else:
|
||||
pass
|
||||
|
||||
# 4. 组装系统 -> 子系统、舰艇 -> 系统 关系(先把完整子树挂好)
|
||||
for link in links:
|
||||
if link.get("label") != "包含":
|
||||
continue
|
||||
|
||||
src_id = link["source"]
|
||||
tgt_id = link["target"]
|
||||
|
||||
if src_id in ships and tgt_id in systems:
|
||||
ships[src_id]["children"].append(systems[tgt_id])
|
||||
elif src_id in systems and tgt_id in sub_systems:
|
||||
systems[src_id]["children"].append(sub_systems[tgt_id])
|
||||
elif src_id in ships and tgt_id in sub_systems:
|
||||
ships[src_id]["children"].append(sub_systems[tgt_id])
|
||||
|
||||
# 5. 所有舰艇 children 全部保留,仅用 collapsed 标记默认折叠状态。
|
||||
# 前端读取 collapsed=False 的舰艇默认展开,其它折叠;
|
||||
# 用户点击折叠的舰艇时,前端切换 collapsed 即可展示子树。
|
||||
# 只保留名称包含 "122" 的舰艇(隐藏 JZ贱 等其它分支)
|
||||
#sorted_ships = sorted(
|
||||
# (s for s in ships.values() if "122" in s.get("name", "")),
|
||||
# key=lambda s: s.get("name", ""),
|
||||
#)
|
||||
sorted_ships = sorted(ships.values(), key=lambda s: s.get("name", ""))
|
||||
for idx, ship in enumerate(sorted_ships):
|
||||
ship["collapsed"] = (idx != 0)
|
||||
|
||||
# 6. 挂载到舰船根节点;如果数据里没有舰船根,就创建一个虚拟根
|
||||
if fleet_roots:
|
||||
# 取第一个舰船根节点作为父(一般场景下只有一个)
|
||||
root = next(iter(fleet_roots.values()))
|
||||
root["children"] = sorted_ships
|
||||
return [root]
|
||||
else:
|
||||
virtual_root = {
|
||||
"id": "__fleet_root__",
|
||||
"name": "舰号",
|
||||
"labels": ["舰船"],
|
||||
"properties": {},
|
||||
"color": None,
|
||||
"size": None,
|
||||
"children": sorted_ships,
|
||||
}
|
||||
return [virtual_root]
|
||||
def build_tree1(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
从 nodes + links 构建树结构。
|
||||
- 根节点:label 包含 '舰艇' 的节点
|
||||
- 父子关系:links 中 label == '包含' 的边 (source -> target)
|
||||
- 支持多棵树(多个舰艇根节点)
|
||||
"""
|
||||
|
||||
# 1. 建立 id -> node 的映射
|
||||
node_map: Dict[str, Dict] = {n["id"]: n for n in nodes}
|
||||
|
||||
# 2. 建立 父id -> [子id列表] 的邻接表(只处理"包含"关系)
|
||||
children_map: Dict[str, List[str]] = {n["id"]: [] for n in nodes}
|
||||
parent_map: Dict[str, str] = {} # 子id -> 父id,用于识别根节点
|
||||
|
||||
for link in links:
|
||||
if link.get("label") == "包含":
|
||||
src = link["source"]
|
||||
tgt = link["target"]
|
||||
if src in children_map:
|
||||
children_map[src].append(tgt)
|
||||
if tgt not in parent_map:
|
||||
parent_map[tgt] = src
|
||||
|
||||
# 3. 递归构建子树
|
||||
def build_subtree(node_id: str, ancestors: set = None) -> Dict[str, Any]:
|
||||
if ancestors is None:
|
||||
ancestors = set()
|
||||
if node_id in ancestors:
|
||||
return None
|
||||
ancestors.add(node_id)
|
||||
node = node_map[node_id]
|
||||
subtree = {"id": node_id, "name": node.get("name", ""), "labels": node.get("labels", []), "properties": node.get("properties", {}), "color": node.get("color"), "size": node.get("size"), "children": []}
|
||||
current_ancestors = ancestors | {node_id}
|
||||
for child_id in children_map.get(node_id, []):
|
||||
child = build_subtree(child_id, current_ancestors)
|
||||
if child is not None:
|
||||
subtree["children"].append(child)
|
||||
return subtree
|
||||
|
||||
# def build_subtree(node_id: str) -> Dict[str, Any]:
|
||||
# node = node_map[node_id]
|
||||
# subtree = {
|
||||
# "id": node_id,
|
||||
# "name": node.get("name", ""),
|
||||
# "labels": node.get("labels", []),
|
||||
# "properties": node.get("properties", {}),
|
||||
# "color": node.get("color"),
|
||||
# "size": node.get("size"),
|
||||
# "children": []
|
||||
# }
|
||||
# for child_id in children_map.get(node_id, []):
|
||||
# subtree["children"].append(build_subtree(child_id))
|
||||
# return subtree
|
||||
|
||||
# 4. 找出所有根节点:label 包含 '舰艇',且没有父节点
|
||||
roots = [n["id"] for n in nodes if "舰艇" in n.get("labels", []) and n["id"] not in parent_map]
|
||||
|
||||
# 5. 为每棵树构建结构,并打上树编号
|
||||
forest = []
|
||||
for i, root_id in enumerate(roots):
|
||||
tree = build_subtree(root_id)
|
||||
tree["tree_index"] = i # 区分不同树
|
||||
forest.append(tree)
|
||||
|
||||
return forest
|
||||
|
||||
|
||||
def _filter_searchable(graph: dict) -> dict:
|
||||
"""剥掉节点的 Searchable 标签;若剥完后 labels 为空则整体移除该节点及其关联边"""
|
||||
for n in graph["nodes"]:
|
||||
n["labels"] = [l for l in n.get("labels", []) if l != "Searchable"]
|
||||
valid_ids = {n["id"] for n in graph["nodes"] if n["labels"]}
|
||||
return {
|
||||
"nodes": [n for n in graph["nodes"] if n["id"] in valid_ids],
|
||||
"links": [
|
||||
l for l in graph["links"]
|
||||
if l["source"] in valid_ids and l["target"] in valid_ids
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_graph_from_paths(paths):
|
||||
"""
|
||||
核心公共方法
|
||||
从 Neo4j 路径结果构建 graphData
|
||||
"""
|
||||
node_map: Dict[str, dict] = {}
|
||||
link_map: Dict[str, dict] = {}
|
||||
|
||||
def add_edge(rel):
|
||||
s = rel.start_node
|
||||
t = rel.end_node
|
||||
|
||||
if s.element_id not in node_map:
|
||||
node_map[s.element_id] = node_to_json(s)
|
||||
|
||||
if t.element_id not in node_map:
|
||||
node_map[t.element_id] = node_to_json(t)
|
||||
|
||||
if rel.element_id not in link_map:
|
||||
link_map[rel.element_id] = rel_to_json(rel)
|
||||
|
||||
for record in paths:
|
||||
p = record["p"]
|
||||
|
||||
if not is_simple_path(p):
|
||||
continue
|
||||
|
||||
for r in p.relationships:
|
||||
add_edge(r)
|
||||
|
||||
return _filter_searchable({"nodes": list(node_map.values()), "links": list(link_map.values())})
|
||||
|
||||
|
||||
# def fetch_graph_sample(driver,limit: int = 200):
|
||||
# """提取采样逻辑为可复用函数"""
|
||||
# with driver.session() as session:
|
||||
# result = session.run("""
|
||||
# MATCH (n)
|
||||
# WITH n
|
||||
# ORDER BY elementId(n) ASC
|
||||
# LIMIT $limit
|
||||
# MATCH (n)-[rel]->(m)
|
||||
# RETURN n, rel, m
|
||||
# """, limit=limit)
|
||||
|
||||
# node_map, link_map = {}, {}
|
||||
# for rec in result:
|
||||
# n, r, m = rec["n"], rec["rel"], rec["m"]
|
||||
# node_map[n.element_id] = node_to_json(n)
|
||||
# node_map[m.element_id] = node_to_json(m)
|
||||
# link_map[r.element_id] = rel_to_json(r)
|
||||
|
||||
# return {
|
||||
# "nodes": list(node_map.values()),
|
||||
# "links": list(link_map.values())
|
||||
# }
|
||||
|
||||
# def fetch_graph_sample(driver, limit: int = 200):
|
||||
# """
|
||||
# 获取采样数据:
|
||||
# 1. 选取前 $limit 个种子节点(Level 0 或 1,取决于业务定义,这里定义种子为 Level 0)。
|
||||
# 2. 对每个节点向下探索最多 3 跳。
|
||||
# 3. 每个节点对象中增加 level 字段,表示其距离种子节点的深度。
|
||||
# """
|
||||
# with driver.session() as session:
|
||||
# # Cypher 逻辑:
|
||||
# # - 选出种子节点 n
|
||||
# # - 匹配 1-3 跳路径
|
||||
# # - 返回种子节点 n,路径 path,以及该路径的长度(即层级)
|
||||
# result = session.run("""
|
||||
# MATCH (n)
|
||||
# WITH n ORDER BY elementId(n) ASC LIMIT $limit
|
||||
# OPTIONAL MATCH path = (n)-[*1..3]-(m)
|
||||
# UNWIND nodes(path) AS x
|
||||
# WITH n, path, m, COUNT(DISTINCT x) AS nodeCount
|
||||
# WHERE path IS NULL OR nodeCount = LENGTH(path) + 1
|
||||
# RETURN n, path, LENGTH(path) AS depth
|
||||
# """, limit=limit)
|
||||
|
||||
# node_map, link_map = {}, {}
|
||||
|
||||
# for rec in result:
|
||||
# seed_node = rec["n"]
|
||||
# path = rec["path"]
|
||||
# depth = rec["depth"] or 0 # 如果没有路径,深度为 0
|
||||
|
||||
# # 1. 处理种子节点 (Level 0)
|
||||
# if seed_node.element_id not in node_map:
|
||||
# node_data = node_to_json(seed_node)
|
||||
# node_data["level"] = 0 # 种子节点设为 0 层
|
||||
# node_map[seed_node.element_id] = node_data
|
||||
|
||||
# # 2. 如果存在路径,解析路径中的所有节点和关系
|
||||
# if path:
|
||||
# # 路径中的节点处理
|
||||
# # path.nodes 包含了从起始到终点的所有节点
|
||||
# for i, node in enumerate(path.nodes):
|
||||
# if node.element_id not in node_map:
|
||||
# node_data = node_to_json(node)
|
||||
# # 层级即为该节点在当前路径中的索引
|
||||
# node_data["level"] = i
|
||||
# node_map[node.element_id] = node_data
|
||||
# else:
|
||||
# # 如果节点已存在,保留最小的 level (即最靠近种子的距离)
|
||||
# node_map[node.element_id]["level"] = min(node_map[node.element_id].get("level", 3), i)
|
||||
|
||||
# # 路径中的关系处理
|
||||
# for rel in path.relationships:
|
||||
# if rel.element_id not in link_map:
|
||||
# link_map[rel.element_id] = rel_to_json(rel)
|
||||
|
||||
# return {
|
||||
# "nodes": list(node_map.values()),
|
||||
# "links": list(link_map.values())
|
||||
# }
|
||||
|
||||
|
||||
# def fetch_graph_sample(driver, limit=200, max_depth=3):
|
||||
# with driver.session() as session:
|
||||
# result = session.run("""
|
||||
# MATCH (c)
|
||||
# WITH c ORDER BY elementId(c) ASC LIMIT $limit
|
||||
|
||||
# MATCH path = (c)-[*0..3]-(n)
|
||||
# WITH n, MIN(length(path)) AS dist
|
||||
# OPTIONAL MATCH (n)-[r]-(m)
|
||||
|
||||
# RETURN n, dist, r, m
|
||||
# """, limit=limit)
|
||||
|
||||
# node_map, link_map = {}, {}
|
||||
|
||||
# for rec in result:
|
||||
# n = rec["n"]
|
||||
# dist = rec["dist"]
|
||||
# r = rec["r"]
|
||||
# m = rec["m"]
|
||||
|
||||
# # ---- 节点 ----
|
||||
# nid = n.element_id
|
||||
# if nid not in node_map:
|
||||
# node_data = node_to_json(n)
|
||||
# node_data["level"] = min(dist, 3) if dist is not None else 0
|
||||
# node_map[nid] = node_data
|
||||
|
||||
# # ---- 边 ----
|
||||
# if r and m:
|
||||
# mid = m.element_id
|
||||
# if mid not in node_map:
|
||||
# node_map[mid] = node_to_json(m)
|
||||
|
||||
# link_map.setdefault(r.element_id, rel_to_json(r))
|
||||
|
||||
# return {
|
||||
# "nodes": list(node_map.values()),
|
||||
# "links": list(link_map.values())
|
||||
# }
|
||||
|
||||
def fetch_graph_sample(driver):
|
||||
"""
|
||||
查询所有舰艇及其关联的系统和子系统
|
||||
路径结构:(舰艇) -> (系统) -> (子系统)
|
||||
"""
|
||||
|
||||
with driver.session() as session:
|
||||
# =========================
|
||||
# 统一查询:舰艇 -> 系统 -> 子系统
|
||||
# =========================
|
||||
# 逻辑说明:
|
||||
# 1. MATCH (ship:舰艇):选中所有舰艇
|
||||
# 2. -[*1..3]->(target):查找深度为 1 到 3 的路径
|
||||
# - 深度 1:舰艇 -> 系统
|
||||
# - 深度 2:舰艇 -> 系统 -> 子系统
|
||||
# 3. WHERE target:系统 OR target:子系统:确保终点是我们关心的节点类型
|
||||
# (防止查询出其他无关的深层节点)
|
||||
|
||||
query = """
|
||||
MATCH path = (ship:舰艇)-[*1..3]->(target)
|
||||
WHERE target:系统 OR target:子系统
|
||||
RETURN path
|
||||
"""
|
||||
|
||||
result = session.run(query)
|
||||
|
||||
# =========================
|
||||
# 解析 paths
|
||||
# =========================
|
||||
all_nodes = {}
|
||||
all_links = {}
|
||||
|
||||
for record in result:
|
||||
path = record["path"]
|
||||
|
||||
# 解析节点
|
||||
for node in path.nodes:
|
||||
# 兼容 ID 获取
|
||||
node_key = node.element_id if hasattr(node, 'element_id') else node.id
|
||||
if node_key not in all_nodes:
|
||||
all_nodes[node_key] = node_to_json(node)
|
||||
|
||||
# 解析关系
|
||||
for rel in path.relationships:
|
||||
rel_key = rel.element_id if hasattr(rel, 'element_id') else rel.id
|
||||
if rel_key not in all_links:
|
||||
all_links[rel_key] = rel_to_json(rel)
|
||||
|
||||
return _filter_searchable({
|
||||
"nodes": list(all_nodes.values()),
|
||||
"links": list(all_links.values())
|
||||
})
|
||||
def fetch_graph_sample1(driver):
|
||||
"""
|
||||
查找路径:
|
||||
1. 优先:舰艇 -> 子系统
|
||||
2. fallback:舰艇 -> 系统
|
||||
|
||||
注意:根据数据库实际结构,使用 Label (:舰艇, :子系统) 进行匹配,
|
||||
而不是 WHERE category = '...'
|
||||
"""
|
||||
|
||||
# 定义采样数量
|
||||
SAMPLE_LIMIT = 400
|
||||
|
||||
with driver.session() as session:
|
||||
# =========================
|
||||
# 第一阶段:舰艇 -> 子系统
|
||||
# =========================
|
||||
# 修改点:直接使用 (:舰艇) 和 (:子系统) 标签匹配,去掉 WHERE 子句
|
||||
result = session.run(
|
||||
"""
|
||||
MATCH (ship:舰艇)
|
||||
WITH ship LIMIT $limit
|
||||
|
||||
MATCH path = (ship)-[*1..3]->(sub:子系统)
|
||||
RETURN path
|
||||
""",
|
||||
limit=SAMPLE_LIMIT,
|
||||
)
|
||||
|
||||
paths = [record["path"] for record in result]
|
||||
|
||||
# =========================
|
||||
# fallback:舰艇 -> 系统
|
||||
# =========================
|
||||
if not paths:
|
||||
# 修改点:直接使用 (:舰艇) 和 (:系统) 标签匹配
|
||||
result = session.run(
|
||||
"""
|
||||
MATCH (ship:舰艇)
|
||||
WITH ship LIMIT $limit
|
||||
|
||||
MATCH path = (ship)-[*1..2]->(sys:系统)
|
||||
RETURN path
|
||||
""",
|
||||
limit=SAMPLE_LIMIT,
|
||||
)
|
||||
|
||||
paths = [record["path"] for record in result]
|
||||
|
||||
# =========================
|
||||
# 解析 paths
|
||||
# =========================
|
||||
all_nodes = {}
|
||||
all_links = {}
|
||||
|
||||
for path in paths:
|
||||
# 节点
|
||||
for node in path.nodes:
|
||||
# 兼容 Neo4j 5.x (element_id) 和 4.x (id)
|
||||
node_key = node.element_id if hasattr(node, "element_id") else node.id
|
||||
if node_key not in all_nodes:
|
||||
all_nodes[node_key] = node_to_json(node)
|
||||
|
||||
# 关系
|
||||
for rel in path.relationships:
|
||||
rel_key = rel.element_id if hasattr(rel, "element_id") else rel.id
|
||||
if rel_key not in all_links:
|
||||
all_links[rel_key] = rel_to_json(rel)
|
||||
|
||||
return {"nodes": list(all_nodes.values()), "links": list(all_links.values())}
|
||||
|
||||
|
||||
# =============================
|
||||
# 文件名处理方法
|
||||
# =============================
|
||||
|
||||
# BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
|
||||
|
||||
|
||||
def _load_tree_data() -> Dict:
|
||||
# 注意:实际运行时请确保 result.json 存在,此处仅为代码结构展示
|
||||
if not os.path.exists(TREE_JSON_PATH):
|
||||
return {}
|
||||
with open(TREE_JSON_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _resolve_ship_root(data: Dict, ship_code: str) -> Dict:
|
||||
raw = data.get(ship_code)
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
|
||||
# 结构 1:直接是节点 {"code","name","children"}
|
||||
if {"code", "name", "children"}.issubset(raw.keys()):
|
||||
return raw
|
||||
|
||||
# 结构 2:包装层 {"101": {...root node...}}
|
||||
if ship_code in raw and isinstance(raw[ship_code], dict):
|
||||
node = raw[ship_code]
|
||||
if {"code", "name", "children"}.issubset(node.keys()):
|
||||
return node
|
||||
|
||||
# 兜底:取第一个 node-like 值
|
||||
for value in raw.values():
|
||||
if isinstance(value, dict) and {"code", "name", "children"}.issubset(value.keys()):
|
||||
return value
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _longest_common_substring_len(a: str, b: str) -> int:
|
||||
if not a or not b:
|
||||
return 0
|
||||
# 优化:确保 a 是较短的字符串以节省空间(可选),原逻辑保持不动也没问题
|
||||
dp = [0] * (len(b) + 1)
|
||||
best = 0
|
||||
for i in range(1, len(a) + 1):
|
||||
prev = 0
|
||||
for j in range(1, len(b) + 1):
|
||||
temp = dp[j]
|
||||
if a[i - 1] == b[j - 1]:
|
||||
dp[j] = prev + 1
|
||||
if dp[j] > best:
|
||||
best = dp[j]
|
||||
else:
|
||||
dp[j] = 0
|
||||
prev = temp
|
||||
return best
|
||||
|
||||
|
||||
def _match_level4_tool(filename_text: str, device_node: Dict) -> Tuple[str, str]:
|
||||
"""
|
||||
匹配 Level 4 零件。
|
||||
规则:如果文件名与 Level 3 (设备名) 的最大共同字符串长度 >= 与任何 Level 4 (零件名) 的长度,
|
||||
则只匹配到 Level 3,返回空。
|
||||
只有当 Level 4 的匹配度严格高于 Level 3 时,才返回 Level 4 信息。
|
||||
"""
|
||||
children = device_node.get("children") or {}
|
||||
|
||||
# 1. 计算文件名与当前设备 (Level 3) 名称的匹配度
|
||||
device_name = str(device_node.get("name") or "")
|
||||
level3_score = _longest_common_substring_len(filename_text, device_name)
|
||||
|
||||
best_name = ""
|
||||
best_code = ""
|
||||
best_score = 0 # 记录 Level 4 中的最佳得分
|
||||
|
||||
# 2. 遍历 Level 4 子节点
|
||||
for child in children.values():
|
||||
tool_name = str(child.get("name") or "")
|
||||
tool_code = str(child.get("code") or "")
|
||||
|
||||
score = _longest_common_substring_len(filename_text, tool_name)
|
||||
|
||||
# 关键修改:只有当 Level 4 的得分严格大于 Level 3 的得分时,才视为有效匹配
|
||||
# 并且要比当前找到的其他 Level 4 更好
|
||||
if score > level3_score and score > best_score:
|
||||
best_score = score
|
||||
best_name = tool_name
|
||||
best_code = tool_code
|
||||
|
||||
# 如果没有找到比 Level 3 匹配度更高的 Level 4,则返回空
|
||||
if best_score <= level3_score:
|
||||
return "", ""
|
||||
|
||||
return best_name, best_code
|
||||
|
||||
|
||||
def process_filename(filename: str) -> Dict[str, str]:
|
||||
name, _ = os.path.splitext(os.path.basename(filename))
|
||||
parts = re.split(r"[_-]", name)
|
||||
|
||||
xian_number = ""
|
||||
level_1_system_name = ""
|
||||
level_2_system_name = ""
|
||||
device_name = ""
|
||||
tool_name = ""
|
||||
tool_code = ""
|
||||
|
||||
if not parts:
|
||||
return {}
|
||||
|
||||
xian_number_code = parts[0].strip()
|
||||
level1_code = ""
|
||||
level2_code = ""
|
||||
device_code = ""
|
||||
|
||||
if len(parts) > 1:
|
||||
part = parts[1].strip()
|
||||
# 防止索引越界
|
||||
if len(part) < 2:
|
||||
return {}
|
||||
|
||||
level1_code = part[:2]
|
||||
level2_code = part[2:4] if len(part) > 2 else ""
|
||||
device_code = part[4:] if len(part) > 4 else ""
|
||||
# 若 parts[2] 存在,则用 parts[2] 作为 device_code(支持 122-06A0014-B01001_发动机 格式)
|
||||
if len(parts) > 2:
|
||||
device_code_alt = parts[2].strip()
|
||||
if device_code_alt:
|
||||
device_code = device_code_alt
|
||||
|
||||
data = _load_tree_data()
|
||||
root = _resolve_ship_root(data, xian_number_code)
|
||||
if not root:
|
||||
return {}
|
||||
|
||||
xian_number = root.get("name", "")
|
||||
|
||||
# 一级
|
||||
s1 = root.get("children", {}).get(level1_code)
|
||||
if s1:
|
||||
level_1_system_name = s1.get("name", "")
|
||||
|
||||
# 二级
|
||||
s2 = s1.get("children", {}).get(level2_code)
|
||||
if s2:
|
||||
level_2_system_name = s2.get("name", "")
|
||||
|
||||
# 三级设备
|
||||
device = s2.get("children", {}).get(device_code)
|
||||
if device:
|
||||
device_name = device.get("name", "")
|
||||
# 若存在 level4,匹配与文件名最大相同字符串的零件
|
||||
# 内部已包含逻辑:如果设备名匹配度更高,则不返回零件
|
||||
tool_name, tool_code = _match_level4_tool(name, device)
|
||||
|
||||
return {
|
||||
"Xian_Number": xian_number,
|
||||
"Xian_Number_code": xian_number_code,
|
||||
"Level_1_System_Name": level_1_system_name,
|
||||
"level1_code": level1_code,
|
||||
"Level_2_System_Name": level_2_system_name,
|
||||
"level2_code": level2_code,
|
||||
"Device_Name": device_name,
|
||||
"device_code": device_code,
|
||||
"tool_name": tool_name,
|
||||
"tool_code": tool_code,
|
||||
}
|
||||
|
||||
|
||||
def get_entity(filename):
|
||||
result = process_filename(filename)
|
||||
entities = []
|
||||
relationships = []
|
||||
low_level = ""
|
||||
|
||||
if result:
|
||||
xian_number = result.get("Xian_Number")
|
||||
xian_number_code = result.get("Xian_Number_code")
|
||||
level_1_system_name = result.get("Level_1_System_Name")
|
||||
level1_code = result.get("level1_code")
|
||||
level_2_system_name = result.get("Level_2_System_Name")
|
||||
level2_code = result.get("level2_code")
|
||||
device_name = result.get("Device_Name")
|
||||
device_code = result.get("device_code")
|
||||
tool_name = result.get("tool_name")
|
||||
tool_code = result.get("tool_code")
|
||||
|
||||
if xian_number:
|
||||
entities.append(
|
||||
{
|
||||
"type": "舰艇",
|
||||
"properties": {
|
||||
"名称": xian_number,
|
||||
"舷号": xian_number_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
low_level = xian_number
|
||||
|
||||
if level_1_system_name:
|
||||
entities.append(
|
||||
{
|
||||
"type": "系统",
|
||||
"properties": {
|
||||
"名称": level_1_system_name,
|
||||
"系统编码": level1_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
low_level = level_1_system_name
|
||||
|
||||
if xian_number and level_1_system_name:
|
||||
relationships.append(
|
||||
{
|
||||
"type": "包含",
|
||||
"from_entity": xian_number,
|
||||
"to_entity": level_1_system_name,
|
||||
}
|
||||
)
|
||||
|
||||
if level_2_system_name:
|
||||
entities.append(
|
||||
{
|
||||
"type": "子系统",
|
||||
"properties": {
|
||||
"名称": level_2_system_name,
|
||||
"子系统编码": level2_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
low_level = level_2_system_name
|
||||
|
||||
if level_1_system_name and level_2_system_name:
|
||||
relationships.append(
|
||||
{
|
||||
"type": "包含",
|
||||
"from_entity": level_1_system_name,
|
||||
"to_entity": level_2_system_name,
|
||||
}
|
||||
)
|
||||
|
||||
if device_name:
|
||||
entities.append(
|
||||
{
|
||||
"type": "设备",
|
||||
"properties": {
|
||||
"名称": device_name,
|
||||
"设备编码": device_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
low_level = device_name
|
||||
|
||||
if level_2_system_name and device_name:
|
||||
relationships.append(
|
||||
{
|
||||
"type": "包含",
|
||||
"from_entity": level_2_system_name,
|
||||
"to_entity": device_name,
|
||||
}
|
||||
)
|
||||
|
||||
if tool_name:
|
||||
entities.append(
|
||||
{
|
||||
"type": "零件",
|
||||
"properties": {
|
||||
"名称": tool_name,
|
||||
"零件编码": tool_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
low_level = tool_name
|
||||
|
||||
if device_name and tool_name:
|
||||
relationships.append(
|
||||
{
|
||||
"type": "包含",
|
||||
"from_entity": device_name,
|
||||
"to_entity": tool_name,
|
||||
}
|
||||
)
|
||||
|
||||
final_result = {
|
||||
"entities": entities,
|
||||
"relationships": relationships,
|
||||
"low_level": low_level,
|
||||
}
|
||||
|
||||
return final_result
|
||||
|
||||
|
||||
# 示例测试逻辑(非必须,仅供验证)
|
||||
if __name__ == "__main__":
|
||||
# 阀控蓄电池脉冲式快速充电装置
|
||||
test_list = [
|
||||
"163-06A0015-B01001_发动机-维修手册.pdf",
|
||||
|
||||
]
|
||||
for i in test_list:
|
||||
result = get_entity(i)
|
||||
lower_entity = result.get("low_level")
|
||||
print(result)
|
||||
print(lower_entity)
|
||||
#from neo4j import GraphDatabase
|
||||
|
||||
#NEO4J_URI = "bolt://192.168.0.111:7687"
|
||||
#NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
|
||||
#NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD","zdht123@") # 不设默认值,强制要求提供
|
||||
#NEO4J_DATABASE = os.getenv("NEO4J_DATABASE", "neo4j")
|
||||
#driver = GraphDatabase.driver(
|
||||
# NEO4J_URI,
|
||||
# auth=(NEO4J_USER, NEO4J_PASSWORD),
|
||||
# database=NEO4J_DATABASE
|
||||
#)
|
||||
#res = fetch_graph_sample(driver)
|
||||
#print(res)
|
||||
@ -5,7 +5,7 @@ import asyncio
|
||||
import logging
|
||||
from typing import Optional, Tuple, Dict
|
||||
import aiofiles # pip install aiofiles
|
||||
from config import SEARCH_DIR,IMAGE_DIR
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -103,6 +103,7 @@ async def encode_images_to_base64(
|
||||
|
||||
async def process_document(
|
||||
input_file_name: str,
|
||||
search_dir: Optional[str] = "/app/mineru_output",
|
||||
local_image_dir: Optional[str] = None,
|
||||
concurrency: int = 32,
|
||||
) -> Optional[Tuple[list, Dict[str, str]]]:
|
||||
@ -119,7 +120,6 @@ async def process_document(
|
||||
成功: (content_list, images_dict)
|
||||
失败: None
|
||||
"""
|
||||
search_dir = SEARCH_DIR
|
||||
content, content_path = await find_and_read_content_list(search_dir, input_file_name)
|
||||
if content is None:
|
||||
logger.error("❌ 未找到指定的 _content_list.json 文件。")
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
from rapidocr_onnxruntime import RapidOCR
|
||||
|
||||
engine = RapidOCR()
|
||||
ocr_result, elapse = engine(r"E:\ZKYNLP\aidocproject\audiotransaction\gwdoc\图片.png")
|
||||
|
||||
texts = [item[1] for item in (ocr_result or [])]
|
||||
full_text = "图片包含的文字内容为:"+",".join(texts)
|
||||
|
||||
|
||||
|
||||
print("\n合并文本:")
|
||||
print(full_text)
|
||||
|
||||
print("\n耗时:", elapse)
|
||||
Binary file not shown.
Binary file not shown.
@ -5,6 +5,7 @@ import os
|
||||
import numpy as np
|
||||
from openai import OpenAI
|
||||
from config import LLM_CONFIG, EMBEDDING_CONFIG
|
||||
from neo4j_graphrag.embeddings.base import Embedder
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
@ -140,7 +141,7 @@ class OpenaiAPI:
|
||||
# 2. 构建强约束的 System Prompt
|
||||
# 核心目标:禁止思考标签,禁止废话,只给结果
|
||||
system_prompt = (
|
||||
"你是一个视觉分析专家。你的任务是从图片中识别内容并进行描述。\n"
|
||||
"你是一个工业视觉分析专家。你的任务是从图片中识别设备并诊断故障。\n"
|
||||
"【严格约束】\n"
|
||||
"1. 绝对禁止输出 <think>, <think>, <reasoning> 等任何思考过程标签。\n"
|
||||
"2. 绝对禁止输出'好的'、'根据图片'、'分析如下'等开场白或结束语。\n"
|
||||
@ -150,7 +151,10 @@ class OpenaiAPI:
|
||||
# 3. 构建针对性的 User Prompt
|
||||
default_task = (
|
||||
"请分析这张图片,描述图片内容,重点关注以下信息\n"
|
||||
"1. 图片整体阐述了什么主题,描述了哪些内容\n"
|
||||
"1. 设备识别:图中是什么设备?型号或标签是否可见?\n"
|
||||
"2. 状态评估:仪表盘数值是否在正常区间?指示灯颜色是什么?\n"
|
||||
"3. 异常检测:是否存在漏油、生锈、电缆破损、螺丝松动或烟雾?\n"
|
||||
"4. 环境风险:周围是否有杂物堆放或安全隐患?\n"
|
||||
"请用专业、客观、简短的语言描述。"
|
||||
)
|
||||
|
||||
@ -181,7 +185,7 @@ class OpenaiAPI:
|
||||
kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
|
||||
|
||||
try:
|
||||
print(f"🚀 正在调用 {model} 进行图片内容分析...")
|
||||
print(f"🚀 正在调用 {model} 进行设备故障分析...")
|
||||
response = await client.chat.completions.create(**kwargs)
|
||||
raw_content = response.choices[0].message.content or ""
|
||||
|
||||
@ -190,7 +194,7 @@ class OpenaiAPI:
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_msg = f"❌ 图片内容分析失败:{str(e)}\n{traceback.format_exc()}"
|
||||
error_msg = f"❌ 设备故障分析失败:{str(e)}\n{traceback.format_exc()}"
|
||||
print(error_msg)
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
@ -349,7 +353,24 @@ class OpenaiAPI:
|
||||
|
||||
return 0.5 * jaccard + 0.5 * len_penalty
|
||||
|
||||
class LocalBgeM3Embeddings(Embedder):
|
||||
def __init__(self, base_url: str, api_key: str, model_name: str = "bge-m3"):
|
||||
self.base_url = base_url
|
||||
self.api_key = api_key
|
||||
self.model_name = model_name
|
||||
|
||||
def embed_query(self, text: str) -> List[float]:
|
||||
return self.embed_documents([text])[0]
|
||||
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
url = self.base_url
|
||||
with httpx.Client(timeout=60) as client:
|
||||
response = client.post(
|
||||
url,
|
||||
json={"model": self.model_name, "input": texts},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
)
|
||||
return [item["embedding"] for item in response.json()["data"]]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 353 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user